Commit 87bb8b913bd958cd13016dc921c46d39ce44ca5e

Authored by Renan
2 parents 392671f2 9823122a
Exists in master

Merge branch 'master' into multimodal_tracking

# Conflicts:
#	invesalius/gui/task_navigator.py
invesalius/constants.py
... ... @@ -828,7 +828,7 @@ TREKKER_CONFIG = {'seed_max': 1, 'step_size': 0.1, 'min_fod': 0.1, 'probe_qualit
828 828 'write_interval': 50, 'numb_threads': '', 'max_lenth': 200,
829 829 'min_lenth': 20, 'max_sampling_step': 100}
830 830  
831   -MARKER_FILE_MAGICK_STRING = "INVESALIUS3_MARKER_FILE_"
  831 +MARKER_FILE_MAGICK_STRING = "##INVESALIUS3_MARKER_FILE_"
832 832 CURRENT_MARKER_FILE_VERSION = 0
833 833 WILDCARD_MARKER_FILES = _("Marker scanner coord files (*.mkss)|*.mkss")
834 834  
... ...
invesalius/control.py
... ... @@ -122,8 +122,6 @@ class Controller():
122 122  
123 123 Publisher.subscribe(self.OnSaveProject, 'Save project')
124 124  
125   - Publisher.subscribe(self.Send_affine, 'Get affine matrix')
126   -
127 125 Publisher.subscribe(self.create_project_from_matrix, 'Create project from matrix')
128 126  
129 127 Publisher.subscribe(self.show_mask_preview, 'Show mask preview')
... ... @@ -506,8 +504,9 @@ class Controller():
506 504 utils.debug("No medical images found on given directory")
507 505 return
508 506  
509   - matrix, matrix_filename = self.OpenOtherFiles(group)
510   - self.CreateOtherProject(str(name[0]), matrix, matrix_filename)
  507 + if group:
  508 + matrix, matrix_filename = self.OpenOtherFiles(group)
  509 + self.CreateOtherProject(str(name[0]), matrix, matrix_filename)
511 510 # OPTION 4: Nothing...
512 511  
513 512 self.LoadProject()
... ... @@ -623,6 +622,11 @@ class Controller():
623 622 Publisher.sendMessage(('Set scroll position', 'AXIAL'), index=proj.matrix_shape[0]/2)
624 623 Publisher.sendMessage(('Set scroll position', 'SAGITAL'),index=proj.matrix_shape[1]/2)
625 624 Publisher.sendMessage(('Set scroll position', 'CORONAL'),index=proj.matrix_shape[2]/2)
  625 +
  626 + if self.Slice.affine is not None:
  627 + Publisher.sendMessage('Enable Go-to-Coord', status=True)
  628 + else:
  629 + Publisher.sendMessage('Enable Go-to-Coord', status=False)
626 630  
627 631 Publisher.sendMessage('End busy cursor')
628 632  
... ... @@ -715,9 +719,6 @@ class Controller():
715 719 proj.matrix_dtype = matrix.dtype.name
716 720 proj.matrix_filename = matrix_filename
717 721  
718   - if self.affine is not None:
719   - proj.affine = self.affine.tolist()
720   -
721 722 # Orientation must be CORONAL in order to as_closes_canonical and
722 723 # swap axis in img2memmap to work in a standardized way.
723 724 # TODO: Create standard import image for all acquisition orientations
... ... @@ -730,6 +731,8 @@ class Controller():
730 731 proj.level = self.Slice.window_level
731 732 proj.threshold_range = int(matrix.min()), int(matrix.max())
732 733 proj.spacing = self.Slice.spacing
  734 + if self.Slice.affine is not None:
  735 + proj.affine = self.Slice.affine.tolist()
733 736  
734 737 ######
735 738 session = ses.Session()
... ... @@ -915,16 +918,7 @@ class Controller():
915 918 matrix, matrix_filename = self.OpenOtherFiles(group)
916 919 self.CreateOtherProject(name, matrix, matrix_filename)
917 920 self.LoadProject()
918   - if group.affine.any():
919   - # TODO: replace the inverse of the affine by the actual affine in the whole code
920   - # remove scaling factor for non-unitary voxel dimensions
921   - # self.affine = image_utils.world2invspace(affine=group.affine)
922   - scale, shear, angs, trans, persp = tr.decompose_matrix(group.affine)
923   - self.affine = np.linalg.inv(tr.compose_matrix(scale=None, shear=shear,
924   - angles=angs, translate=trans, perspective=persp))
925   - # print("repos_img: {}".format(repos_img))
926   - self.Slice.affine = self.affine
927   - Publisher.sendMessage('Update affine matrix', affine=self.affine)
  921 +
928 922 Publisher.sendMessage("Enable state project", state=True)
929 923 else:
930 924 dialog.ImportInvalidFiles(ftype="Others")
... ... @@ -1034,13 +1028,7 @@ class Controller():
1034 1028 self.matrix, scalar_range, self.filename = image_utils.img2memmap(group)
1035 1029  
1036 1030 hdr = group.header
1037   - # if group.affine.any():
1038   - # self.affine = group.affine
1039   - # Publisher.sendMessage('Update affine matrix',
1040   - # affine=self.affine, status=True)
1041 1031 hdr.set_data_dtype('int16')
1042   - dims = hdr.get_zooms()
1043   - dimsf = tuple([float(s) for s in dims])
1044 1032  
1045 1033 wl = float((scalar_range[0] + scalar_range[1]) * 0.5)
1046 1034 ww = float((scalar_range[1] - scalar_range[0]))
... ... @@ -1048,19 +1036,29 @@ class Controller():
1048 1036 self.Slice = sl.Slice()
1049 1037 self.Slice.matrix = self.matrix
1050 1038 self.Slice.matrix_filename = self.filename
1051   -
1052   - self.Slice.spacing = dimsf
  1039 + # even though the axes 0 and 2 are swapped when creating self.matrix
  1040 + # the spacing should be kept the original, as it is modified somewhere later
  1041 + # otherwise generate wrong results
  1042 + # also need to convert to float because original get_zooms return numpy.float32
  1043 + # which is unsupported by the plist for saving the project
  1044 + self.Slice.spacing = tuple([float(s) for s in hdr.get_zooms()])
1053 1045 self.Slice.window_level = wl
1054 1046 self.Slice.window_width = ww
1055 1047  
  1048 + if group.affine.any():
  1049 + # remove scaling factor for non-unitary voxel dimensions
  1050 + scale, shear, angs, trans, persp = tr.decompose_matrix(group.affine)
  1051 + self.Slice.affine = np.linalg.inv(tr.compose_matrix(scale=None, shear=shear,
  1052 + angles=angs, translate=trans, perspective=persp))
  1053 + else:
  1054 + self.Slice.affine = None
  1055 +
1056 1056 scalar_range = int(scalar_range[0]), int(scalar_range[1])
  1057 +
1057 1058 Publisher.sendMessage('Update threshold limits list',
1058 1059 threshold_range=scalar_range)
1059   - return self.matrix, self.filename
1060 1060  
1061   - def Send_affine(self):
1062   - if self.affine is not None:
1063   - Publisher.sendMessage('Update affine matrix', affine=self.affine)
  1061 + return self.matrix, self.filename
1064 1062  
1065 1063 def LoadImagedataInfo(self):
1066 1064 proj = prj.Project()
... ...
invesalius/data/imagedata_utils.py
... ... @@ -549,6 +549,17 @@ def get_LUT_value_255(data, window, level):
549 549 return data
550 550  
551 551  
  552 +def get_LUT_value(data, window, level):
  553 + shape = data.shape
  554 + data_ = data.ravel()
  555 + data = np.piecewise(data_,
  556 + [data_ <= (level - 0.5 - (window-1)/2),
  557 + data_ > (level - 0.5 + (window-1)/2)],
  558 + [0, window, lambda data_: ((data_ - (level - 0.5))/(window-1) + 0.5)*(window)])
  559 + data.shape = shape
  560 + return data
  561 +
  562 +
552 563 def image_normalize(image, min_=0.0, max_=1.0, output_dtype=np.int16):
553 564 output = np.empty(shape=image.shape, dtype=output_dtype)
554 565 imin, imax = image.min(), image.max()
... ... @@ -595,7 +606,7 @@ def convert_invesalius_to_voxel(position):
595 606 :return: a vector of 3 coordinates in the voxel space
596 607 """
597 608 slice = sl.Slice()
598   - return np.array((position[0], slice.matrix.shape[1] - position[1] - 1, position[2]))
  609 + return np.array((position[0], slice.spacing[1]*(slice.matrix.shape[1] - 1) - position[1], position[2]))
599 610  
600 611  
601 612 def convert_invesalius_to_world(position, orientation):
... ...
invesalius/data/styles.py
... ... @@ -45,6 +45,7 @@ import invesalius.utils as utils
45 45 from invesalius.data.measures import (CircleDensityMeasure, MeasureData,
46 46 PolygonDensityMeasure)
47 47  
  48 +from invesalius.data.imagedata_utils import get_LUT_value, get_LUT_value_255
48 49 from invesalius_cy import floodfill
49 50  
50 51 # For tracts
... ... @@ -69,26 +70,6 @@ WATERSHED_OPERATIONS = {_(&quot;Erase&quot;): BRUSH_ERASE,
69 70 _("Foreground"): BRUSH_FOREGROUND,
70 71 _("Background"): BRUSH_BACKGROUND,}
71 72  
72   -def get_LUT_value(data, window, level):
73   - shape = data.shape
74   - data_ = data.ravel()
75   - data = np.piecewise(data_,
76   - [data_ <= (level - 0.5 - (window-1)/2),
77   - data_ > (level - 0.5 + (window-1)/2)],
78   - [0, window, lambda data_: ((data_ - (level - 0.5))/(window-1) + 0.5)*(window)])
79   - data.shape = shape
80   - return data
81   -
82   -def get_LUT_value_255(data, window, level):
83   - shape = data.shape
84   - data_ = data.ravel()
85   - data = np.piecewise(data_,
86   - [data_ <= (level - 0.5 - (window-1)/2),
87   - data_ > (level - 0.5 + (window-1)/2)],
88   - [0, 255, lambda data_: ((data_ - (level - 0.5))/(window-1) + 0.5)*(255)])
89   - data.shape = shape
90   - return data
91   -
92 73  
93 74 class BaseImageInteractorStyle(vtk.vtkInteractorStyleImage):
94 75 def __init__(self, viewer):
... ...
invesalius/data/surface.py
... ... @@ -51,6 +51,7 @@ else:
51 51  
52 52 import invesalius.constants as const
53 53 import invesalius.data.imagedata_utils as iu
  54 +import invesalius.data.slice_ as sl
54 55 import invesalius.data.polydata_utils as pu
55 56 import invesalius.project as prj
56 57 import invesalius.session as ses
... ... @@ -159,7 +160,6 @@ class SurfaceManager():
159 160 def __init__(self):
160 161 self.actors_dict = {}
161 162 self.last_surface_index = 0
162   - self.affine_vtk = None
163 163 self.convert2inv = None
164 164 self.__bind_events()
165 165  
... ... @@ -207,7 +207,6 @@ class SurfaceManager():
207 207  
208 208 Publisher.subscribe(self.OnImportSurfaceFile, 'Import surface file')
209 209  
210   - Publisher.subscribe(self.UpdateAffineMatrix, 'Update affine matrix')
211 210 Publisher.subscribe(self.UpdateConvert2InvFlag, 'Update convert2inv flag')
212 211  
213 212 Publisher.subscribe(self.CreateSurfaceFromPolydata, 'Create surface from polydata')
... ... @@ -338,16 +337,6 @@ class SurfaceManager():
338 337 name = os.path.splitext(os.path.split(filename)[-1])[0]
339 338 self.CreateSurfaceFromPolydata(polydata, name=name, scalar=scalar)
340 339  
341   - def UpdateAffineMatrix(self, affine):
342   - if affine is not None:
343   - prj_data = prj.Project()
344   - matrix_shape = tuple(prj_data.matrix_shape)
345   - affine = affine.copy()
346   - affine[1, -1] -= matrix_shape[1]
347   - self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine)
348   - else:
349   - self.affine_vtk = None
350   -
351 340 def UpdateConvert2InvFlag(self, convert2inv=False):
352 341 self.convert2inv = convert2inv
353 342  
... ... @@ -373,12 +362,17 @@ class SurfaceManager():
373 362 actor.SetMapper(mapper)
374 363 actor.GetProperty().SetBackfaceCulling(1)
375 364  
376   - if self.convert2inv and (self.affine_vtk is not None):
377   - actor.SetUserMatrix(self.affine_vtk)
  365 + if self.convert2inv:
  366 + # convert between invesalius and world space with shift in the Y coordinate
  367 + affine = sl.Slice().affine
  368 + if affine is not None:
  369 + affine[1, -1] -= sl.Slice().spacing[1] * (sl.Slice().matrix.shape[1] - 1)
  370 + affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine)
  371 + actor.SetUserMatrix(affine_vtk)
378 372  
379 373 if overwrite:
380 374 if index is None:
381   - index = self.last_surface_index
  375 + index = self.last_surface_index
382 376 surface = Surface(index=index)
383 377 else:
384 378 surface = Surface()
... ...
invesalius/data/viewer_volume.py
... ... @@ -1455,7 +1455,8 @@ class Viewer(wx.Panel):
1455 1455 self.ren.RemoveActor(self.object_orientation_torus_actor)
1456 1456 self.ren.RemoveActor(self.obj_projection_arrow_actor)
1457 1457 self.actor_peel = None
1458   - self.ball_actor.SetVisibility(1)
  1458 + if self.ball_actor:
  1459 + self.ball_actor.SetVisibility(1)
1459 1460  
1460 1461 if flag and actor:
1461 1462 self.ren.AddActor(actor)
... ...
invesalius/gui/dialogs.py
... ... @@ -4208,37 +4208,23 @@ class GoToDialogScannerCoord(wx.Dialog):
4208 4208 self.__bind_events()
4209 4209  
4210 4210 btn_ok.Bind(wx.EVT_BUTTON, self.OnOk)
4211   - Publisher.sendMessage('Get affine matrix')
4212 4211  
4213 4212 def __bind_events(self):
4214 4213 Publisher.subscribe(self.SetNewFocalPoint, 'Cross focal point')
4215   - Publisher.subscribe(self.UpdateAffineMatrix, 'Update affine matrix')
4216   -
4217   - def UpdateAffineMatrix(self, affine):
4218   - self.affine = affine
4219 4214  
4220 4215 def SetNewFocalPoint(self, coord, spacing):
4221 4216 Publisher.sendMessage('Update cross pos', coord=self.result*spacing)
4222 4217  
4223 4218 def OnOk(self, evt):
4224   - from numpy.linalg import inv
4225 4219 import invesalius.data.slice_ as slc
4226 4220 try:
4227   - #get affine from image import
4228   - if self.affine is not None:
4229   - affine = self.affine
4230   - #get affine from project
4231   - else:
4232   - from invesalius.project import Project
4233   - affine = Project().affine
4234   -
4235 4221 point = [float(self.goto_sagital.GetValue()),
4236 4222 float(self.goto_coronal.GetValue()),
4237 4223 float(self.goto_axial.GetValue())]
4238 4224  
4239 4225 # transformation from scanner coordinates to inv coord system
4240   - affine = inv(affine)
4241   - self.result = np.dot(affine[:3, :3], np.transpose(point[0:3])) + affine[:3, 3]
  4226 + affine_inverse = np.linalg.inv(slc.Slice().affine)
  4227 + self.result = np.dot(affine_inverse[:3, :3], np.transpose(point[0:3])) + affine_inverse[:3, 3]
4242 4228 self.result[1] = slc.Slice().GetMaxSliceNumber(const.CORONAL_STR) - self.result[1]
4243 4229  
4244 4230 Publisher.sendMessage('Update status text in GUI', label=_("Calculating the transformation ..."))
... ...
invesalius/gui/frame.py
... ... @@ -892,7 +892,7 @@ class MenuBar(wx.MenuBar):
892 892 sub(self.OnEnableState, "Enable state project")
893 893 sub(self.OnEnableUndo, "Enable undo")
894 894 sub(self.OnEnableRedo, "Enable redo")
895   - sub(self.OnEnableGotoCoord, "Update affine matrix")
  895 + sub(self.OnEnableGotoCoord, "Enable Go-to-Coord")
896 896 sub(self.OnEnableNavigation, "Navigation status")
897 897  
898 898 sub(self.OnAddMask, "Add mask")
... ... @@ -1242,15 +1242,13 @@ class MenuBar(wx.MenuBar):
1242 1242 else:
1243 1243 self.FindItemById(wx.ID_REDO).Enable(False)
1244 1244  
1245   - def OnEnableGotoCoord(self, affine):
  1245 + def OnEnableGotoCoord(self, status=True):
1246 1246 """
1247   - Disable goto coord either if there is no affine matrix or affine is wrongly imported.
1248   - :param status: Affine matrix status
  1247 + Enable or disable goto coord depending on the imported affine matrix.
  1248 + :param status: True for enabling and False for disabling the Go-To-Coord
1249 1249 """
1250   - if affine is not None:
1251   - self.FindItemById(const.ID_GOTO_COORD).Enable(True)
1252   - else:
1253   - self.FindItemById(const.ID_GOTO_COORD).Enable(False)
  1250 +
  1251 + self.FindItemById(const.ID_GOTO_COORD).Enable(status)
1254 1252  
1255 1253 def OnEnableNavigation(self, nav_status, vis_status):
1256 1254 """
... ...
invesalius/gui/task_navigator.py
... ... @@ -691,8 +691,8 @@ class NeuronavigationPanel(wx.Panel):
691 691 size = 2
692 692 seed = 3 * [0.]
693 693  
694   - Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size, label=label, seed=seed)
695   -
  694 + Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size,
  695 + label=label, seed=seed)
696 696 else:
697 697 for m in [0, 1, 2]:
698 698 self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m]))
... ... @@ -1189,11 +1189,11 @@ class MarkersPanel(wx.Panel):
1189 1189 res += '\t'.join(map(lambda x: 'N/A' if x is None else str(x), (*position_world, *orientation_world)))
1190 1190 return res
1191 1191  
1192   - def from_string(self, str):
1193   - """Deserialize from a tab-separated string. If the string is not
  1192 + def from_string(self, inp_str):
  1193 + """Deserialize from a tab-separated string. If the string is not
1194 1194 properly formatted, might throw an exception and leave the object
1195 1195 in an inconsistent state."""
1196   - for field, str_val in zip(dataclasses.fields(self.__class__), str.split('\t')):
  1196 + for field, str_val in zip(dataclasses.fields(self.__class__), inp_str.split('\t')):
1197 1197 if field.type is float:
1198 1198 setattr(self, field.name, float(str_val))
1199 1199 if field.type is int:
... ... @@ -1294,7 +1294,7 @@ class MarkersPanel(wx.Panel):
1294 1294 self.lc.InsertColumn(1, 'X')
1295 1295 self.lc.InsertColumn(2, 'Y')
1296 1296 self.lc.InsertColumn(3, 'Z')
1297   - self.lc.InsertColumn(4, 'ID')
  1297 + self.lc.InsertColumn(4, 'Label')
1298 1298 self.lc.InsertColumn(5, 'Target')
1299 1299 self.lc.InsertColumn(6, 'Session')
1300 1300  
... ... @@ -2125,7 +2125,7 @@ class SessionPanel(wx.Panel):
2125 2125 except AttributeError:
2126 2126 default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR)
2127 2127 self.SetBackgroundColour(default_colour)
2128   -
  2128 +
2129 2129 # session count spinner
2130 2130 self.__spin_session = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
2131 2131 self.__spin_session.SetRange(1, 99)
... ... @@ -2133,13 +2133,13 @@ class SessionPanel(wx.Panel):
2133 2133  
2134 2134 self.__spin_session.Bind(wx.EVT_TEXT, self.OnSessionChanged)
2135 2135 self.__spin_session.Bind(wx.EVT_SPINCTRL, self.OnSessionChanged)
2136   -
  2136 +
2137 2137 sizer_create = wx.FlexGridSizer(rows=1, cols=1, hgap=5, vgap=5)
2138 2138 sizer_create.AddMany([(self.__spin_session, 1)])
2139 2139  
2140 2140 def OnSessionChanged(self, evt):
2141 2141 Publisher.sendMessage('Current session changed', new_session_id=self.__spin_session.GetValue())
2142   -
  2142 +
2143 2143  
2144 2144 class InputAttributes(object):
2145 2145 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
... ...
invesalius/project.py
... ... @@ -329,8 +329,6 @@ class Project(metaclass=Singleton):
329 329  
330 330 if project.get("affine", ""):
331 331 self.affine = project["affine"]
332   - Publisher.sendMessage('Update affine matrix',
333   - affine=np.asarray(self.affine).reshape(4, 4))
334 332  
335 333 # Opening the masks
336 334 self.mask_dict = TwoWaysDictionary()
... ...
invesalius/segmentation/brain/segment.py
... ... @@ -22,17 +22,6 @@ SIZE = 48
22 22 OVERLAP = SIZE // 2 + 1
23 23  
24 24  
25   -def get_LUT_value(data, window, level):
26   - shape = data.shape
27   - data_ = data.ravel()
28   - data = np.piecewise(data_,
29   - [data_ <= (level - 0.5 - (window-1)/2),
30   - data_ > (level - 0.5 + (window-1)/2)],
31   - [0, window, lambda data_: ((data_ - (level - 0.5))/(window-1) + 0.5)*(window)])
32   - data.shape = shape
33   - return data
34   -
35   -
36 25 def gen_patches(image, patch_size, overlap):
37 26 sz, sy, sx = image.shape
38 27 i_cuts = list(
... ... @@ -188,8 +177,7 @@ class SegmentProcess(ctx.Process):
188 177 )
189 178  
190 179 if self.apply_wwwl:
191   - print("Applying window level")
192   - image = get_LUT_value(image, self.window_width, self.window_level)
  180 + image = imagedata_utils.get_LUT_value(image, self.window_width, self.window_level)
193 181  
194 182 probability_array = np.memmap(
195 183 self._prob_array_filename,
... ...