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 516 ID_MASK_DENSITY_MEASURE = wx.NewId()
517 517 ID_CREATE_SURFACE = wx.NewId()
518 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 523 ID_GOTO_SLICE = wx.NewId()
521 524 ID_GOTO_COORD = wx.NewId()
... ...
invesalius/control.py
... ... @@ -126,6 +126,12 @@ class Controller():
126 126  
127 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 135 def SetBitmapSpacing(self, spacing):
130 136 proj = prj.Project()
131 137 proj.spacing = spacing
... ... @@ -1108,3 +1114,30 @@ class Controller():
1108 1114  
1109 1115 if err_msg:
1110 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 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 28 if orientation == "SAGITTAL":
30 29 orientation = "SAGITAL"
31 30  
... ... @@ -67,6 +66,37 @@ def to_vtk(n_array, spacing, slice_number, orientation, origin=(0, 0, 0), paddin
67 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 100 def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)):
71 101 dy, dx, dc = n_array.shape
72 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 22 import random
23 23 import shutil
24 24 import tempfile
25   -
26   -import numpy as np
27   -import vtk
  25 +import time
  26 +import weakref
28 27  
29 28 import invesalius.constants as const
  29 +import invesalius.data.converters as converters
30 30 import invesalius.data.imagedata_utils as iu
31 31 import invesalius.session as ses
32   -
  32 +from invesalius.data.volume import VolumeMask
  33 +import numpy as np
  34 +import vtk
33 35 from invesalius_cy import floodfill
34   -
35 36 from pubsub import pub as Publisher
36 37 from scipy import ndimage
  38 +from vtk.util import numpy_support
  39 +
37 40  
38 41 class EditionHistoryNode(object):
39 42 def __init__(self, index, orientation, array, clean=False):
... ... @@ -189,7 +192,9 @@ class Mask():
189 192 def __init__(self):
190 193 Mask.general_index += 1
191 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 198 self.colour = random.choice(const.MASK_COLOUR)
194 199 self.opacity = const.MASK_OPACITY
195 200 self.threshold_range = const.THRESHOLD_RANGE
... ... @@ -198,7 +203,11 @@ class Mask():
198 203 self.is_shown = 1
199 204 self.edited_points = {}
200 205 self.was_edited = False
  206 + self.volume = None
  207 + self.auto_update_mask = True
  208 + self.modified_time = 0
201 209 self.__bind_events()
  210 + self._modified_callbacks = []
202 211  
203 212 self.history = EditionHistory()
204 213  
... ... @@ -206,11 +215,24 @@ class Mask():
206 215 Publisher.subscribe(self.OnFlipVolume, 'Flip volume')
207 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 230 def save_history(self, index, orientation, array, p_array, clean=False):
210 231 self.history.new_node(index, orientation, array, p_array, clean)
211 232  
212 233 def undo_history(self, actual_slices):
213 234 self.history.undo(self.matrix, actual_slices)
  235 + self.modified()
214 236  
215 237 # Marking the project as changed
216 238 session = ses.Session()
... ... @@ -218,6 +240,7 @@ class Mask():
218 240  
219 241 def redo_history(self, actual_slices):
220 242 self.history.redo(self.matrix, actual_slices)
  243 + self.modified()
221 244  
222 245 # Marking the project as changed
223 246 session = ses.Session()
... ... @@ -226,6 +249,27 @@ class Mask():
226 249 def on_show(self):
227 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 273 def SavePlist(self, dir_temp, filelist):
230 274 mask = {}
231 275 filename = u'mask_%d' % self.index
... ... @@ -284,10 +328,15 @@ class Mask():
284 328 elif axis == 2:
285 329 submatrix[:] = submatrix[:, :, ::-1]
286 330 self.matrix[0, 0, 1::] = self.matrix[0, 0, :0:-1]
  331 + self.modified()
287 332  
288 333 def OnSwapVolumeAxes(self, axes):
289 334 axis0, axis1 = axes
290 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 341 def _save_mask(self, filename):
293 342 shutil.copyfile(self.temp_file, filename)
... ... @@ -300,6 +349,22 @@ class Mask():
300 349 def _set_class_index(self, index):
301 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 368 def create_mask(self, shape):
304 369 """
305 370 Creates a new mask object. This method do not append this new mask into the project.
... ... @@ -311,11 +376,35 @@ class Mask():
311 376 shape = shape[0] + 1, shape[1] + 1, shape[2] + 1
312 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 395 def clean(self):
315 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 409 def copy(self, copy_name):
321 410 """
... ... @@ -334,6 +423,7 @@ class Mask():
334 423  
335 424 new_mask.create_mask(shape=[i-1 for i in self.matrix.shape])
336 425 new_mask.matrix[:] = self.matrix[:]
  426 + new_mask.spacing = self.spacing
337 427  
338 428 return new_mask
339 429  
... ... @@ -384,7 +474,4 @@ class Mask():
384 474 self.save_history(index, orientation, matrix.copy(), cp_mask)
385 475  
386 476 def __del__(self):
387   - if self.is_shown:
388   - self.history._config_undo_redo(False)
389   - del self.matrix
390 477 os.remove(self.temp_file)
... ...
invesalius/data/slice_.py
... ... @@ -384,9 +384,21 @@ class Slice(metaclass=utils.Singleton):
384 384 self.current_mask.matrix[:] = 0
385 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 400 to_reload = False
388 401 if threshold_range != self.current_mask.threshold_range:
389   - to_reload = True
390 402 for orientation in self.buffer_slices:
391 403 self.buffer_slices[orientation].discard_vtk_mask()
392 404 self.SetMaskThreshold(
... ... @@ -417,6 +429,7 @@ class Slice(metaclass=utils.Singleton):
417 429  
418 430 if to_reload:
419 431 Publisher.sendMessage("Reload actual slice")
  432 + self.current_mask.modified(all_volume=False)
420 433  
421 434 def __set_current_mask_threshold_actual_slice(self, threshold_range):
422 435 if self.current_mask is None:
... ... @@ -1061,7 +1074,7 @@ class Slice(metaclass=utils.Singleton):
1061 1074 def SetMaskColour(self, index, colour, update=True):
1062 1075 "Set a mask colour given its index and colour (RGB 0-1 values)"
1063 1076 proj = Project()
1064   - proj.mask_dict[index].colour = colour
  1077 + proj.mask_dict[index].set_colour(colour)
1065 1078  
1066 1079 (r, g, b) = colour[:3]
1067 1080 colour_wx = [r * 255, g * 255, b * 255]
... ... @@ -1108,6 +1121,7 @@ class Slice(metaclass=utils.Singleton):
1108 1121 # TODO: find out a better way to do threshold
1109 1122 if slice_number is None:
1110 1123 for n, slice_ in enumerate(self.matrix):
  1124 + print(n)
1111 1125 m = np.ones(slice_.shape, self.current_mask.matrix.dtype)
1112 1126 m[slice_ < thresh_min] = 0
1113 1127 m[slice_ > thresh_max] = 0
... ... @@ -1346,6 +1360,7 @@ class Slice(metaclass=utils.Singleton):
1346 1360 """
1347 1361 future_mask = Mask()
1348 1362 future_mask.create_mask(self.matrix.shape)
  1363 + future_mask.spacing = self.spacing
1349 1364  
1350 1365 if name:
1351 1366 future_mask.name = name
... ... @@ -1605,6 +1620,7 @@ class Slice(metaclass=utils.Singleton):
1605 1620  
1606 1621 future_mask = Mask()
1607 1622 future_mask.create_mask(self.matrix.shape)
  1623 + future_mask.spacing = spacing
1608 1624 future_mask.name = new_name
1609 1625  
1610 1626 future_mask.matrix[:] = 1
... ... @@ -1805,6 +1821,7 @@ class Slice(metaclass=utils.Singleton):
1805 1821 self.buffer_slices["CORONAL"].discard_vtk_mask()
1806 1822 self.buffer_slices["SAGITAL"].discard_vtk_mask()
1807 1823  
  1824 + self.current_mask.modified(target == '3D')
1808 1825 Publisher.sendMessage("Reload actual slice")
1809 1826  
1810 1827 def calc_image_density(self, mask=None):
... ...
invesalius/data/styles.py
... ... @@ -1445,6 +1445,7 @@ class EditorInteractorStyle(DefaultInteractorStyle):
1445 1445 self.viewer._flush_buffer = True
1446 1446 self.viewer.slice_.apply_slice_buffer_to_mask(self.orientation)
1447 1447 self.viewer._flush_buffer = False
  1448 + self.viewer.slice_.current_mask.modified()
1448 1449  
1449 1450 def EOnScrollForward(self, evt, obj):
1450 1451 iren = self.viewer.interactor
... ... @@ -1822,7 +1823,6 @@ class WaterShedInteractorStyle(DefaultInteractorStyle):
1822 1823 self.viewer.slice_.current_mask.matrix[0 , 0, n+1]
1823 1824 markers = self.matrix[:, :, n]
1824 1825  
1825   -
1826 1826 ww = self.viewer.slice_.window_width
1827 1827 wl = self.viewer.slice_.window_level
1828 1828  
... ... @@ -1866,6 +1866,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle):
1866 1866  
1867 1867  
1868 1868 self.viewer.slice_.current_mask.was_edited = True
  1869 + self.viewer.slice_.current_mask.modified()
1869 1870 self.viewer.slice_.current_mask.clear_history()
1870 1871  
1871 1872 # Marking the project as changed
... ... @@ -2017,10 +2018,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle):
2017 2018 mask[(tmp_mask==2) & ((mask == 0) | (mask == 2) | (mask == 253))] = 2
2018 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 2023 self.viewer.slice_.discard_all_buffers()
2026 2024 self.viewer.slice_.current_mask.clear_history()
... ... @@ -2403,6 +2401,7 @@ class FloodFillMaskInteractorStyle(DefaultInteractorStyle):
2403 2401 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
2404 2402  
2405 2403 self.viewer.slice_.current_mask.was_edited = True
  2404 + self.viewer.slice_.current_mask.modified(True)
2406 2405 Publisher.sendMessage('Reload actual slice')
2407 2406  
2408 2407  
... ... @@ -2522,6 +2521,7 @@ class CropMaskInteractorStyle(DefaultInteractorStyle):
2522 2521 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
2523 2522  
2524 2523 self.viewer.slice_.current_mask.was_edited = True
  2524 + self.viewer.slice_.current_mask.modified(True)
2525 2525 Publisher.sendMessage('Reload actual slice')
2526 2526  
2527 2527  
... ... @@ -2713,6 +2713,7 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2713 2713 self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
2714 2714  
2715 2715 self.viewer.slice_.current_mask.was_edited = True
  2716 + self.viewer.slice_.current_mask.modified(self.config.target == '3D')
2716 2717 Publisher.sendMessage('Reload actual slice')
2717 2718  
2718 2719 def do_2d_seg(self):
... ...
invesalius/data/viewer_volume.py
... ... @@ -320,6 +320,9 @@ class Viewer(wx.Panel):
320 320 Publisher.subscribe(self.UpdateMarkerOffsetPosition, 'Update marker offset')
321 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 326 def SetStereoMode(self, mode):
324 327 ren_win = self.interactor.GetRenderWindow()
325 328  
... ... @@ -1663,6 +1666,19 @@ class Viewer(wx.Panel):
1663 1666 # self._to_show_ball -= 1
1664 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 1682 def OnSetViewAngle(self, view):
1667 1683 self.SetViewAngle(view)
1668 1684  
... ... @@ -1893,11 +1909,9 @@ class SlicePlane:
1893 1909 Publisher.sendMessage('Update slice 3D',
1894 1910 widget=self.plane_z,
1895 1911 orientation="AXIAL")
1896   -
1897 1912  
1898 1913 def DeletePlanes(self):
1899 1914 del self.plane_x
1900 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 19 import plistlib
20 20 import os
21 21 import weakref
  22 +from distutils.version import LooseVersion
22 23  
23 24 import numpy
24 25 import vtk
... ... @@ -707,9 +708,93 @@ class Volume():
707 708 #else:
708 709 # valor = value
709 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 798 def __init__(self, img, volume_mapper):
714 799 self.img = img
715 800 self.volume_mapper = volume_mapper
... ...
invesalius/gui/data_notebook.py
... ... @@ -576,12 +576,9 @@ class MasksListCtrlPanel(InvListCtrl):
576 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 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 582 self.SetItem(index, 2, threshold)
586 583 # self.SetItemImage(index, 1)
587 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 319 from pubsub import pub as Publisher
320 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 322 RAYCASTING_TOOLS = wx.NewId()
324 323  
325 324 ID_TO_NAME = {}
... ... @@ -330,6 +329,8 @@ ID_TO_ITEMSLICEMENU = {}
330 329 ID_TO_ITEM_3DSTEREO = {}
331 330 ID_TO_STEREO_NAME = {}
332 331  
  332 +ICON_SIZE = (32, 32)
  333 +
333 334  
334 335 class VolumeViewerCover(wx.Panel):
335 336 def __init__(self, parent):
... ... @@ -349,47 +350,22 @@ class VolumeToolPanel(wx.Panel):
349 350 wx.Panel.__init__(self, parent)
350 351  
351 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 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 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 370 # VOLUME COLOUR BUTTON
395 371 if sys.platform.startswith('linux'):
... ... @@ -399,18 +375,17 @@ class VolumeToolPanel(wx.Panel):
399 375 size = (24,24)
400 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 380 # SIZER TO ORGANIZE ALL
407 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 390 self.navigation_status = False
416 391 self.status_target_select = False
... ...
invesalius/gui/frame.py
... ... @@ -118,6 +118,7 @@ class Frame(wx.Frame):
118 118 self.actived_interpolated_slices = main_menu.view_menu
119 119 self.actived_navigation_mode = main_menu.mode_menu
120 120 self.actived_dbs_mode = main_menu.mode_dbs
  121 + self.tools_menu = main_menu.tools_menu
121 122  
122 123 # Set menus, status and task bar
123 124 self.SetMenuBar(main_menu)
... ... @@ -538,6 +539,15 @@ class Frame(wx.Frame):
538 539 elif id == const.ID_CROP_MASK:
539 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 551 elif id == const.ID_CREATE_SURFACE:
542 552 Publisher.sendMessage('Open create surface dialog')
543 553  
... ... @@ -769,6 +779,15 @@ class Frame(wx.Frame):
769 779 def OnCropMask(self):
770 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 791 def ShowPluginsFolder(self):
773 792 """
774 793 Show getting started window.
... ... @@ -954,6 +973,22 @@ class MenuBar(wx.MenuBar):
954 973 self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop"))
955 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 992 # Segmentation Menu
958 993 segmentation_menu = wx.Menu()
959 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 1030 tools_menu.Append(-1, _(u"Mask"), mask_menu)
996 1031 tools_menu.Append(-1, _(u"Segmentation"), segmentation_menu)
997 1032 tools_menu.Append(-1, _(u"Surface"), surface_menu)
  1033 + self.tools_menu = tools_menu
998 1034  
999 1035 #View
1000 1036 self.view_menu = view_menu = wx.Menu()
... ... @@ -1200,6 +1236,9 @@ class MenuBar(wx.MenuBar):
1200 1236 def OnAddMask(self, mask):
1201 1237 self.num_masks += 1
1202 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 1243 def OnRemoveMasks(self, mask_indexes):
1205 1244 self.num_masks -= len(mask_indexes)
... ... @@ -1208,6 +1247,9 @@ class MenuBar(wx.MenuBar):
1208 1247 def OnShowMask(self, index, value):
1209 1248 self.clean_mask_menu.Enable(value)
1210 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 120 def RemoveMask(self, index):
121 121 new_dict = {}
122 122 for i in self.mask_dict:
  123 + mask = self.mask_dict[i]
  124 + mask.cleanup()
123 125 if i < index:
124 126 new_dict[i] = self.mask_dict[i]
125 127 if i > index:
... ... @@ -331,6 +333,7 @@ class Project(metaclass=Singleton):
331 333 filename = project["masks"][index]
332 334 filepath = os.path.join(dirpath, filename)
333 335 m = msk.Mask()
  336 + m.spacing = self.spacing
334 337 m.OpenPList(filepath)
335 338 self.mask_dict[m.index] = m
336 339  
... ...
invesalius/segmentation/brain/segment.py
... ... @@ -175,8 +175,8 @@ class SegmentProcess(ctx.Process):
175 175 self.mask = slc.Slice().create_new_mask(name=name)
176 176  
177 177 self.mask.was_edited = True
178   - self.mask.matrix[:] = 1
179 178 self.mask.matrix[1:, 1:, 1:] = (self._probability_array >= threshold) * 255
  179 + self.mask.modified(True)
180 180  
181 181 def get_completion(self):
182 182 return self._comm_array[0]
... ...
invesalius/session.py
... ... @@ -34,7 +34,7 @@ import json
34 34 from pubsub import pub as Publisher
35 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 38 from random import randint
39 39  
40 40 from invesalius import inv_paths
... ... @@ -59,6 +59,7 @@ class Session(metaclass=Singleton):
59 59 'session': {
60 60 'status': 3,
61 61 'language': '',
  62 + 'auto_reload_preview': False,
62 63 },
63 64 'project': {
64 65 },
... ... @@ -76,6 +77,7 @@ class Session(metaclass=Singleton):
76 77 'surface_interpolation': ('session', 'surface_interpolation'),
77 78 'rendering': ('session', 'rendering'),
78 79 'slice_interpolation': ('session', 'slice_interpolation'),
  80 + 'auto_reload_preview': ('session', 'auto_reload_preview'),
79 81 'recent_projects': ('project', 'recent_projects'),
80 82 'homedir': ('paths', 'homedir'),
81 83 'tempdir': ('paths', 'homedir'),
... ... @@ -95,6 +97,7 @@ class Session(metaclass=Singleton):
95 97 'surface_interpolation': 1,
96 98 'rendering': 0,
97 99 'slice_interpolation': 0,
  100 + 'auto_reload_preview': False,
98 101 },
99 102  
100 103 'project': {
... ... @@ -268,7 +271,8 @@ class Session(metaclass=Singleton):
268 271 def _read_cfg_from_json(self, json_filename):
269 272 with open(json_filename, 'r') as cfg_file:
270 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 277 # Do not reading project status from the config file, since there
274 278 # isn't a recover session tool in InVesalius yet.
... ...
invesalius/utils.py
... ... @@ -23,6 +23,7 @@ import re
23 23 import locale
24 24 import math
25 25 import traceback
  26 +import collections.abc
26 27  
27 28 from distutils.version import LooseVersion
28 29 from functools import wraps
... ... @@ -487,3 +488,13 @@ def log_traceback(ex):
487 488 tb_lines = [line.rstrip('\n') for line in
488 489 traceback.format_exception(ex.__class__, ex, ex_traceback)]
489 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
... ...