Commit 99e6d36b1697f260027dee9cfac67603dffdfdce

Authored by Thiago Franco de Moraes
Committed by GitHub
1 parent 3349921d
Exists in master

Mask 3d preview (#252)

* Added methods to preview mask

* Added a button in viewer volume to activate mask 3d view

* Some cleaning and style modifications

* Showing mask preview and updating after edition

* Created a method to mark mask as modified

* Added update 3D preview in region growing tools

* Added update 3D preview in brain segmentation

* Added update 3D preview in swap and flip mask

* Added update 3D preview in threshold

* Improved rendering of mask

* Better showing mask in volume viewer

* Flip

* Removed the new button in volume viewer

* Increased the size of 3d preview in datanotebook

* Fixed problem with VTK7

* Fixed problem with VTK7

* Avoiding deepcopy

* Using IsoSurface when in VTK > 8 and with GPU support

* Created the class VolumeMask to keep all stuf about mask volume raycasting

* Changing volume mask colour when changing mask colour

* Changing mask axes also change volume mask rendering

* When remove mask removing volume mask visualization

* Changing mask threshold was not updating mask preview

* Added callbacks for when modifying mask

* Added method to remove modified callbacks

* Created menu to mask 3d preview

* Enable mask preview is working

* Rendering volume viewer after activating mask preview

* Loading and removing mask preview via menu

* Autoreload working

* Update working

* Removed mask preview from data notebook and task slice

* Auto reload mask preview shortcut
invesalius/constants.py
@@ -516,6 +516,9 @@ ID_DENSITY_MEASURE = wx.NewId() @@ -516,6 +516,9 @@ ID_DENSITY_MEASURE = wx.NewId()
516 ID_MASK_DENSITY_MEASURE = wx.NewId() 516 ID_MASK_DENSITY_MEASURE = wx.NewId()
517 ID_CREATE_SURFACE = wx.NewId() 517 ID_CREATE_SURFACE = wx.NewId()
518 ID_CREATE_MASK = wx.NewId() 518 ID_CREATE_MASK = wx.NewId()
  519 +ID_MASK_3D_PREVIEW = wx.NewId()
  520 +ID_MASK_3D_RELOAD = wx.NewId()
  521 +ID_MASK_3D_AUTO_RELOAD = wx.NewId()
519 522
520 ID_GOTO_SLICE = wx.NewId() 523 ID_GOTO_SLICE = wx.NewId()
521 ID_GOTO_COORD = wx.NewId() 524 ID_GOTO_COORD = wx.NewId()
invesalius/control.py
@@ -126,6 +126,12 @@ class Controller(): @@ -126,6 +126,12 @@ class Controller():
126 126
127 Publisher.subscribe(self.create_project_from_matrix, 'Create project from matrix') 127 Publisher.subscribe(self.create_project_from_matrix, 'Create project from matrix')
128 128
  129 + Publisher.subscribe(self.show_mask_preview, 'Show mask preview')
  130 +
  131 + Publisher.subscribe(self.enable_mask_preview, 'Enable mask 3D preview')
  132 + Publisher.subscribe(self.disable_mask_preview, 'Disable mask 3D preview')
  133 + Publisher.subscribe(self.update_mask_preview, 'Update mask 3D preview')
  134 +
129 def SetBitmapSpacing(self, spacing): 135 def SetBitmapSpacing(self, spacing):
130 proj = prj.Project() 136 proj = prj.Project()
131 proj.spacing = spacing 137 proj.spacing = spacing
@@ -1108,3 +1114,30 @@ class Controller(): @@ -1108,3 +1114,30 @@ class Controller():
1108 1114
1109 if err_msg: 1115 if err_msg:
1110 dialog.MessageBox(None, "It was not possible to launch new instance of InVesalius3 dsfa dfdsfa sdfas fdsaf asdfasf dsaa", err_msg) 1116 dialog.MessageBox(None, "It was not possible to launch new instance of InVesalius3 dsfa dfdsfa sdfas fdsaf asdfasf dsaa", err_msg)
  1117 +
  1118 + def show_mask_preview(self, index, flag=True):
  1119 + proj = prj.Project()
  1120 + mask = proj.mask_dict[index]
  1121 + slc = self.Slice.do_threshold_to_all_slices(mask)
  1122 + mask.create_3d_preview()
  1123 + Publisher.sendMessage("Load mask preview", mask_3d_actor=mask.volume._actor, flag=flag)
  1124 + Publisher.sendMessage("Reload actual slice")
  1125 +
  1126 + def enable_mask_preview(self):
  1127 + mask = self.Slice.current_mask
  1128 + if mask is not None:
  1129 + self.Slice.do_threshold_to_all_slices(mask)
  1130 + mask.create_3d_preview()
  1131 + Publisher.sendMessage("Load mask preview", mask_3d_actor=mask.volume._actor, flag=True)
  1132 + Publisher.sendMessage("Render volume viewer")
  1133 +
  1134 + def disable_mask_preview(self):
  1135 + mask = self.Slice.current_mask
  1136 + if mask is not None:
  1137 + Publisher.sendMessage("Remove mask preview", mask_3d_actor=mask.volume._actor)
  1138 + Publisher.sendMessage("Render volume viewer")
  1139 +
  1140 + def update_mask_preview(self):
  1141 + mask = self.Slice.current_mask
  1142 + if mask is not None:
  1143 + mask._update_imagedata()
invesalius/data/converters.py
@@ -24,8 +24,7 @@ import vtk @@ -24,8 +24,7 @@ import vtk
24 from vtk.util import numpy_support 24 from vtk.util import numpy_support
25 25
26 26
27 -def to_vtk(n_array, spacing, slice_number, orientation, origin=(0, 0, 0), padding=(0, 0, 0)):  
28 - 27 +def to_vtk(n_array, spacing=(1.0, 1.0, 1.0), slice_number=0, orientation='AXIAL', origin=(0, 0, 0), padding=(0, 0, 0)):
29 if orientation == "SAGITTAL": 28 if orientation == "SAGITTAL":
30 orientation = "SAGITAL" 29 orientation = "SAGITAL"
31 30
@@ -67,6 +66,37 @@ def to_vtk(n_array, spacing, slice_number, orientation, origin=(0, 0, 0), paddin @@ -67,6 +66,37 @@ def to_vtk(n_array, spacing, slice_number, orientation, origin=(0, 0, 0), paddin
67 return image_copy 66 return image_copy
68 67
69 68
  69 +def to_vtk_mask(n_array, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0)):
  70 + dz, dy, dx = n_array.shape
  71 + ox, oy, oz = origin
  72 + sx, sy, sz = spacing
  73 +
  74 + ox -= sx
  75 + oy -= sy
  76 + oz -= sz
  77 +
  78 + v_image = numpy_support.numpy_to_vtk(n_array.flat)
  79 + extent = (0, dx - 1, 0, dy - 1, 0, dz - 1)
  80 +
  81 + # Generating the vtkImageData
  82 + image = vtk.vtkImageData()
  83 + image.SetOrigin(ox, oy, oz)
  84 + image.SetSpacing(sx, sy, sz)
  85 + image.SetDimensions(dx - 1, dy - 1, dz - 1)
  86 + # SetNumberOfScalarComponents and SetScalrType were replaced by
  87 + # AllocateScalars
  88 + # image.SetNumberOfScalarComponents(1)
  89 + # image.SetScalarType(numpy_support.get_vtk_array_type(n_array.dtype))
  90 + image.AllocateScalars(numpy_support.get_vtk_array_type(n_array.dtype), 1)
  91 + image.SetExtent(extent)
  92 + image.GetPointData().SetScalars(v_image)
  93 +
  94 + # image_copy = vtk.vtkImageData()
  95 + # image_copy.DeepCopy(image)
  96 +
  97 + return image
  98 +
  99 +
70 def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)): 100 def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)):
71 dy, dx, dc = n_array.shape 101 dy, dx, dc = n_array.shape
72 v_image = numpy_support.numpy_to_vtk(n_array.reshape(dy*dx, dc)) 102 v_image = numpy_support.numpy_to_vtk(n_array.reshape(dy*dx, dc))
invesalius/data/mask.py
@@ -22,18 +22,21 @@ import plistlib @@ -22,18 +22,21 @@ import plistlib
22 import random 22 import random
23 import shutil 23 import shutil
24 import tempfile 24 import tempfile
25 -  
26 -import numpy as np  
27 -import vtk 25 +import time
  26 +import weakref
28 27
29 import invesalius.constants as const 28 import invesalius.constants as const
  29 +import invesalius.data.converters as converters
30 import invesalius.data.imagedata_utils as iu 30 import invesalius.data.imagedata_utils as iu
31 import invesalius.session as ses 31 import invesalius.session as ses
32 - 32 +from invesalius.data.volume import VolumeMask
  33 +import numpy as np
  34 +import vtk
33 from invesalius_cy import floodfill 35 from invesalius_cy import floodfill
34 -  
35 from pubsub import pub as Publisher 36 from pubsub import pub as Publisher
36 from scipy import ndimage 37 from scipy import ndimage
  38 +from vtk.util import numpy_support
  39 +
37 40
38 class EditionHistoryNode(object): 41 class EditionHistoryNode(object):
39 def __init__(self, index, orientation, array, clean=False): 42 def __init__(self, index, orientation, array, clean=False):
@@ -189,7 +192,9 @@ class Mask(): @@ -189,7 +192,9 @@ class Mask():
189 def __init__(self): 192 def __init__(self):
190 Mask.general_index += 1 193 Mask.general_index += 1
191 self.index = Mask.general_index 194 self.index = Mask.general_index
192 - self.imagedata = '' 195 + self.matrix = None
  196 + self.spacing = (1.0, 1.0, 1.0)
  197 + self.imagedata = None
193 self.colour = random.choice(const.MASK_COLOUR) 198 self.colour = random.choice(const.MASK_COLOUR)
194 self.opacity = const.MASK_OPACITY 199 self.opacity = const.MASK_OPACITY
195 self.threshold_range = const.THRESHOLD_RANGE 200 self.threshold_range = const.THRESHOLD_RANGE
@@ -198,7 +203,11 @@ class Mask(): @@ -198,7 +203,11 @@ class Mask():
198 self.is_shown = 1 203 self.is_shown = 1
199 self.edited_points = {} 204 self.edited_points = {}
200 self.was_edited = False 205 self.was_edited = False
  206 + self.volume = None
  207 + self.auto_update_mask = True
  208 + self.modified_time = 0
201 self.__bind_events() 209 self.__bind_events()
  210 + self._modified_callbacks = []
202 211
203 self.history = EditionHistory() 212 self.history = EditionHistory()
204 213
@@ -206,11 +215,24 @@ class Mask(): @@ -206,11 +215,24 @@ class Mask():
206 Publisher.subscribe(self.OnFlipVolume, 'Flip volume') 215 Publisher.subscribe(self.OnFlipVolume, 'Flip volume')
207 Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') 216 Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes')
208 217
  218 + def as_vtkimagedata(self):
  219 + print("Converting to VTK")
  220 + vimg = converters.to_vtk_mask(self.matrix, self.spacing)
  221 + print("Converted")
  222 + return vimg
  223 +
  224 + def set_colour(self, colour):
  225 + self.colour = colour
  226 + if self.volume is not None:
  227 + self.volume.set_colour(colour)
  228 + Publisher.sendMessage("Render volume viewer")
  229 +
209 def save_history(self, index, orientation, array, p_array, clean=False): 230 def save_history(self, index, orientation, array, p_array, clean=False):
210 self.history.new_node(index, orientation, array, p_array, clean) 231 self.history.new_node(index, orientation, array, p_array, clean)
211 232
212 def undo_history(self, actual_slices): 233 def undo_history(self, actual_slices):
213 self.history.undo(self.matrix, actual_slices) 234 self.history.undo(self.matrix, actual_slices)
  235 + self.modified()
214 236
215 # Marking the project as changed 237 # Marking the project as changed
216 session = ses.Session() 238 session = ses.Session()
@@ -218,6 +240,7 @@ class Mask(): @@ -218,6 +240,7 @@ class Mask():
218 240
219 def redo_history(self, actual_slices): 241 def redo_history(self, actual_slices):
220 self.history.redo(self.matrix, actual_slices) 242 self.history.redo(self.matrix, actual_slices)
  243 + self.modified()
221 244
222 # Marking the project as changed 245 # Marking the project as changed
223 session = ses.Session() 246 session = ses.Session()
@@ -226,6 +249,27 @@ class Mask(): @@ -226,6 +249,27 @@ class Mask():
226 def on_show(self): 249 def on_show(self):
227 self.history._config_undo_redo(self.is_shown) 250 self.history._config_undo_redo(self.is_shown)
228 251
  252 + def create_3d_preview(self):
  253 + if self.volume is None:
  254 + if self.imagedata is None:
  255 + self.imagedata = self.as_vtkimagedata()
  256 + self.volume = VolumeMask(self)
  257 + self.volume.create_volume()
  258 +
  259 + def _update_imagedata(self, update_volume_viewer=True):
  260 + if self.imagedata is not None:
  261 + dz, dy, dx = self.matrix.shape
  262 + # np_image = numpy_support.vtk_to_numpy(self.imagedata.GetPointData().GetScalars())
  263 + # np_image[:] = self.matrix.reshape(-1)
  264 + self.imagedata.SetDimensions(dx - 1, dy - 1, dz - 1)
  265 + self.imagedata.SetSpacing(self.spacing)
  266 + self.imagedata.SetExtent(0, dx - 1, 0, dy - 1, 0, dz - 1)
  267 + self.imagedata.Modified()
  268 + self.volume._actor.Update()
  269 +
  270 + if update_volume_viewer:
  271 + Publisher.sendMessage("Render volume viewer")
  272 +
229 def SavePlist(self, dir_temp, filelist): 273 def SavePlist(self, dir_temp, filelist):
230 mask = {} 274 mask = {}
231 filename = u'mask_%d' % self.index 275 filename = u'mask_%d' % self.index
@@ -284,10 +328,15 @@ class Mask(): @@ -284,10 +328,15 @@ class Mask():
284 elif axis == 2: 328 elif axis == 2:
285 submatrix[:] = submatrix[:, :, ::-1] 329 submatrix[:] = submatrix[:, :, ::-1]
286 self.matrix[0, 0, 1::] = self.matrix[0, 0, :0:-1] 330 self.matrix[0, 0, 1::] = self.matrix[0, 0, :0:-1]
  331 + self.modified()
287 332
288 def OnSwapVolumeAxes(self, axes): 333 def OnSwapVolumeAxes(self, axes):
289 axis0, axis1 = axes 334 axis0, axis1 = axes
290 self.matrix = self.matrix.swapaxes(axis0, axis1) 335 self.matrix = self.matrix.swapaxes(axis0, axis1)
  336 + if self.volume:
  337 + self.imagedata = self.as_vtkimagedata()
  338 + self.volume.change_imagedata()
  339 + self.modified()
291 340
292 def _save_mask(self, filename): 341 def _save_mask(self, filename):
293 shutil.copyfile(self.temp_file, filename) 342 shutil.copyfile(self.temp_file, filename)
@@ -300,6 +349,22 @@ class Mask(): @@ -300,6 +349,22 @@ class Mask():
300 def _set_class_index(self, index): 349 def _set_class_index(self, index):
301 Mask.general_index = index 350 Mask.general_index = index
302 351
  352 + def add_modified_callback(self, callback):
  353 + ref = weakref.WeakMethod(callback)
  354 + self._modified_callbacks.append(ref)
  355 +
  356 + def remove_modified_callback(self, callback):
  357 + callbacks = []
  358 + removed = False
  359 + for cb in self._modified_callbacks:
  360 + if cb() is not None:
  361 + if cb() != callback:
  362 + callbacks.append(cb)
  363 + else:
  364 + removed = True
  365 + self._modified_callbacks = callbacks
  366 + return removed
  367 +
303 def create_mask(self, shape): 368 def create_mask(self, shape):
304 """ 369 """
305 Creates a new mask object. This method do not append this new mask into the project. 370 Creates a new mask object. This method do not append this new mask into the project.
@@ -311,11 +376,35 @@ class Mask(): @@ -311,11 +376,35 @@ class Mask():
311 shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 376 shape = shape[0] + 1, shape[1] + 1, shape[2] + 1
312 self.matrix = np.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) 377 self.matrix = np.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape)
313 378
  379 + def modified(self, all_volume=False):
  380 + if all_volume:
  381 + self.matrix[0] = 1
  382 + self.matrix[:, 0, :] = 1
  383 + self.matrix[:, :, 0] = 1
  384 + if ses.Session().auto_reload_preview:
  385 + self._update_imagedata()
  386 + self.modified_time = time.monotonic()
  387 + callbacks = []
  388 + print(self._modified_callbacks)
  389 + for callback in self._modified_callbacks:
  390 + if callback() is not None:
  391 + callback()()
  392 + callbacks.append(callback)
  393 + self._modified_callbacks = callbacks
  394 +
314 def clean(self): 395 def clean(self):
315 self.matrix[1:, 1:, 1:] = 0 396 self.matrix[1:, 1:, 1:] = 0
316 - self.matrix[0, :, :] = 1  
317 - self.matrix[:, 0, :] = 1  
318 - self.matrix[:, :, 0] = 1 397 + self.modified(all_volume=True)
  398 +
  399 + def cleanup(self):
  400 + if self.is_shown:
  401 + self.history._config_undo_redo(False)
  402 + if self.volume:
  403 + Publisher.sendMessage("Unload volume", volume=self.volume._actor)
  404 + Publisher.sendMessage("Render volume viewer")
  405 + self.imagedata = None
  406 + self.volume = None
  407 + del self.matrix
319 408
320 def copy(self, copy_name): 409 def copy(self, copy_name):
321 """ 410 """
@@ -334,6 +423,7 @@ class Mask(): @@ -334,6 +423,7 @@ class Mask():
334 423
335 new_mask.create_mask(shape=[i-1 for i in self.matrix.shape]) 424 new_mask.create_mask(shape=[i-1 for i in self.matrix.shape])
336 new_mask.matrix[:] = self.matrix[:] 425 new_mask.matrix[:] = self.matrix[:]
  426 + new_mask.spacing = self.spacing
337 427
338 return new_mask 428 return new_mask
339 429
@@ -384,7 +474,4 @@ class Mask(): @@ -384,7 +474,4 @@ class Mask():
384 self.save_history(index, orientation, matrix.copy(), cp_mask) 474 self.save_history(index, orientation, matrix.copy(), cp_mask)
385 475
386 def __del__(self): 476 def __del__(self):
387 - if self.is_shown:  
388 - self.history._config_undo_redo(False)  
389 - del self.matrix  
390 os.remove(self.temp_file) 477 os.remove(self.temp_file)
invesalius/data/slice_.py
@@ -384,9 +384,21 @@ class Slice(metaclass=utils.Singleton): @@ -384,9 +384,21 @@ class Slice(metaclass=utils.Singleton):
384 self.current_mask.matrix[:] = 0 384 self.current_mask.matrix[:] = 0
385 self.current_mask.clear_history() 385 self.current_mask.clear_history()
386 386
  387 + if self.current_mask.auto_update_mask and self.current_mask.volume is not None:
  388 + to_reload = True
  389 + self.SetMaskThreshold(
  390 + index,
  391 + threshold_range,
  392 + slice_number = None,
  393 + orientation = None
  394 + )
  395 + self.discard_all_buffers()
  396 + Publisher.sendMessage("Reload actual slice")
  397 + self.current_mask.modified(all_volume=True)
  398 + return
  399 +
387 to_reload = False 400 to_reload = False
388 if threshold_range != self.current_mask.threshold_range: 401 if threshold_range != self.current_mask.threshold_range:
389 - to_reload = True  
390 for orientation in self.buffer_slices: 402 for orientation in self.buffer_slices:
391 self.buffer_slices[orientation].discard_vtk_mask() 403 self.buffer_slices[orientation].discard_vtk_mask()
392 self.SetMaskThreshold( 404 self.SetMaskThreshold(
@@ -417,6 +429,7 @@ class Slice(metaclass=utils.Singleton): @@ -417,6 +429,7 @@ class Slice(metaclass=utils.Singleton):
417 429
418 if to_reload: 430 if to_reload:
419 Publisher.sendMessage("Reload actual slice") 431 Publisher.sendMessage("Reload actual slice")
  432 + self.current_mask.modified(all_volume=False)
420 433
421 def __set_current_mask_threshold_actual_slice(self, threshold_range): 434 def __set_current_mask_threshold_actual_slice(self, threshold_range):
422 if self.current_mask is None: 435 if self.current_mask is None:
@@ -1061,7 +1074,7 @@ class Slice(metaclass=utils.Singleton): @@ -1061,7 +1074,7 @@ class Slice(metaclass=utils.Singleton):
1061 def SetMaskColour(self, index, colour, update=True): 1074 def SetMaskColour(self, index, colour, update=True):
1062 "Set a mask colour given its index and colour (RGB 0-1 values)" 1075 "Set a mask colour given its index and colour (RGB 0-1 values)"
1063 proj = Project() 1076 proj = Project()
1064 - proj.mask_dict[index].colour = colour 1077 + proj.mask_dict[index].set_colour(colour)
1065 1078
1066 (r, g, b) = colour[:3] 1079 (r, g, b) = colour[:3]
1067 colour_wx = [r * 255, g * 255, b * 255] 1080 colour_wx = [r * 255, g * 255, b * 255]
@@ -1108,6 +1121,7 @@ class Slice(metaclass=utils.Singleton): @@ -1108,6 +1121,7 @@ class Slice(metaclass=utils.Singleton):
1108 # TODO: find out a better way to do threshold 1121 # TODO: find out a better way to do threshold
1109 if slice_number is None: 1122 if slice_number is None:
1110 for n, slice_ in enumerate(self.matrix): 1123 for n, slice_ in enumerate(self.matrix):
  1124 + print(n)
1111 m = np.ones(slice_.shape, self.current_mask.matrix.dtype) 1125 m = np.ones(slice_.shape, self.current_mask.matrix.dtype)
1112 m[slice_ < thresh_min] = 0 1126 m[slice_ < thresh_min] = 0
1113 m[slice_ > thresh_max] = 0 1127 m[slice_ > thresh_max] = 0
@@ -1346,6 +1360,7 @@ class Slice(metaclass=utils.Singleton): @@ -1346,6 +1360,7 @@ class Slice(metaclass=utils.Singleton):
1346 """ 1360 """
1347 future_mask = Mask() 1361 future_mask = Mask()
1348 future_mask.create_mask(self.matrix.shape) 1362 future_mask.create_mask(self.matrix.shape)
  1363 + future_mask.spacing = self.spacing
1349 1364
1350 if name: 1365 if name:
1351 future_mask.name = name 1366 future_mask.name = name
@@ -1605,6 +1620,7 @@ class Slice(metaclass=utils.Singleton): @@ -1605,6 +1620,7 @@ class Slice(metaclass=utils.Singleton):
1605 1620
1606 future_mask = Mask() 1621 future_mask = Mask()
1607 future_mask.create_mask(self.matrix.shape) 1622 future_mask.create_mask(self.matrix.shape)
  1623 + future_mask.spacing = spacing
1608 future_mask.name = new_name 1624 future_mask.name = new_name
1609 1625
1610 future_mask.matrix[:] = 1 1626 future_mask.matrix[:] = 1
@@ -1805,6 +1821,7 @@ class Slice(metaclass=utils.Singleton): @@ -1805,6 +1821,7 @@ class Slice(metaclass=utils.Singleton):
1805 self.buffer_slices["CORONAL"].discard_vtk_mask() 1821 self.buffer_slices["CORONAL"].discard_vtk_mask()
1806 self.buffer_slices["SAGITAL"].discard_vtk_mask() 1822 self.buffer_slices["SAGITAL"].discard_vtk_mask()
1807 1823
  1824 + self.current_mask.modified(target == '3D')
1808 Publisher.sendMessage("Reload actual slice") 1825 Publisher.sendMessage("Reload actual slice")
1809 1826
1810 def calc_image_density(self, mask=None): 1827 def calc_image_density(self, mask=None):
invesalius/data/styles.py
@@ -1445,6 +1445,7 @@ class EditorInteractorStyle(DefaultInteractorStyle): @@ -1445,6 +1445,7 @@ class EditorInteractorStyle(DefaultInteractorStyle):
1445 self.viewer._flush_buffer = True 1445 self.viewer._flush_buffer = True
1446 self.viewer.slice_.apply_slice_buffer_to_mask(self.orientation) 1446 self.viewer.slice_.apply_slice_buffer_to_mask(self.orientation)
1447 self.viewer._flush_buffer = False 1447 self.viewer._flush_buffer = False
  1448 + self.viewer.slice_.current_mask.modified()
1448 1449
1449 def EOnScrollForward(self, evt, obj): 1450 def EOnScrollForward(self, evt, obj):
1450 iren = self.viewer.interactor 1451 iren = self.viewer.interactor
@@ -1822,7 +1823,6 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): @@ -1822,7 +1823,6 @@ class WaterShedInteractorStyle(DefaultInteractorStyle):
1822 self.viewer.slice_.current_mask.matrix[0 , 0, n+1] 1823 self.viewer.slice_.current_mask.matrix[0 , 0, n+1]
1823 markers = self.matrix[:, :, n] 1824 markers = self.matrix[:, :, n]
1824 1825
1825 -  
1826 ww = self.viewer.slice_.window_width 1826 ww = self.viewer.slice_.window_width
1827 wl = self.viewer.slice_.window_level 1827 wl = self.viewer.slice_.window_level
1828 1828
@@ -1866,6 +1866,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): @@ -1866,6 +1866,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle):
1866 1866
1867 1867
1868 self.viewer.slice_.current_mask.was_edited = True 1868 self.viewer.slice_.current_mask.was_edited = True
  1869 + self.viewer.slice_.current_mask.modified()
1869 self.viewer.slice_.current_mask.clear_history() 1870 self.viewer.slice_.current_mask.clear_history()
1870 1871
1871 # Marking the project as changed 1872 # Marking the project as changed
@@ -2017,10 +2018,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): @@ -2017,10 +2018,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle):
2017 mask[(tmp_mask==2) & ((mask == 0) | (mask == 2) | (mask == 253))] = 2 2018 mask[(tmp_mask==2) & ((mask == 0) | (mask == 2) | (mask == 253))] = 2
2018 mask[(tmp_mask==1) & ((mask == 0) | (mask == 2) | (mask == 253))] = 253 2019 mask[(tmp_mask==1) & ((mask == 0) | (mask == 2) | (mask == 253))] = 253
2019 2020
2020 - #mask[:] = tmp_mask  
2021 - self.viewer.slice_.current_mask.matrix[0] = 1  
2022 - self.viewer.slice_.current_mask.matrix[:, 0, :] = 1  
2023 - self.viewer.slice_.current_mask.matrix[:, :, 0] = 1 2021 + self.viewer.slice_.current_mask.modified(True)
2024 2022
2025 self.viewer.slice_.discard_all_buffers() 2023 self.viewer.slice_.discard_all_buffers()
2026 self.viewer.slice_.current_mask.clear_history() 2024 self.viewer.slice_.current_mask.clear_history()
@@ -2403,6 +2401,7 @@ class FloodFillMaskInteractorStyle(DefaultInteractorStyle): @@ -2403,6 +2401,7 @@ class FloodFillMaskInteractorStyle(DefaultInteractorStyle):
2403 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() 2401 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
2404 2402
2405 self.viewer.slice_.current_mask.was_edited = True 2403 self.viewer.slice_.current_mask.was_edited = True
  2404 + self.viewer.slice_.current_mask.modified(True)
2406 Publisher.sendMessage('Reload actual slice') 2405 Publisher.sendMessage('Reload actual slice')
2407 2406
2408 2407
@@ -2522,6 +2521,7 @@ class CropMaskInteractorStyle(DefaultInteractorStyle): @@ -2522,6 +2521,7 @@ class CropMaskInteractorStyle(DefaultInteractorStyle):
2522 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() 2521 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
2523 2522
2524 self.viewer.slice_.current_mask.was_edited = True 2523 self.viewer.slice_.current_mask.was_edited = True
  2524 + self.viewer.slice_.current_mask.modified(True)
2525 Publisher.sendMessage('Reload actual slice') 2525 Publisher.sendMessage('Reload actual slice')
2526 2526
2527 2527
@@ -2713,6 +2713,7 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): @@ -2713,6 +2713,7 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2713 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() 2713 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
2714 2714
2715 self.viewer.slice_.current_mask.was_edited = True 2715 self.viewer.slice_.current_mask.was_edited = True
  2716 + self.viewer.slice_.current_mask.modified(self.config.target == '3D')
2716 Publisher.sendMessage('Reload actual slice') 2717 Publisher.sendMessage('Reload actual slice')
2717 2718
2718 def do_2d_seg(self): 2719 def do_2d_seg(self):
invesalius/data/viewer_volume.py
@@ -320,6 +320,9 @@ class Viewer(wx.Panel): @@ -320,6 +320,9 @@ class Viewer(wx.Panel):
320 Publisher.subscribe(self.UpdateMarkerOffsetPosition, 'Update marker offset') 320 Publisher.subscribe(self.UpdateMarkerOffsetPosition, 'Update marker offset')
321 Publisher.subscribe(self.AddPeeledSurface, 'Update peel') 321 Publisher.subscribe(self.AddPeeledSurface, 'Update peel')
322 322
  323 + Publisher.subscribe(self.load_mask_preview, 'Load mask preview')
  324 + Publisher.subscribe(self.remove_mask_preview, 'Remove mask preview')
  325 +
323 def SetStereoMode(self, mode): 326 def SetStereoMode(self, mode):
324 ren_win = self.interactor.GetRenderWindow() 327 ren_win = self.interactor.GetRenderWindow()
325 328
@@ -1663,6 +1666,19 @@ class Viewer(wx.Panel): @@ -1663,6 +1666,19 @@ class Viewer(wx.Panel):
1663 # self._to_show_ball -= 1 1666 # self._to_show_ball -= 1
1664 # self._check_and_set_ball_visibility() 1667 # self._check_and_set_ball_visibility()
1665 1668
  1669 + def load_mask_preview(self, mask_3d_actor, flag=True):
  1670 + if flag:
  1671 + self.ren.AddVolume(mask_3d_actor)
  1672 + else:
  1673 + self.ren.RemoveVolume(mask_3d_actor)
  1674 +
  1675 + if self.ren.GetActors().GetNumberOfItems() == 0 and self.ren.GetVolumes().GetNumberOfItems() == 1:
  1676 + self.ren.ResetCamera()
  1677 + self.ren.ResetCameraClippingRange()
  1678 +
  1679 + def remove_mask_preview(self, mask_3d_actor):
  1680 + self.ren.RemoveVolume(mask_3d_actor)
  1681 +
1666 def OnSetViewAngle(self, view): 1682 def OnSetViewAngle(self, view):
1667 self.SetViewAngle(view) 1683 self.SetViewAngle(view)
1668 1684
@@ -1893,11 +1909,9 @@ class SlicePlane: @@ -1893,11 +1909,9 @@ class SlicePlane:
1893 Publisher.sendMessage('Update slice 3D', 1909 Publisher.sendMessage('Update slice 3D',
1894 widget=self.plane_z, 1910 widget=self.plane_z,
1895 orientation="AXIAL") 1911 orientation="AXIAL")
1896 -  
1897 1912
1898 def DeletePlanes(self): 1913 def DeletePlanes(self):
1899 del self.plane_x 1914 del self.plane_x
1900 del self.plane_y 1915 del self.plane_y
1901 - del self.plane_z  
1902 - 1916 + del self.plane_z
1903 1917
invesalius/data/volume.py
@@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
19 import plistlib 19 import plistlib
20 import os 20 import os
21 import weakref 21 import weakref
  22 +from distutils.version import LooseVersion
22 23
23 import numpy 24 import numpy
24 import vtk 25 import vtk
@@ -707,9 +708,93 @@ class Volume(): @@ -707,9 +708,93 @@ class Volume():
707 #else: 708 #else:
708 # valor = value 709 # valor = value
709 return value - scale[0] 710 return value - scale[0]
710 -  
711 -  
712 -class CutPlane: 711 +
  712 +class VolumeMask:
  713 + def __init__(self, mask):
  714 + self.mask = mask
  715 + self.colour = mask.colour
  716 + self._volume_mapper = None
  717 + self._flip = None
  718 + self._color_transfer = None
  719 + self._piecewise_function = None
  720 + self._actor = None
  721 +
  722 + def create_volume(self):
  723 + if self._actor is None:
  724 + if int(ses.Session().rendering) == 0:
  725 + self._volume_mapper = vtk.vtkFixedPointVolumeRayCastMapper()
  726 + #volume_mapper.AutoAdjustSampleDistancesOff()
  727 + self._volume_mapper.IntermixIntersectingGeometryOn()
  728 + pix_diag = 2.0
  729 + self._volume_mapper.SetImageSampleDistance(0.25)
  730 + self._volume_mapper.SetSampleDistance(pix_diag / 5.0)
  731 + else:
  732 + self._volume_mapper = vtk.vtkGPUVolumeRayCastMapper()
  733 + self._volume_mapper.UseJitteringOn()
  734 +
  735 + if LooseVersion(vtk.vtkVersion().GetVTKVersion()) > LooseVersion('8.0'):
  736 + self._volume_mapper.SetBlendModeToIsoSurface()
  737 +
  738 + # else:
  739 + # isosurfaceFunc = vtk.vtkVolumeRayCastIsosurfaceFunction()
  740 + # isosurfaceFunc.SetIsoValue(127)
  741 +
  742 + # self._volume_mapper = vtk.vtkVolumeRayCastMapper()
  743 + # self._volume_mapper.SetVolumeRayCastFunction(isosurfaceFunc)
  744 +
  745 + self._flip = vtk.vtkImageFlip()
  746 + self._flip.SetInputData(self.mask.imagedata)
  747 + self._flip.SetFilteredAxis(1)
  748 + self._flip.FlipAboutOriginOn()
  749 +
  750 + self._volume_mapper.SetInputConnection(self._flip.GetOutputPort())
  751 + self._volume_mapper.Update()
  752 +
  753 + r, g, b = self.colour
  754 +
  755 + self._color_transfer = vtk.vtkColorTransferFunction()
  756 + self._color_transfer.RemoveAllPoints()
  757 + self._color_transfer.AddRGBPoint(0.0, 0, 0, 0)
  758 + self._color_transfer.AddRGBPoint(254.0, r, g, b)
  759 + self._color_transfer.AddRGBPoint(255.0, r, g, b)
  760 +
  761 + self._piecewise_function = vtk.vtkPiecewiseFunction()
  762 + self._piecewise_function.RemoveAllPoints()
  763 + self._piecewise_function.AddPoint(0.0, 0.0)
  764 + self._piecewise_function.AddPoint(127, 1.0)
  765 +
  766 + self._volume_property = vtk.vtkVolumeProperty()
  767 + self._volume_property.SetColor(self._color_transfer)
  768 + self._volume_property.SetScalarOpacity(self._piecewise_function)
  769 + self._volume_property.ShadeOn()
  770 + self._volume_property.SetInterpolationTypeToLinear()
  771 + #vp.SetSpecular(1.75)
  772 + #vp.SetSpecularPower(8)
  773 +
  774 + if not self._volume_mapper.IsA("vtkGPUVolumeRayCastMapper"):
  775 + self._volume_property.SetScalarOpacityUnitDistance(pix_diag)
  776 + else:
  777 + if LooseVersion(vtk.vtkVersion().GetVTKVersion()) > LooseVersion('8.0'):
  778 + self._volume_property.GetIsoSurfaceValues().SetValue(0, 127)
  779 +
  780 + self._actor = vtk.vtkVolume()
  781 + self._actor.SetMapper(self._volume_mapper)
  782 + self._actor.SetProperty(self._volume_property)
  783 + self._actor.Update()
  784 +
  785 + def change_imagedata(self):
  786 + self._flip.SetInputData(self.mask.imagedata)
  787 +
  788 + def set_colour(self, colour):
  789 + self.colour = colour
  790 + r, g, b = self.colour
  791 + self._color_transfer.RemoveAllPoints()
  792 + self._color_transfer.AddRGBPoint(0.0, 0, 0, 0)
  793 + self._color_transfer.AddRGBPoint(254.0, r, g, b)
  794 + self._color_transfer.AddRGBPoint(255.0, r, g, b)
  795 +
  796 +
  797 +class CutPlane:
713 def __init__(self, img, volume_mapper): 798 def __init__(self, img, volume_mapper):
714 self.img = img 799 self.img = img
715 self.volume_mapper = volume_mapper 800 self.volume_mapper = volume_mapper
invesalius/gui/data_notebook.py
@@ -576,12 +576,9 @@ class MasksListCtrlPanel(InvListCtrl): @@ -576,12 +576,9 @@ class MasksListCtrlPanel(InvListCtrl):
576 Publisher.sendMessage('Show mask', index=index, value=flag) 576 Publisher.sendMessage('Show mask', index=index, value=flag)
577 577
578 578
579 -  
580 - def InsertNewItem(self, index=0, label=_("Mask"), threshold="(1000, 4500)",  
581 - colour=None): 579 + def InsertNewItem(self, index=0, label=_("Mask"), threshold="(1000, 4500)", colour=None):
582 self.InsertItem(index, "") 580 self.InsertItem(index, "")
583 - self.SetItem(index, 1, label,  
584 - imageId=self.mask_list_index[index]) 581 + self.SetItem(index, 1, label, imageId=self.mask_list_index[index])
585 self.SetItem(index, 2, threshold) 582 self.SetItem(index, 2, threshold)
586 # self.SetItemImage(index, 1) 583 # self.SetItemImage(index, 1)
587 # for key in self.mask_list_index.keys(): 584 # for key in self.mask_list_index.keys():
invesalius/gui/default_viewers.py
@@ -319,7 +319,6 @@ import wx.lib.buttons as btn @@ -319,7 +319,6 @@ import wx.lib.buttons as btn
319 from pubsub import pub as Publisher 319 from pubsub import pub as Publisher
320 import wx.lib.colourselect as csel 320 import wx.lib.colourselect as csel
321 321
322 -[BUTTON_RAYCASTING, BUTTON_VIEW, BUTTON_SLICE_PLANE, BUTTON_3D_STEREO, BUTTON_TARGET] = [wx.NewId() for num in range(5)]  
323 RAYCASTING_TOOLS = wx.NewId() 322 RAYCASTING_TOOLS = wx.NewId()
324 323
325 ID_TO_NAME = {} 324 ID_TO_NAME = {}
@@ -330,6 +329,8 @@ ID_TO_ITEMSLICEMENU = {} @@ -330,6 +329,8 @@ ID_TO_ITEMSLICEMENU = {}
330 ID_TO_ITEM_3DSTEREO = {} 329 ID_TO_ITEM_3DSTEREO = {}
331 ID_TO_STEREO_NAME = {} 330 ID_TO_STEREO_NAME = {}
332 331
  332 +ICON_SIZE = (32, 32)
  333 +
333 334
334 class VolumeViewerCover(wx.Panel): 335 class VolumeViewerCover(wx.Panel):
335 def __init__(self, parent): 336 def __init__(self, parent):
@@ -349,47 +350,22 @@ class VolumeToolPanel(wx.Panel): @@ -349,47 +350,22 @@ class VolumeToolPanel(wx.Panel):
349 wx.Panel.__init__(self, parent) 350 wx.Panel.__init__(self, parent)
350 351
351 # VOLUME RAYCASTING BUTTON 352 # VOLUME RAYCASTING BUTTON
352 - BMP_RAYCASTING = wx.Bitmap(os.path.join(inv_paths.ICON_DIR, "volume_raycasting.png"),  
353 - wx.BITMAP_TYPE_PNG)  
354 -  
355 - BMP_SLICE_PLANE = wx.Bitmap(os.path.join(inv_paths.ICON_DIR, "slice_plane.png"),  
356 - wx.BITMAP_TYPE_PNG)  
357 -  
358 -  
359 - BMP_3D_STEREO = wx.Bitmap(os.path.join(inv_paths.ICON_DIR, "3D_glasses.png"),  
360 - wx.BITMAP_TYPE_PNG)  
361 -  
362 - BMP_TARGET = wx.Bitmap(os.path.join(inv_paths.ICON_DIR, "target.png"),  
363 - wx.BITMAP_TYPE_PNG)  
364 -  
365 -  
366 - button_raycasting = pbtn.PlateButton(self, BUTTON_RAYCASTING,"",  
367 - BMP_RAYCASTING, style=pbtn.PB_STYLE_SQUARE,  
368 - size=(32,32))  
369 -  
370 - button_stereo = pbtn.PlateButton(self, BUTTON_3D_STEREO,"",  
371 - BMP_3D_STEREO, style=pbtn.PB_STYLE_SQUARE,  
372 - size=(32,32))  
373 -  
374 - button_slice_plane = self.button_slice_plane = pbtn.PlateButton(self, BUTTON_SLICE_PLANE,"",  
375 - BMP_SLICE_PLANE, style=pbtn.PB_STYLE_SQUARE,  
376 - size=(32,32))  
377 -  
378 - button_target = self.button_target = pbtn.PlateButton(self, BUTTON_TARGET,"",  
379 - BMP_TARGET, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE,  
380 - size=(32,32)) 353 + BMP_RAYCASTING = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("volume_raycasting.png")), wx.BITMAP_TYPE_PNG)
  354 + BMP_SLICE_PLANE = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("slice_plane.png")), wx.BITMAP_TYPE_PNG)
  355 + BMP_3D_STEREO = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("3D_glasses.png")), wx.BITMAP_TYPE_PNG)
  356 + BMP_TARGET = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("target.png")), wx.BITMAP_TYPE_PNG)
  357 + BMP_3D_MASK = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("file_from_internet.png")), wx.BITMAP_TYPE_PNG)
  358 +
  359 + self.button_raycasting = pbtn.PlateButton(self, -1,"", BMP_RAYCASTING, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE)
  360 + self.button_stereo = pbtn.PlateButton(self, -1,"", BMP_3D_STEREO, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE)
  361 + self.button_slice_plane = pbtn.PlateButton(self, -1, "", BMP_SLICE_PLANE, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE)
  362 + self.button_target = pbtn.PlateButton(self, -1,"", BMP_TARGET, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE, size=ICON_SIZE)
381 self.button_target.Enable(0) 363 self.button_target.Enable(0)
382 -  
383 - self.button_raycasting = button_raycasting  
384 - self.button_stereo = button_stereo 364 + # self.button_3d_mask = pbtn.PlateButton(self, -1, "", BMP_3D_MASK, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE, size=ICON_SIZE)
385 365
386 # VOLUME VIEW ANGLE BUTTON 366 # VOLUME VIEW ANGLE BUTTON
387 - BMP_FRONT = wx.Bitmap(ID_TO_BMP[const.VOL_FRONT][1],  
388 - wx.BITMAP_TYPE_PNG)  
389 - button_view = pbtn.PlateButton(self, BUTTON_VIEW, "",  
390 - BMP_FRONT, size=(32,32),  
391 - style=pbtn.PB_STYLE_SQUARE)  
392 - self.button_view = button_view 367 + BMP_FRONT = wx.Bitmap(ID_TO_BMP[const.VOL_FRONT][1], wx.BITMAP_TYPE_PNG)
  368 + self.button_view = pbtn.PlateButton(self, -1, "", BMP_FRONT, size=(32,32), style=pbtn.PB_STYLE_SQUARE)
393 369
394 # VOLUME COLOUR BUTTON 370 # VOLUME COLOUR BUTTON
395 if sys.platform.startswith('linux'): 371 if sys.platform.startswith('linux'):
@@ -399,18 +375,17 @@ class VolumeToolPanel(wx.Panel): @@ -399,18 +375,17 @@ class VolumeToolPanel(wx.Panel):
399 size = (24,24) 375 size = (24,24)
400 sp = 5 376 sp = 5
401 377
402 - button_colour= csel.ColourSelect(self, 111,colour=(0,0,0),  
403 - size=size)  
404 - self.button_colour = button_colour 378 + self.button_colour= csel.ColourSelect(self, -1, colour=(0,0,0), size=size)
405 379
406 # SIZER TO ORGANIZE ALL 380 # SIZER TO ORGANIZE ALL
407 sizer = wx.BoxSizer(wx.VERTICAL) 381 sizer = wx.BoxSizer(wx.VERTICAL)
408 - sizer.Add(button_colour, 0, wx.ALL, sp)  
409 - sizer.Add(button_raycasting, 0, wx.TOP|wx.BOTTOM, 1)  
410 - sizer.Add(button_view, 0, wx.TOP|wx.BOTTOM, 1)  
411 - sizer.Add(button_slice_plane, 0, wx.TOP|wx.BOTTOM, 1)  
412 - sizer.Add(button_stereo, 0, wx.TOP|wx.BOTTOM, 1)  
413 - sizer.Add(button_target, 0, wx.TOP | wx.BOTTOM, 1) 382 + sizer.Add(self.button_colour, 0, wx.ALL, sp)
  383 + sizer.Add(self.button_raycasting, 0, wx.TOP|wx.BOTTOM, 1)
  384 + sizer.Add(self.button_view, 0, wx.TOP|wx.BOTTOM, 1)
  385 + sizer.Add(self.button_slice_plane, 0, wx.TOP|wx.BOTTOM, 1)
  386 + sizer.Add(self.button_stereo, 0, wx.TOP|wx.BOTTOM, 1)
  387 + sizer.Add(self.button_target, 0, wx.TOP | wx.BOTTOM, 1)
  388 + # sizer.Add(self.button_3d_mask, 0, wx.TOP | wx.BOTTOM, 1)
414 389
415 self.navigation_status = False 390 self.navigation_status = False
416 self.status_target_select = False 391 self.status_target_select = False
invesalius/gui/frame.py
@@ -118,6 +118,7 @@ class Frame(wx.Frame): @@ -118,6 +118,7 @@ class Frame(wx.Frame):
118 self.actived_interpolated_slices = main_menu.view_menu 118 self.actived_interpolated_slices = main_menu.view_menu
119 self.actived_navigation_mode = main_menu.mode_menu 119 self.actived_navigation_mode = main_menu.mode_menu
120 self.actived_dbs_mode = main_menu.mode_dbs 120 self.actived_dbs_mode = main_menu.mode_dbs
  121 + self.tools_menu = main_menu.tools_menu
121 122
122 # Set menus, status and task bar 123 # Set menus, status and task bar
123 self.SetMenuBar(main_menu) 124 self.SetMenuBar(main_menu)
@@ -538,6 +539,15 @@ class Frame(wx.Frame): @@ -538,6 +539,15 @@ class Frame(wx.Frame):
538 elif id == const.ID_CROP_MASK: 539 elif id == const.ID_CROP_MASK:
539 self.OnCropMask() 540 self.OnCropMask()
540 541
  542 + elif id == const.ID_MASK_3D_PREVIEW:
  543 + self.OnEnableMask3DPreview(value=self.tools_menu.IsChecked(const.ID_MASK_3D_PREVIEW))
  544 +
  545 + elif id == const.ID_MASK_3D_AUTO_RELOAD:
  546 + ses.Session().auto_reload_preview = self.tools_menu.IsChecked(const.ID_MASK_3D_AUTO_RELOAD)
  547 +
  548 + elif id == const.ID_MASK_3D_RELOAD:
  549 + self.OnUpdateMaskPreview()
  550 +
541 elif id == const.ID_CREATE_SURFACE: 551 elif id == const.ID_CREATE_SURFACE:
542 Publisher.sendMessage('Open create surface dialog') 552 Publisher.sendMessage('Open create surface dialog')
543 553
@@ -769,6 +779,15 @@ class Frame(wx.Frame): @@ -769,6 +779,15 @@ class Frame(wx.Frame):
769 def OnCropMask(self): 779 def OnCropMask(self):
770 Publisher.sendMessage('Enable style', style=const.SLICE_STATE_CROP_MASK) 780 Publisher.sendMessage('Enable style', style=const.SLICE_STATE_CROP_MASK)
771 781
  782 + def OnEnableMask3DPreview(self, value):
  783 + if value:
  784 + Publisher.sendMessage('Enable mask 3D preview')
  785 + else:
  786 + Publisher.sendMessage('Disable mask 3D preview')
  787 +
  788 + def OnUpdateMaskPreview(self):
  789 + Publisher.sendMessage('Update mask 3D preview')
  790 +
772 def ShowPluginsFolder(self): 791 def ShowPluginsFolder(self):
773 """ 792 """
774 Show getting started window. 793 Show getting started window.
@@ -954,6 +973,22 @@ class MenuBar(wx.MenuBar): @@ -954,6 +973,22 @@ class MenuBar(wx.MenuBar):
954 self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop")) 973 self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop"))
955 self.crop_mask_menu.Enable(False) 974 self.crop_mask_menu.Enable(False)
956 975
  976 + mask_menu.AppendSeparator()
  977 +
  978 + mask_preview_menu = wx.Menu()
  979 +
  980 + self.mask_preview = mask_preview_menu.Append(const.ID_MASK_3D_PREVIEW, _("Enable") + "\tCtrl+Shift+M", "", wx.ITEM_CHECK)
  981 + self.mask_preview.Enable(False)
  982 +
  983 + self.mask_auto_reload = mask_preview_menu.Append(const.ID_MASK_3D_AUTO_RELOAD, _("Auto reload") + "\tCtrl+Shift+U", "", wx.ITEM_CHECK)
  984 + self.mask_auto_reload.Check(ses.Session().auto_reload_preview)
  985 + self.mask_auto_reload.Enable(False)
  986 +
  987 + self.mask_preview_reload = mask_preview_menu.Append(const.ID_MASK_3D_RELOAD, _("Reload") + "\tCtrl+Shift+R")
  988 + self.mask_preview_reload.Enable(False)
  989 +
  990 + mask_menu.Append(-1, _('Mask 3D Preview'), mask_preview_menu)
  991 +
957 # Segmentation Menu 992 # Segmentation Menu
958 segmentation_menu = wx.Menu() 993 segmentation_menu = wx.Menu()
959 self.threshold_segmentation = segmentation_menu.Append(const.ID_THRESHOLD_SEGMENTATION, _(u"Threshold\tCtrl+Shift+T")) 994 self.threshold_segmentation = segmentation_menu.Append(const.ID_THRESHOLD_SEGMENTATION, _(u"Threshold\tCtrl+Shift+T"))
@@ -995,6 +1030,7 @@ class MenuBar(wx.MenuBar): @@ -995,6 +1030,7 @@ class MenuBar(wx.MenuBar):
995 tools_menu.Append(-1, _(u"Mask"), mask_menu) 1030 tools_menu.Append(-1, _(u"Mask"), mask_menu)
996 tools_menu.Append(-1, _(u"Segmentation"), segmentation_menu) 1031 tools_menu.Append(-1, _(u"Segmentation"), segmentation_menu)
997 tools_menu.Append(-1, _(u"Surface"), surface_menu) 1032 tools_menu.Append(-1, _(u"Surface"), surface_menu)
  1033 + self.tools_menu = tools_menu
998 1034
999 #View 1035 #View
1000 self.view_menu = view_menu = wx.Menu() 1036 self.view_menu = view_menu = wx.Menu()
@@ -1200,6 +1236,9 @@ class MenuBar(wx.MenuBar): @@ -1200,6 +1236,9 @@ class MenuBar(wx.MenuBar):
1200 def OnAddMask(self, mask): 1236 def OnAddMask(self, mask):
1201 self.num_masks += 1 1237 self.num_masks += 1
1202 self.bool_op_menu.Enable(self.num_masks >= 2) 1238 self.bool_op_menu.Enable(self.num_masks >= 2)
  1239 + self.mask_preview.Enable(True)
  1240 + self.mask_auto_reload.Enable(True)
  1241 + self.mask_preview_reload.Enable(True)
1203 1242
1204 def OnRemoveMasks(self, mask_indexes): 1243 def OnRemoveMasks(self, mask_indexes):
1205 self.num_masks -= len(mask_indexes) 1244 self.num_masks -= len(mask_indexes)
@@ -1208,6 +1247,9 @@ class MenuBar(wx.MenuBar): @@ -1208,6 +1247,9 @@ class MenuBar(wx.MenuBar):
1208 def OnShowMask(self, index, value): 1247 def OnShowMask(self, index, value):
1209 self.clean_mask_menu.Enable(value) 1248 self.clean_mask_menu.Enable(value)
1210 self.crop_mask_menu.Enable(value) 1249 self.crop_mask_menu.Enable(value)
  1250 + self.mask_preview.Enable(value)
  1251 + self.mask_auto_reload.Enable(value)
  1252 + self.mask_preview_reload.Enable(value)
1211 1253
1212 1254
1213 # ------------------------------------------------------------------ 1255 # ------------------------------------------------------------------
invesalius/project.py
@@ -120,6 +120,8 @@ class Project(metaclass=Singleton): @@ -120,6 +120,8 @@ class Project(metaclass=Singleton):
120 def RemoveMask(self, index): 120 def RemoveMask(self, index):
121 new_dict = {} 121 new_dict = {}
122 for i in self.mask_dict: 122 for i in self.mask_dict:
  123 + mask = self.mask_dict[i]
  124 + mask.cleanup()
123 if i < index: 125 if i < index:
124 new_dict[i] = self.mask_dict[i] 126 new_dict[i] = self.mask_dict[i]
125 if i > index: 127 if i > index:
@@ -331,6 +333,7 @@ class Project(metaclass=Singleton): @@ -331,6 +333,7 @@ class Project(metaclass=Singleton):
331 filename = project["masks"][index] 333 filename = project["masks"][index]
332 filepath = os.path.join(dirpath, filename) 334 filepath = os.path.join(dirpath, filename)
333 m = msk.Mask() 335 m = msk.Mask()
  336 + m.spacing = self.spacing
334 m.OpenPList(filepath) 337 m.OpenPList(filepath)
335 self.mask_dict[m.index] = m 338 self.mask_dict[m.index] = m
336 339
invesalius/segmentation/brain/segment.py
@@ -175,8 +175,8 @@ class SegmentProcess(ctx.Process): @@ -175,8 +175,8 @@ class SegmentProcess(ctx.Process):
175 self.mask = slc.Slice().create_new_mask(name=name) 175 self.mask = slc.Slice().create_new_mask(name=name)
176 176
177 self.mask.was_edited = True 177 self.mask.was_edited = True
178 - self.mask.matrix[:] = 1  
179 self.mask.matrix[1:, 1:, 1:] = (self._probability_array >= threshold) * 255 178 self.mask.matrix[1:, 1:, 1:] = (self._probability_array >= threshold) * 255
  179 + self.mask.modified(True)
180 180
181 def get_completion(self): 181 def get_completion(self):
182 return self._comm_array[0] 182 return self._comm_array[0]
invesalius/session.py
@@ -34,7 +34,7 @@ import json @@ -34,7 +34,7 @@ import json
34 from pubsub import pub as Publisher 34 from pubsub import pub as Publisher
35 import wx 35 import wx
36 36
37 -from invesalius.utils import Singleton, debug, decode 37 +from invesalius.utils import Singleton, debug, decode, deep_merge_dict
38 from random import randint 38 from random import randint
39 39
40 from invesalius import inv_paths 40 from invesalius import inv_paths
@@ -59,6 +59,7 @@ class Session(metaclass=Singleton): @@ -59,6 +59,7 @@ class Session(metaclass=Singleton):
59 'session': { 59 'session': {
60 'status': 3, 60 'status': 3,
61 'language': '', 61 'language': '',
  62 + 'auto_reload_preview': False,
62 }, 63 },
63 'project': { 64 'project': {
64 }, 65 },
@@ -76,6 +77,7 @@ class Session(metaclass=Singleton): @@ -76,6 +77,7 @@ class Session(metaclass=Singleton):
76 'surface_interpolation': ('session', 'surface_interpolation'), 77 'surface_interpolation': ('session', 'surface_interpolation'),
77 'rendering': ('session', 'rendering'), 78 'rendering': ('session', 'rendering'),
78 'slice_interpolation': ('session', 'slice_interpolation'), 79 'slice_interpolation': ('session', 'slice_interpolation'),
  80 + 'auto_reload_preview': ('session', 'auto_reload_preview'),
79 'recent_projects': ('project', 'recent_projects'), 81 'recent_projects': ('project', 'recent_projects'),
80 'homedir': ('paths', 'homedir'), 82 'homedir': ('paths', 'homedir'),
81 'tempdir': ('paths', 'homedir'), 83 'tempdir': ('paths', 'homedir'),
@@ -95,6 +97,7 @@ class Session(metaclass=Singleton): @@ -95,6 +97,7 @@ class Session(metaclass=Singleton):
95 'surface_interpolation': 1, 97 'surface_interpolation': 1,
96 'rendering': 0, 98 'rendering': 0,
97 'slice_interpolation': 0, 99 'slice_interpolation': 0,
  100 + 'auto_reload_preview': False,
98 }, 101 },
99 102
100 'project': { 103 'project': {
@@ -268,7 +271,8 @@ class Session(metaclass=Singleton): @@ -268,7 +271,8 @@ class Session(metaclass=Singleton):
268 def _read_cfg_from_json(self, json_filename): 271 def _read_cfg_from_json(self, json_filename):
269 with open(json_filename, 'r') as cfg_file: 272 with open(json_filename, 'r') as cfg_file:
270 cfg_dict = json.load(cfg_file) 273 cfg_dict = json.load(cfg_file)
271 - self._values.update(cfg_dict) 274 + self._value = deep_merge_dict(self._values, cfg_dict)
  275 + print(self._values)
272 276
273 # Do not reading project status from the config file, since there 277 # Do not reading project status from the config file, since there
274 # isn't a recover session tool in InVesalius yet. 278 # isn't a recover session tool in InVesalius yet.
invesalius/utils.py
@@ -23,6 +23,7 @@ import re @@ -23,6 +23,7 @@ import re
23 import locale 23 import locale
24 import math 24 import math
25 import traceback 25 import traceback
  26 +import collections.abc
26 27
27 from distutils.version import LooseVersion 28 from distutils.version import LooseVersion
28 from functools import wraps 29 from functools import wraps
@@ -487,3 +488,13 @@ def log_traceback(ex): @@ -487,3 +488,13 @@ def log_traceback(ex):
487 tb_lines = [line.rstrip('\n') for line in 488 tb_lines = [line.rstrip('\n') for line in
488 traceback.format_exception(ex.__class__, ex, ex_traceback)] 489 traceback.format_exception(ex.__class__, ex, ex_traceback)]
489 return ''.join(tb_lines) 490 return ''.join(tb_lines)
  491 +
  492 +
  493 +
  494 +def deep_merge_dict(d, u):
  495 + for k, v in u.items():
  496 + if isinstance(v, collections.abc.Mapping):
  497 + d[k] = deep_merge_dict(d.get(k, {}), v)
  498 + else:
  499 + d[k] = v
  500 + return d