Commit 99e6d36b1697f260027dee9cfac67603dffdfdce
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
Showing
15 changed files
with
385 additions
and
83 deletions
Show diff stats
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 |