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 | 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 | ... | ... |