Commit 03ed982ba7b0be007742bed4244fe4af378ee89c
1 parent
87c3a46d
Exists in
fill_holes_auto
filling hole without gui
Showing
6 changed files
with
179 additions
and
9 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,6 +4,7 @@ cimport cython | @@ -4,6 +4,7 @@ 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 |
8 | from libcpp.deque cimport deque as cdeque | 9 | from libcpp.deque cimport deque as cdeque |
9 | from libcpp.vector cimport vector | 10 | from libcpp.vector cimport vector |
@@ -230,3 +231,28 @@ def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float | @@ -230,3 +231,28 @@ def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float | ||
230 | 231 | ||
231 | if to_return: | 232 | if to_return: |
232 | return out | 233 | return out |
234 | + | ||
235 | + | ||
236 | +# @cython.boundscheck(False) | ||
237 | +# @cython.wraparound(False) | ||
238 | +# @cython.nonecheck(False) | ||
239 | +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): | ||
240 | + cdef np.ndarray[np.uint32_t, ndim=1] sizes = np.zeros(shape=(nlabels + 1), dtype=np.uint32) | ||
241 | + cdef int x, y, z | ||
242 | + cdef int dx, dy, dz | ||
243 | + | ||
244 | + dz = mask.shape[0] | ||
245 | + dy = mask.shape[1] | ||
246 | + dx = mask.shape[2] | ||
247 | + | ||
248 | + for z in xrange(dz): | ||
249 | + for y in xrange(dy): | ||
250 | + for x in xrange(dx): | ||
251 | + sizes[labels[z, y, x]] += 1 | ||
252 | + | ||
253 | + | ||
254 | + for z in prange(dz, nogil=True): | ||
255 | + for y in xrange(dy): | ||
256 | + for x in xrange(dx): | ||
257 | + if sizes[labels[z, y, x]] <= max_size: | ||
258 | + mask[z, y, x] = 254 |
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,24 @@ class Mask(): | @@ -340,6 +342,24 @@ 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, idx): | ||
346 | + matrix = self.matrix[idx+1, 1:, 1:] | ||
347 | + matrix = matrix.reshape(1, matrix.shape[0], matrix.shape[1]) | ||
348 | + imask = (~(matrix > 127)) | ||
349 | + labels, nlabels = ndimage.label(imask, output=np.uint16) | ||
350 | + | ||
351 | + floodfill.fill_holes_automatically(matrix, labels, nlabels, 100000) | ||
352 | + | ||
353 | + # for l in xrange(nlabels): | ||
354 | + # trues = (labels == l) | ||
355 | + # size = trues.sum() | ||
356 | + # print l, size | ||
357 | + | ||
358 | + # if size <= 1000: | ||
359 | + # matrix[trues] = 254 | ||
360 | + # self.was_edited = True | ||
361 | + | ||
362 | + | ||
343 | def __del__(self): | 363 | def __del__(self): |
344 | if self.is_shown: | 364 | if self.is_shown: |
345 | self.history._config_undo_redo(False) | 365 | 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,17 @@ class Slice(object): | @@ -1482,3 +1484,17 @@ 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 | + self.do_threshold_to_all_slices() | ||
1490 | + self.current_mask.fill_holes_auto(self.buffer_slices['AXIAL'].index) | ||
1491 | + | ||
1492 | + self.buffer_slices['AXIAL'].discard_mask() | ||
1493 | + self.buffer_slices['CORONAL'].discard_mask() | ||
1494 | + self.buffer_slices['SAGITAL'].discard_mask() | ||
1495 | + | ||
1496 | + self.buffer_slices['AXIAL'].discard_vtk_mask() | ||
1497 | + self.buffer_slices['CORONAL'].discard_vtk_mask() | ||
1498 | + self.buffer_slices['SAGITAL'].discard_vtk_mask() | ||
1499 | + | ||
1500 | + 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,6 +1879,14 @@ class Panel2DConnectivity(wx.Panel): | @@ -1879,6 +1879,14 @@ 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() |
@@ -2473,3 +2481,90 @@ class CropOptionsDialog(wx.Dialog): | @@ -2473,3 +2481,90 @@ class CropOptionsDialog(wx.Dialog): | ||
2473 | Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK) | 2481 | Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK) |
2474 | evt.Skip() | 2482 | evt.Skip() |
2475 | self.Destroy() | 2483 | self.Destroy() |
2484 | + | ||
2485 | + | ||
2486 | +class FillHolesAutoDialog(wx.Dialog): | ||
2487 | + def __init__(self, title, config): | ||
2488 | + pre = wx.PreDialog() | ||
2489 | + pre.Create(wx.GetApp().GetTopWindow(), -1, title, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) | ||
2490 | + self.PostCreate(pre) | ||
2491 | + | ||
2492 | + self._init_gui() | ||
2493 | + | ||
2494 | + def _init_gui(self): | ||
2495 | + if sys.platform == "win32": | ||
2496 | + border_style = wx.SIMPLE_BORDER | ||
2497 | + else: | ||
2498 | + border_style = wx.SUNKEN_BORDER | ||
2499 | + | ||
2500 | + self.panel_target = PanelTargeFFill(self, style=border_style|wx.TAB_TRAVERSAL) | ||
2501 | + self.panel2dcon = Panel2DConnectivity(self, show_orientation=True, style=border_style|wx.TAB_TRAVERSAL) | ||
2502 | + self.panel3dcon = Panel3DConnectivity(self, style=border_style|wx.TAB_TRAVERSAL) | ||
2503 | + | ||
2504 | + self.panel_target.target_2d.SetValue(1) | ||
2505 | + self.panel2dcon.Enable(1) | ||
2506 | + self.panel3dcon.Enable(0) | ||
2507 | + | ||
2508 | + self.panel2dcon.conect2D_8.SetValue(1) | ||
2509 | + | ||
2510 | + self.apply_btn = wx.Button(self, wx.ID_APPLY) | ||
2511 | + self.close_btn = wx.Button(self, wx.ID_CLOSE) | ||
2512 | + | ||
2513 | + # Sizer | ||
2514 | + sizer = wx.BoxSizer(wx.VERTICAL) | ||
2515 | + | ||
2516 | + sizer.AddSpacer(5) | ||
2517 | + sizer.Add(wx.StaticText(self, -1, _(u"Parameters")), flag=wx.LEFT, border=5) | ||
2518 | + sizer.AddSpacer(5) | ||
2519 | + sizer.Add(self.panel_target, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) | ||
2520 | + sizer.AddSpacer(5) | ||
2521 | + sizer.Add(self.panel2dcon, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) | ||
2522 | + sizer.AddSpacer(5) | ||
2523 | + sizer.Add(self.panel3dcon, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) | ||
2524 | + sizer.AddSpacer(5) | ||
2525 | + sizer.Add(self.apply_btn, 0, flag=wx.ALIGN_RIGHT|wx.RIGHT, border=7) | ||
2526 | + sizer.Add(self.close_btn, 0, flag=wx.ALIGN_RIGHT|wx.RIGHT, border=7) | ||
2527 | + sizer.AddSpacer(5) | ||
2528 | + | ||
2529 | + self.SetSizer(sizer) | ||
2530 | + sizer.Fit(self) | ||
2531 | + self.Layout() | ||
2532 | + | ||
2533 | + self.close_btn.Bind(wx.EVT_BUTTON, self.OnBtnClose) | ||
2534 | + self.Bind(wx.EVT_RADIOBUTTON, self.OnSetRadio) | ||
2535 | + self.Bind(wx.EVT_CLOSE, self.OnClose) | ||
2536 | + | ||
2537 | + def OnBtnClose(self, evt): | ||
2538 | + self.Close() | ||
2539 | + | ||
2540 | + def OnSetRadio(self, evt): | ||
2541 | + # Target | ||
2542 | + if self.panel_target.target_2d.GetValue(): | ||
2543 | + self.config.target = "2D" | ||
2544 | + self.panel2dcon.Enable(1) | ||
2545 | + self.panel3dcon.Enable(0) | ||
2546 | + else: | ||
2547 | + self.config.target = "3D" | ||
2548 | + self.panel3dcon.Enable(1) | ||
2549 | + self.panel2dcon.Enable(0) | ||
2550 | + | ||
2551 | + # 2D | ||
2552 | + if self.panel2dcon.conect2D_4.GetValue(): | ||
2553 | + self.config.con_2d = 4 | ||
2554 | + elif self.panel2dcon.conect2D_8.GetValue(): | ||
2555 | + self.config.con_2d = 8 | ||
2556 | + | ||
2557 | + # 3D | ||
2558 | + if self.panel3dcon.conect3D_6.GetValue(): | ||
2559 | + self.config.con_3d = 6 | ||
2560 | + elif self.panel3dcon.conect3D_18.GetValue(): | ||
2561 | + self.config.con_3d = 18 | ||
2562 | + elif self.panel3dcon.conect3D_26.GetValue(): | ||
2563 | + self.config.con_3d = 26 | ||
2564 | + | ||
2565 | + def OnClose(self, evt): | ||
2566 | + print "ONCLOSE" | ||
2567 | + if self.config.dlg_visible: | ||
2568 | + Publisher.sendMessage('Disable style', const.SLICE_STATE_MASK_FFILL) | ||
2569 | + evt.Skip() | ||
2570 | + self.Destroy() |
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,11 @@ class Frame(wx.Frame): | @@ -592,6 +595,11 @@ 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 | + # Publisher.sendMessage('Fill holes automatically') | ||
600 | + fdlg = dlg.FillHolesAutoDialog() | ||
601 | + fdlg.Show() | ||
602 | + | ||
595 | def OnRemoveMaskParts(self): | 603 | def OnRemoveMaskParts(self): |
596 | Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS) | 604 | Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS) |
597 | 605 | ||
@@ -628,6 +636,7 @@ class MenuBar(wx.MenuBar): | @@ -628,6 +636,7 @@ class MenuBar(wx.MenuBar): | ||
628 | const.ID_PROJECT_CLOSE, | 636 | const.ID_PROJECT_CLOSE, |
629 | const.ID_REORIENT_IMG, | 637 | const.ID_REORIENT_IMG, |
630 | const.ID_FLOODFILL_MASK, | 638 | const.ID_FLOODFILL_MASK, |
639 | + const.ID_FILL_HOLE_AUTO, | ||
631 | const.ID_REMOVE_MASK_PART, | 640 | const.ID_REMOVE_MASK_PART, |
632 | const.ID_SELECT_MASK_PART, | 641 | const.ID_SELECT_MASK_PART, |
633 | const.ID_FLOODFILL_SEGMENTATION,] | 642 | const.ID_FLOODFILL_SEGMENTATION,] |
@@ -746,6 +755,9 @@ class MenuBar(wx.MenuBar): | @@ -746,6 +755,9 @@ class MenuBar(wx.MenuBar): | ||
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 | + | ||
749 | self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts")) | 761 | self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts")) |
750 | self.remove_mask_part_menu.Enable(False) | 762 | self.remove_mask_part_menu.Enable(False) |
751 | 763 |