Commit 388d09bdbdcb80deed072310532a9b4a57fba34e
Committed by
GitHub
1 parent
d7439bcd
Exists in
master
and in
16 other branches
Fill holes automatically (#56)
Close holes 2D or 3D automatically with size <= max_size Ctrl-z and Ctrl-y is working
Showing
6 changed files
with
269 additions
and
11 deletions
Show diff stats
invesalius/constants.py
| @@ -517,6 +517,7 @@ ID_CLEAN_MASK = wx.NewId() | @@ -517,6 +517,7 @@ ID_CLEAN_MASK = wx.NewId() | ||
| 517 | 517 | ||
| 518 | ID_REORIENT_IMG = wx.NewId() | 518 | ID_REORIENT_IMG = wx.NewId() |
| 519 | ID_FLOODFILL_MASK = wx.NewId() | 519 | ID_FLOODFILL_MASK = wx.NewId() |
| 520 | +ID_FILL_HOLE_AUTO = wx.NewId() | ||
| 520 | ID_REMOVE_MASK_PART = wx.NewId() | 521 | ID_REMOVE_MASK_PART = wx.NewId() |
| 521 | ID_SELECT_MASK_PART = wx.NewId() | 522 | ID_SELECT_MASK_PART = wx.NewId() |
| 522 | ID_FLOODFILL_SEGMENTATION = wx.NewId() | 523 | ID_FLOODFILL_SEGMENTATION = wx.NewId() |
invesalius/data/floodfill.pyx
| @@ -4,7 +4,9 @@ cimport cython | @@ -4,7 +4,9 @@ cimport cython | ||
| 4 | 4 | ||
| 5 | from collections import deque | 5 | from collections import deque |
| 6 | 6 | ||
| 7 | +from cython.parallel import prange | ||
| 7 | from libc.math cimport floor, ceil | 8 | from libc.math cimport floor, ceil |
| 9 | +from libcpp cimport bool | ||
| 8 | from libcpp.deque cimport deque as cdeque | 10 | from libcpp.deque cimport deque as cdeque |
| 9 | from libcpp.vector cimport vector | 11 | from libcpp.vector cimport vector |
| 10 | 12 | ||
| @@ -230,3 +232,43 @@ def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float | @@ -230,3 +232,43 @@ def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float | ||
| 230 | 232 | ||
| 231 | if to_return: | 233 | if to_return: |
| 232 | return out | 234 | return out |
| 235 | + | ||
| 236 | + | ||
| 237 | +@cython.boundscheck(False) | ||
| 238 | +@cython.wraparound(False) | ||
| 239 | +@cython.nonecheck(False) | ||
| 240 | +def fill_holes_automatically(np.ndarray[mask_t, ndim=3] mask, np.ndarray[np.uint16_t, ndim=3] labels, unsigned int nlabels, unsigned int max_size): | ||
| 241 | + """ | ||
| 242 | + Fill mask holes automatically. The hole must <= max_size. Return True if any hole were filled. | ||
| 243 | + """ | ||
| 244 | + cdef np.ndarray[np.uint32_t, ndim=1] sizes = np.zeros(shape=(nlabels + 1), dtype=np.uint32) | ||
| 245 | + cdef int x, y, z | ||
| 246 | + cdef int dx, dy, dz | ||
| 247 | + cdef int i | ||
| 248 | + | ||
| 249 | + cdef bool modified = False | ||
| 250 | + | ||
| 251 | + dz = mask.shape[0] | ||
| 252 | + dy = mask.shape[1] | ||
| 253 | + dx = mask.shape[2] | ||
| 254 | + | ||
| 255 | + for z in xrange(dz): | ||
| 256 | + for y in xrange(dy): | ||
| 257 | + for x in xrange(dx): | ||
| 258 | + sizes[labels[z, y, x]] += 1 | ||
| 259 | + | ||
| 260 | + #Checking if any hole will be filled | ||
| 261 | + for i in xrange(nlabels + 1): | ||
| 262 | + if sizes[i] <= max_size: | ||
| 263 | + modified = True | ||
| 264 | + | ||
| 265 | + if not modified: | ||
| 266 | + return 0 | ||
| 267 | + | ||
| 268 | + for z in prange(dz, nogil=True): | ||
| 269 | + for y in xrange(dy): | ||
| 270 | + for x in xrange(dx): | ||
| 271 | + if sizes[labels[z, y, x]] <= max_size: | ||
| 272 | + mask[z, y, x] = 254 | ||
| 273 | + | ||
| 274 | + return modified |
invesalius/data/mask.py
| @@ -23,15 +23,17 @@ import random | @@ -23,15 +23,17 @@ import random | ||
| 23 | import shutil | 23 | import shutil |
| 24 | import tempfile | 24 | import tempfile |
| 25 | 25 | ||
| 26 | -import numpy | 26 | +import numpy as np |
| 27 | import vtk | 27 | import vtk |
| 28 | 28 | ||
| 29 | import invesalius.constants as const | 29 | import invesalius.constants as const |
| 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 | ||
| 33 | -from wx.lib.pubsub import pub as Publisher | 33 | +from . import floodfill |
| 34 | 34 | ||
| 35 | +from wx.lib.pubsub import pub as Publisher | ||
| 36 | +from scipy import ndimage | ||
| 35 | 37 | ||
| 36 | class EditionHistoryNode(object): | 38 | class EditionHistoryNode(object): |
| 37 | def __init__(self, index, orientation, array, clean=False): | 39 | def __init__(self, index, orientation, array, clean=False): |
| @@ -43,11 +45,11 @@ class EditionHistoryNode(object): | @@ -43,11 +45,11 @@ class EditionHistoryNode(object): | ||
| 43 | self._save_array(array) | 45 | self._save_array(array) |
| 44 | 46 | ||
| 45 | def _save_array(self, array): | 47 | def _save_array(self, array): |
| 46 | - numpy.save(self.filename, array) | 48 | + np.save(self.filename, array) |
| 47 | print "Saving history", self.index, self.orientation, self.filename, self.clean | 49 | print "Saving history", self.index, self.orientation, self.filename, self.clean |
| 48 | 50 | ||
| 49 | def commit_history(self, mvolume): | 51 | def commit_history(self, mvolume): |
| 50 | - array = numpy.load(self.filename) | 52 | + array = np.load(self.filename) |
| 51 | if self.orientation == 'AXIAL': | 53 | if self.orientation == 'AXIAL': |
| 52 | mvolume[self.index+1,1:,1:] = array | 54 | mvolume[self.index+1,1:,1:] = array |
| 53 | if self.clean: | 55 | if self.clean: |
| @@ -295,7 +297,7 @@ class Mask(): | @@ -295,7 +297,7 @@ class Mask(): | ||
| 295 | def _open_mask(self, filename, shape, dtype='uint8'): | 297 | def _open_mask(self, filename, shape, dtype='uint8'): |
| 296 | print ">>", filename, shape | 298 | print ">>", filename, shape |
| 297 | self.temp_file = filename | 299 | self.temp_file = filename |
| 298 | - self.matrix = numpy.memmap(filename, shape=shape, dtype=dtype, mode="r+") | 300 | + self.matrix = np.memmap(filename, shape=shape, dtype=dtype, mode="r+") |
| 299 | 301 | ||
| 300 | def _set_class_index(self, index): | 302 | def _set_class_index(self, index): |
| 301 | Mask.general_index = index | 303 | Mask.general_index = index |
| @@ -309,7 +311,7 @@ class Mask(): | @@ -309,7 +311,7 @@ class Mask(): | ||
| 309 | """ | 311 | """ |
| 310 | self.temp_file = tempfile.mktemp() | 312 | self.temp_file = tempfile.mktemp() |
| 311 | shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 | 313 | shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 |
| 312 | - self.matrix = numpy.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) | 314 | + self.matrix = np.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) |
| 313 | 315 | ||
| 314 | def clean(self): | 316 | def clean(self): |
| 315 | self.matrix[1:, 1:, 1:] = 0 | 317 | self.matrix[1:, 1:, 1:] = 0 |
| @@ -340,6 +342,49 @@ class Mask(): | @@ -340,6 +342,49 @@ class Mask(): | ||
| 340 | def clear_history(self): | 342 | def clear_history(self): |
| 341 | self.history.clear_history() | 343 | self.history.clear_history() |
| 342 | 344 | ||
| 345 | + def fill_holes_auto(self, target, conn, orientation, index, size): | ||
| 346 | + CON2D = {4: 1, 8: 2} | ||
| 347 | + CON3D = {6: 1, 18: 2, 26: 3} | ||
| 348 | + | ||
| 349 | + if target == '3D': | ||
| 350 | + cp_mask = self.matrix.copy() | ||
| 351 | + matrix = self.matrix[1:, 1:, 1:] | ||
| 352 | + bstruct = ndimage.generate_binary_structure(3, CON3D[conn]) | ||
| 353 | + | ||
| 354 | + imask = (~(matrix > 127)) | ||
| 355 | + labels, nlabels = ndimage.label(imask, bstruct, output=np.uint16) | ||
| 356 | + | ||
| 357 | + if nlabels == 0: | ||
| 358 | + return | ||
| 359 | + | ||
| 360 | + ret = floodfill.fill_holes_automatically(matrix, labels, nlabels, size) | ||
| 361 | + if ret: | ||
| 362 | + self.save_history(index, orientation, self.matrix.copy(), cp_mask) | ||
| 363 | + else: | ||
| 364 | + bstruct = ndimage.generate_binary_structure(2, CON2D[conn]) | ||
| 365 | + | ||
| 366 | + if orientation == 'AXIAL': | ||
| 367 | + matrix = self.matrix[index+1, 1:, 1:] | ||
| 368 | + elif orientation == 'CORONAL': | ||
| 369 | + matrix = self.matrix[1:, index+1, 1:] | ||
| 370 | + elif orientation == 'SAGITAL': | ||
| 371 | + matrix = self.matrix[1:, 1:, index+1] | ||
| 372 | + | ||
| 373 | + cp_mask = matrix.copy() | ||
| 374 | + | ||
| 375 | + imask = (~(matrix > 127)) | ||
| 376 | + labels, nlabels = ndimage.label(imask, bstruct, output=np.uint16) | ||
| 377 | + | ||
| 378 | + if nlabels == 0: | ||
| 379 | + return | ||
| 380 | + | ||
| 381 | + labels = labels.reshape(1, labels.shape[0], labels.shape[1]) | ||
| 382 | + matrix = matrix.reshape(1, matrix.shape[0], matrix.shape[1]) | ||
| 383 | + | ||
| 384 | + ret = floodfill.fill_holes_automatically(matrix, labels, nlabels, size) | ||
| 385 | + if ret: | ||
| 386 | + self.save_history(index, orientation, matrix.copy(), cp_mask) | ||
| 387 | + | ||
| 343 | def __del__(self): | 388 | def __del__(self): |
| 344 | if self.is_shown: | 389 | if self.is_shown: |
| 345 | self.history._config_undo_redo(False) | 390 | self.history._config_undo_redo(False) |
invesalius/data/slice_.py
| @@ -194,6 +194,8 @@ class Slice(object): | @@ -194,6 +194,8 @@ class Slice(object): | ||
| 194 | 194 | ||
| 195 | Publisher.subscribe(self.__undo_edition, 'Undo edition') | 195 | Publisher.subscribe(self.__undo_edition, 'Undo edition') |
| 196 | Publisher.subscribe(self.__redo_edition, 'Redo edition') | 196 | Publisher.subscribe(self.__redo_edition, 'Redo edition') |
| 197 | + | ||
| 198 | + Publisher.subscribe(self._fill_holes_auto, 'Fill holes automatically') | ||
| 197 | 199 | ||
| 198 | def GetMaxSliceNumber(self, orientation): | 200 | def GetMaxSliceNumber(self, orientation): |
| 199 | shape = self.matrix.shape | 201 | shape = self.matrix.shape |
| @@ -1482,3 +1484,28 @@ class Slice(object): | @@ -1482,3 +1484,28 @@ class Slice(object): | ||
| 1482 | #filename, filetype = pubsub_evt.data | 1484 | #filename, filetype = pubsub_evt.data |
| 1483 | #if (filetype == const.FILETYPE_IMAGEDATA): | 1485 | #if (filetype == const.FILETYPE_IMAGEDATA): |
| 1484 | #iu.Export(imagedata, filename) | 1486 | #iu.Export(imagedata, filename) |
| 1487 | + | ||
| 1488 | + def _fill_holes_auto(self, pubsub_evt): | ||
| 1489 | + data = pubsub_evt.data | ||
| 1490 | + target = data['target'] | ||
| 1491 | + conn = data['conn'] | ||
| 1492 | + orientation = data['orientation'] | ||
| 1493 | + size = data['size'] | ||
| 1494 | + | ||
| 1495 | + if target == '2D': | ||
| 1496 | + index = self.buffer_slices[orientation].index | ||
| 1497 | + else: | ||
| 1498 | + index = 0 | ||
| 1499 | + self.do_threshold_to_all_slices() | ||
| 1500 | + | ||
| 1501 | + self.current_mask.fill_holes_auto(target, conn, orientation, index, size) | ||
| 1502 | + | ||
| 1503 | + self.buffer_slices['AXIAL'].discard_mask() | ||
| 1504 | + self.buffer_slices['CORONAL'].discard_mask() | ||
| 1505 | + self.buffer_slices['SAGITAL'].discard_mask() | ||
| 1506 | + | ||
| 1507 | + self.buffer_slices['AXIAL'].discard_vtk_mask() | ||
| 1508 | + self.buffer_slices['CORONAL'].discard_vtk_mask() | ||
| 1509 | + self.buffer_slices['SAGITAL'].discard_vtk_mask() | ||
| 1510 | + | ||
| 1511 | + Publisher.sendMessage('Reload actual slice') |
invesalius/gui/dialogs.py
| @@ -1863,11 +1863,11 @@ class PanelTargeFFill(wx.Panel): | @@ -1863,11 +1863,11 @@ class PanelTargeFFill(wx.Panel): | ||
| 1863 | self.Layout() | 1863 | self.Layout() |
| 1864 | 1864 | ||
| 1865 | class Panel2DConnectivity(wx.Panel): | 1865 | class Panel2DConnectivity(wx.Panel): |
| 1866 | - def __init__(self, parent, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER): | 1866 | + def __init__(self, parent, ID=-1, show_orientation=False, style=wx.TAB_TRAVERSAL|wx.NO_BORDER): |
| 1867 | wx.Panel.__init__(self, parent, ID, style=style) | 1867 | wx.Panel.__init__(self, parent, ID, style=style) |
| 1868 | - self._init_gui() | 1868 | + self._init_gui(show_orientation) |
| 1869 | 1869 | ||
| 1870 | - def _init_gui(self): | 1870 | + def _init_gui(self, show_orientation): |
| 1871 | self.conect2D_4 = wx.RadioButton(self, -1, "4", style=wx.RB_GROUP) | 1871 | self.conect2D_4 = wx.RadioButton(self, -1, "4", style=wx.RB_GROUP) |
| 1872 | self.conect2D_8 = wx.RadioButton(self, -1, "8") | 1872 | self.conect2D_8 = wx.RadioButton(self, -1, "8") |
| 1873 | 1873 | ||
| @@ -1879,10 +1879,33 @@ class Panel2DConnectivity(wx.Panel): | @@ -1879,10 +1879,33 @@ class Panel2DConnectivity(wx.Panel): | ||
| 1879 | sizer.Add(self.conect2D_8, (2, 1), flag=wx.LEFT, border=7) | 1879 | sizer.Add(self.conect2D_8, (2, 1), flag=wx.LEFT, border=7) |
| 1880 | sizer.AddStretchSpacer((3, 0)) | 1880 | sizer.AddStretchSpacer((3, 0)) |
| 1881 | 1881 | ||
| 1882 | + if show_orientation: | ||
| 1883 | + self.cmb_orientation = wx.ComboBox(self, -1, choices=(_(u"Axial"), _(u"Coronal"), _(u"Sagital")), style=wx.CB_READONLY) | ||
| 1884 | + self.cmb_orientation.SetSelection(0) | ||
| 1885 | + | ||
| 1886 | + sizer.Add(wx.StaticText(self, -1, _(u"Orientation")), (4, 0), (1, 6), flag=wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=5) | ||
| 1887 | + sizer.Add(self.cmb_orientation, (5, 0), (1, 10), flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) | ||
| 1888 | + sizer.AddStretchSpacer((6, 0)) | ||
| 1889 | + | ||
| 1882 | self.SetSizer(sizer) | 1890 | self.SetSizer(sizer) |
| 1883 | sizer.Fit(self) | 1891 | sizer.Fit(self) |
| 1884 | self.Layout() | 1892 | self.Layout() |
| 1885 | 1893 | ||
| 1894 | + def GetConnSelected(self): | ||
| 1895 | + if self.conect2D_4.GetValue(): | ||
| 1896 | + return 4 | ||
| 1897 | + else: | ||
| 1898 | + return 8 | ||
| 1899 | + | ||
| 1900 | + def GetOrientation(self): | ||
| 1901 | + dic_ori = { | ||
| 1902 | + _(u"Axial"): 'AXIAL', | ||
| 1903 | + _(u"Coronal"): 'CORONAL', | ||
| 1904 | + _(u"Sagital"): 'SAGITAL' | ||
| 1905 | + } | ||
| 1906 | + | ||
| 1907 | + return dic_ori[self.cmb_orientation.GetStringSelection()] | ||
| 1908 | + | ||
| 1886 | 1909 | ||
| 1887 | class Panel3DConnectivity(wx.Panel): | 1910 | class Panel3DConnectivity(wx.Panel): |
| 1888 | def __init__(self, parent, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER): | 1911 | def __init__(self, parent, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER): |
| @@ -1907,6 +1930,14 @@ class Panel3DConnectivity(wx.Panel): | @@ -1907,6 +1930,14 @@ class Panel3DConnectivity(wx.Panel): | ||
| 1907 | sizer.Fit(self) | 1930 | sizer.Fit(self) |
| 1908 | self.Layout() | 1931 | self.Layout() |
| 1909 | 1932 | ||
| 1933 | + def GetConnSelected(self): | ||
| 1934 | + if self.conect3D_6.GetValue(): | ||
| 1935 | + return 6 | ||
| 1936 | + elif self.conect3D_18.GetValue(): | ||
| 1937 | + return 18 | ||
| 1938 | + else: | ||
| 1939 | + return 26 | ||
| 1940 | + | ||
| 1910 | 1941 | ||
| 1911 | class PanelFFillThreshold(wx.Panel): | 1942 | class PanelFFillThreshold(wx.Panel): |
| 1912 | def __init__(self, parent, config, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER): | 1943 | def __init__(self, parent, config, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER): |
| @@ -2473,3 +2504,101 @@ class CropOptionsDialog(wx.Dialog): | @@ -2473,3 +2504,101 @@ class CropOptionsDialog(wx.Dialog): | ||
| 2473 | Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK) | 2504 | Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK) |
| 2474 | evt.Skip() | 2505 | evt.Skip() |
| 2475 | self.Destroy() | 2506 | self.Destroy() |
| 2507 | + | ||
| 2508 | + | ||
| 2509 | +class FillHolesAutoDialog(wx.Dialog): | ||
| 2510 | + def __init__(self, title): | ||
| 2511 | + pre = wx.PreDialog() | ||
| 2512 | + pre.Create(wx.GetApp().GetTopWindow(), -1, title, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) | ||
| 2513 | + self.PostCreate(pre) | ||
| 2514 | + | ||
| 2515 | + self._init_gui() | ||
| 2516 | + | ||
| 2517 | + def _init_gui(self): | ||
| 2518 | + if sys.platform == "win32": | ||
| 2519 | + border_style = wx.SIMPLE_BORDER | ||
| 2520 | + else: | ||
| 2521 | + border_style = wx.SUNKEN_BORDER | ||
| 2522 | + | ||
| 2523 | + self.spin_size = wx.SpinCtrl(self, -1, value='1000', min=1, max=1000000000) | ||
| 2524 | + self.panel_target = PanelTargeFFill(self, style=border_style|wx.TAB_TRAVERSAL) | ||
| 2525 | + self.panel2dcon = Panel2DConnectivity(self, show_orientation=True, style=border_style|wx.TAB_TRAVERSAL) | ||
| 2526 | + self.panel3dcon = Panel3DConnectivity(self, style=border_style|wx.TAB_TRAVERSAL) | ||
| 2527 | + | ||
| 2528 | + self.panel_target.target_2d.SetValue(1) | ||
| 2529 | + self.panel2dcon.Enable(1) | ||
| 2530 | + self.panel3dcon.Enable(0) | ||
| 2531 | + | ||
| 2532 | + self.apply_btn = wx.Button(self, wx.ID_APPLY) | ||
| 2533 | + self.close_btn = wx.Button(self, wx.ID_CLOSE) | ||
| 2534 | + | ||
| 2535 | + # Sizer | ||
| 2536 | + sizer = wx.BoxSizer(wx.VERTICAL) | ||
| 2537 | + | ||
| 2538 | + sizer.AddSpacer(5) | ||
| 2539 | + sizer.Add(wx.StaticText(self, -1, _(u"Parameters")), flag=wx.LEFT, border=5) | ||
| 2540 | + sizer.AddSpacer(5) | ||
| 2541 | + | ||
| 2542 | + sizer.Add(self.panel_target, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) | ||
| 2543 | + sizer.AddSpacer(5) | ||
| 2544 | + sizer.Add(self.panel2dcon, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) | ||
| 2545 | + sizer.AddSpacer(5) | ||
| 2546 | + sizer.Add(self.panel3dcon, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) | ||
| 2547 | + sizer.AddSpacer(5) | ||
| 2548 | + | ||
| 2549 | + spin_sizer = wx.BoxSizer(wx.HORIZONTAL) | ||
| 2550 | + spin_sizer.Add(wx.StaticText(self, -1, _(u"Max hole size")), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=5) | ||
| 2551 | + spin_sizer.Add(self.spin_size, 0, flag=wx.LEFT|wx.RIGHT, border=5) | ||
| 2552 | + spin_sizer.Add(wx.StaticText(self, -1, _(u"voxels")), flag=wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=5) | ||
| 2553 | + | ||
| 2554 | + sizer.Add(spin_sizer, 0, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) | ||
| 2555 | + sizer.AddSpacer(5) | ||
| 2556 | + | ||
| 2557 | + btn_sizer = wx.BoxSizer(wx.HORIZONTAL) | ||
| 2558 | + btn_sizer.Add(self.apply_btn, 0, flag=wx.ALIGN_RIGHT, border=5) | ||
| 2559 | + btn_sizer.Add(self.close_btn, 0, flag=wx.LEFT|wx.ALIGN_RIGHT, border=5) | ||
| 2560 | + | ||
| 2561 | + sizer.AddSizer(btn_sizer, 0, flag=wx.ALIGN_RIGHT|wx.LEFT|wx.RIGHT, border=5) | ||
| 2562 | + | ||
| 2563 | + sizer.AddSpacer(5) | ||
| 2564 | + | ||
| 2565 | + self.SetSizer(sizer) | ||
| 2566 | + sizer.Fit(self) | ||
| 2567 | + self.Layout() | ||
| 2568 | + | ||
| 2569 | + self.apply_btn.Bind(wx.EVT_BUTTON, self.OnApply) | ||
| 2570 | + self.close_btn.Bind(wx.EVT_BUTTON, self.OnBtnClose) | ||
| 2571 | + self.Bind(wx.EVT_RADIOBUTTON, self.OnSetRadio) | ||
| 2572 | + | ||
| 2573 | + def OnApply(self, evt): | ||
| 2574 | + if self.panel_target.target_2d.GetValue(): | ||
| 2575 | + target = "2D" | ||
| 2576 | + conn = self.panel2dcon.GetConnSelected() | ||
| 2577 | + orientation = self.panel2dcon.GetOrientation() | ||
| 2578 | + else: | ||
| 2579 | + target = "3D" | ||
| 2580 | + conn = self.panel3dcon.GetConnSelected() | ||
| 2581 | + orientation = 'VOLUME' | ||
| 2582 | + | ||
| 2583 | + data = { | ||
| 2584 | + 'target': target, | ||
| 2585 | + 'conn': conn, | ||
| 2586 | + 'orientation': orientation, | ||
| 2587 | + 'size': self.spin_size.GetValue(), | ||
| 2588 | + } | ||
| 2589 | + | ||
| 2590 | + Publisher.sendMessage("Fill holes automatically", data) | ||
| 2591 | + | ||
| 2592 | + | ||
| 2593 | + def OnBtnClose(self, evt): | ||
| 2594 | + self.Close() | ||
| 2595 | + self.Destroy() | ||
| 2596 | + | ||
| 2597 | + def OnSetRadio(self, evt): | ||
| 2598 | + # Target | ||
| 2599 | + if self.panel_target.target_2d.GetValue(): | ||
| 2600 | + self.panel2dcon.Enable(1) | ||
| 2601 | + self.panel3dcon.Enable(0) | ||
| 2602 | + else: | ||
| 2603 | + self.panel3dcon.Enable(1) | ||
| 2604 | + self.panel2dcon.Enable(0) |
invesalius/gui/frame.py
| @@ -445,6 +445,9 @@ class Frame(wx.Frame): | @@ -445,6 +445,9 @@ class Frame(wx.Frame): | ||
| 445 | elif id == const.ID_FLOODFILL_MASK: | 445 | elif id == const.ID_FLOODFILL_MASK: |
| 446 | self.OnFillHolesManually() | 446 | self.OnFillHolesManually() |
| 447 | 447 | ||
| 448 | + elif id == const.ID_FILL_HOLE_AUTO: | ||
| 449 | + self.OnFillHolesAutomatically() | ||
| 450 | + | ||
| 448 | elif id == const.ID_REMOVE_MASK_PART: | 451 | elif id == const.ID_REMOVE_MASK_PART: |
| 449 | self.OnRemoveMaskParts() | 452 | self.OnRemoveMaskParts() |
| 450 | 453 | ||
| @@ -592,6 +595,10 @@ class Frame(wx.Frame): | @@ -592,6 +595,10 @@ class Frame(wx.Frame): | ||
| 592 | def OnFillHolesManually(self): | 595 | def OnFillHolesManually(self): |
| 593 | Publisher.sendMessage('Enable style', const.SLICE_STATE_MASK_FFILL) | 596 | Publisher.sendMessage('Enable style', const.SLICE_STATE_MASK_FFILL) |
| 594 | 597 | ||
| 598 | + def OnFillHolesAutomatically(self): | ||
| 599 | + fdlg = dlg.FillHolesAutoDialog(_(u"Fill holes automatically")) | ||
| 600 | + fdlg.Show() | ||
| 601 | + | ||
| 595 | def OnRemoveMaskParts(self): | 602 | def OnRemoveMaskParts(self): |
| 596 | Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS) | 603 | Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS) |
| 597 | 604 | ||
| @@ -628,6 +635,7 @@ class MenuBar(wx.MenuBar): | @@ -628,6 +635,7 @@ class MenuBar(wx.MenuBar): | ||
| 628 | const.ID_PROJECT_CLOSE, | 635 | const.ID_PROJECT_CLOSE, |
| 629 | const.ID_REORIENT_IMG, | 636 | const.ID_REORIENT_IMG, |
| 630 | const.ID_FLOODFILL_MASK, | 637 | const.ID_FLOODFILL_MASK, |
| 638 | + const.ID_FILL_HOLE_AUTO, | ||
| 631 | const.ID_REMOVE_MASK_PART, | 639 | const.ID_REMOVE_MASK_PART, |
| 632 | const.ID_SELECT_MASK_PART, | 640 | const.ID_SELECT_MASK_PART, |
| 633 | const.ID_FLOODFILL_SEGMENTATION,] | 641 | const.ID_FLOODFILL_SEGMENTATION,] |
| @@ -743,17 +751,23 @@ class MenuBar(wx.MenuBar): | @@ -743,17 +751,23 @@ class MenuBar(wx.MenuBar): | ||
| 743 | self.clean_mask_menu.Enable(False) | 751 | self.clean_mask_menu.Enable(False) |
| 744 | 752 | ||
| 745 | mask_menu.AppendSeparator() | 753 | mask_menu.AppendSeparator() |
| 754 | + | ||
| 746 | self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill holes manually")) | 755 | self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill holes manually")) |
| 747 | self.fill_hole_mask_menu.Enable(False) | 756 | self.fill_hole_mask_menu.Enable(False) |
| 748 | 757 | ||
| 758 | + self.fill_hole_auto_menu = mask_menu.Append(const.ID_FILL_HOLE_AUTO, _(u"Fill holes automatically")) | ||
| 759 | + self.fill_hole_mask_menu.Enable(False) | ||
| 760 | + | ||
| 761 | + mask_menu.AppendSeparator() | ||
| 762 | + | ||
| 749 | self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts")) | 763 | self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts")) |
| 750 | self.remove_mask_part_menu.Enable(False) | 764 | self.remove_mask_part_menu.Enable(False) |
| 751 | 765 | ||
| 752 | self.select_mask_part_menu = mask_menu.Append(const.ID_SELECT_MASK_PART, _(u"Select parts")) | 766 | self.select_mask_part_menu = mask_menu.Append(const.ID_SELECT_MASK_PART, _(u"Select parts")) |
| 753 | self.select_mask_part_menu.Enable(False) | 767 | self.select_mask_part_menu.Enable(False) |
| 754 | - | 768 | + |
| 755 | mask_menu.AppendSeparator() | 769 | mask_menu.AppendSeparator() |
| 756 | - | 770 | + |
| 757 | self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop")) | 771 | self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop")) |
| 758 | self.crop_mask_menu.Enable(False) | 772 | self.crop_mask_menu.Enable(False) |
| 759 | 773 |