Commit 388d09bdbdcb80deed072310532a9b4a57fba34e

Authored by Thiago Franco de Moraes
Committed by GitHub
1 parent d7439bcd

Fill holes automatically (#56)

Close holes 2D or 3D automatically with size <= max_size
Ctrl-z and Ctrl-y is working
invesalius/constants.py
... ... @@ -517,6 +517,7 @@ ID_CLEAN_MASK = wx.NewId()
517 517  
518 518 ID_REORIENT_IMG = wx.NewId()
519 519 ID_FLOODFILL_MASK = wx.NewId()
  520 +ID_FILL_HOLE_AUTO = wx.NewId()
520 521 ID_REMOVE_MASK_PART = wx.NewId()
521 522 ID_SELECT_MASK_PART = wx.NewId()
522 523 ID_FLOODFILL_SEGMENTATION = wx.NewId()
... ...
invesalius/data/floodfill.pyx
... ... @@ -4,7 +4,9 @@ cimport cython
4 4  
5 5 from collections import deque
6 6  
  7 +from cython.parallel import prange
7 8 from libc.math cimport floor, ceil
  9 +from libcpp cimport bool
8 10 from libcpp.deque cimport deque as cdeque
9 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 232  
231 233 if to_return:
232 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 23 import shutil
24 24 import tempfile
25 25  
26   -import numpy
  26 +import numpy as np
27 27 import vtk
28 28  
29 29 import invesalius.constants as const
30 30 import invesalius.data.imagedata_utils as iu
31 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 38 class EditionHistoryNode(object):
37 39 def __init__(self, index, orientation, array, clean=False):
... ... @@ -43,11 +45,11 @@ class EditionHistoryNode(object):
43 45 self._save_array(array)
44 46  
45 47 def _save_array(self, array):
46   - numpy.save(self.filename, array)
  48 + np.save(self.filename, array)
47 49 print "Saving history", self.index, self.orientation, self.filename, self.clean
48 50  
49 51 def commit_history(self, mvolume):
50   - array = numpy.load(self.filename)
  52 + array = np.load(self.filename)
51 53 if self.orientation == 'AXIAL':
52 54 mvolume[self.index+1,1:,1:] = array
53 55 if self.clean:
... ... @@ -295,7 +297,7 @@ class Mask():
295 297 def _open_mask(self, filename, shape, dtype='uint8'):
296 298 print ">>", filename, shape
297 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 302 def _set_class_index(self, index):
301 303 Mask.general_index = index
... ... @@ -309,7 +311,7 @@ class Mask():
309 311 """
310 312 self.temp_file = tempfile.mktemp()
311 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 316 def clean(self):
315 317 self.matrix[1:, 1:, 1:] = 0
... ... @@ -340,6 +342,49 @@ class Mask():
340 342 def clear_history(self):
341 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 388 def __del__(self):
344 389 if self.is_shown:
345 390 self.history._config_undo_redo(False)
... ...
invesalius/data/slice_.py
... ... @@ -194,6 +194,8 @@ class Slice(object):
194 194  
195 195 Publisher.subscribe(self.__undo_edition, 'Undo edition')
196 196 Publisher.subscribe(self.__redo_edition, 'Redo edition')
  197 +
  198 + Publisher.subscribe(self._fill_holes_auto, 'Fill holes automatically')
197 199  
198 200 def GetMaxSliceNumber(self, orientation):
199 201 shape = self.matrix.shape
... ... @@ -1482,3 +1484,28 @@ class Slice(object):
1482 1484 #filename, filetype = pubsub_evt.data
1483 1485 #if (filetype == const.FILETYPE_IMAGEDATA):
1484 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 1863 self.Layout()
1864 1864  
1865 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 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 1871 self.conect2D_4 = wx.RadioButton(self, -1, "4", style=wx.RB_GROUP)
1872 1872 self.conect2D_8 = wx.RadioButton(self, -1, "8")
1873 1873  
... ... @@ -1879,10 +1879,33 @@ class Panel2DConnectivity(wx.Panel):
1879 1879 sizer.Add(self.conect2D_8, (2, 1), flag=wx.LEFT, border=7)
1880 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 1890 self.SetSizer(sizer)
1883 1891 sizer.Fit(self)
1884 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 1910 class Panel3DConnectivity(wx.Panel):
1888 1911 def __init__(self, parent, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER):
... ... @@ -1907,6 +1930,14 @@ class Panel3DConnectivity(wx.Panel):
1907 1930 sizer.Fit(self)
1908 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 1942 class PanelFFillThreshold(wx.Panel):
1912 1943 def __init__(self, parent, config, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER):
... ... @@ -2473,3 +2504,101 @@ class CropOptionsDialog(wx.Dialog):
2473 2504 Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK)
2474 2505 evt.Skip()
2475 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 445 elif id == const.ID_FLOODFILL_MASK:
446 446 self.OnFillHolesManually()
447 447  
  448 + elif id == const.ID_FILL_HOLE_AUTO:
  449 + self.OnFillHolesAutomatically()
  450 +
448 451 elif id == const.ID_REMOVE_MASK_PART:
449 452 self.OnRemoveMaskParts()
450 453  
... ... @@ -592,6 +595,10 @@ class Frame(wx.Frame):
592 595 def OnFillHolesManually(self):
593 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 602 def OnRemoveMaskParts(self):
596 603 Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS)
597 604  
... ... @@ -628,6 +635,7 @@ class MenuBar(wx.MenuBar):
628 635 const.ID_PROJECT_CLOSE,
629 636 const.ID_REORIENT_IMG,
630 637 const.ID_FLOODFILL_MASK,
  638 + const.ID_FILL_HOLE_AUTO,
631 639 const.ID_REMOVE_MASK_PART,
632 640 const.ID_SELECT_MASK_PART,
633 641 const.ID_FLOODFILL_SEGMENTATION,]
... ... @@ -743,17 +751,23 @@ class MenuBar(wx.MenuBar):
743 751 self.clean_mask_menu.Enable(False)
744 752  
745 753 mask_menu.AppendSeparator()
  754 +
746 755 self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill holes manually"))
747 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 763 self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts"))
750 764 self.remove_mask_part_menu.Enable(False)
751 765  
752 766 self.select_mask_part_menu = mask_menu.Append(const.ID_SELECT_MASK_PART, _(u"Select parts"))
753 767 self.select_mask_part_menu.Enable(False)
754   -
  768 +
755 769 mask_menu.AppendSeparator()
756   -
  770 +
757 771 self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop"))
758 772 self.crop_mask_menu.Enable(False)
759 773  
... ...