Commit ba729de68a727015d028099a1946248d6abdef66

Authored by Thiago Franco de Moraes
1 parent f0faf9e1
Exists in ff_mask

Fill holes in mask

invesalius/constants.py
... ... @@ -481,6 +481,7 @@ ID_BOOLEAN_MASK = wx.NewId()
481 481 ID_CLEAN_MASK = wx.NewId()
482 482  
483 483 ID_REORIENT_IMG = wx.NewId()
  484 +ID_FLOODFILL_MASK = wx.NewId()
484 485  
485 486 #---------------------------------------------------------
486 487 STATE_DEFAULT = 1000
... ... @@ -498,6 +499,7 @@ SLICE_STATE_SCROLL = 3007
498 499 SLICE_STATE_EDITOR = 3008
499 500 SLICE_STATE_WATERSHED = 3009
500 501 SLICE_STATE_REORIENT = 3010
  502 +SLICE_STATE_MASK_FFILL = 3011
501 503  
502 504 VOLUME_STATE_SEED = 2001
503 505 # STATE_LINEAR_MEASURE = 3001
... ... @@ -515,6 +517,7 @@ SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES
515 517 SLICE_STYLES.append(STATE_DEFAULT)
516 518 SLICE_STYLES.append(SLICE_STATE_EDITOR)
517 519 SLICE_STYLES.append(SLICE_STATE_WATERSHED)
  520 +SLICE_STYLES.append(SLICE_STATE_MASK_FFILL)
518 521  
519 522 VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE,
520 523 STATE_MEASURE_ANGLE]
... ... @@ -523,6 +526,7 @@ VOLUME_STYLES.append(STATE_DEFAULT)
523 526  
524 527 STYLE_LEVEL = {SLICE_STATE_EDITOR: 1,
525 528 SLICE_STATE_WATERSHED: 1,
  529 + SLICE_STATE_MASK_FFILL: 1,
526 530 SLICE_STATE_CROSS: 2,
527 531 SLICE_STATE_SCROLL: 2,
528 532 SLICE_STATE_REORIENT: 2,
... ...
invesalius/data/floodfill.pyx 0 → 100644
... ... @@ -0,0 +1,214 @@
  1 +import numpy as np
  2 +cimport numpy as np
  3 +cimport cython
  4 +
  5 +from collections import deque
  6 +
  7 +from libc.math cimport floor, ceil
  8 +from libcpp.deque cimport deque as cdeque
  9 +
  10 +from cy_my_types cimport image_t, mask_t
  11 +
  12 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  13 +@cython.wraparound(False)
  14 +@cython.nonecheck(False)
  15 +@cython.cdivision(True)
  16 +cdef inline void append_queue(cdeque[int]& stack, int x, int y, int z, int d, int h, int w) nogil:
  17 + stack.push_back(z*h*w + y*w + x)
  18 +
  19 +
  20 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  21 +@cython.wraparound(False)
  22 +@cython.nonecheck(False)
  23 +@cython.cdivision(True)
  24 +cdef inline void pop_queue(cdeque[int]& stack, int* x, int* y, int* z, int d, int h, int w) nogil:
  25 + cdef int i = stack.front()
  26 + stack.pop_front()
  27 + x[0] = i % w
  28 + y[0] = (i / w) % h
  29 + z[0] = i / (h * w)
  30 +
  31 +
  32 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  33 +def floodfill(np.ndarray[image_t, ndim=3] data, int i, int j, int k, int v, int fill, np.ndarray[mask_t, ndim=3] out):
  34 +
  35 + cdef int to_return = 0
  36 + if out is None:
  37 + out = np.zeros_like(data)
  38 + to_return = 1
  39 +
  40 + cdef int x, y, z
  41 + cdef int w, h, d
  42 +
  43 + d = data.shape[0]
  44 + h = data.shape[1]
  45 + w = data.shape[2]
  46 +
  47 + stack = [(i, j, k), ]
  48 + out[k, j, i] = fill
  49 +
  50 + while stack:
  51 + x, y, z = stack.pop()
  52 +
  53 + if z + 1 < d and data[z + 1, y, x] == v and out[z + 1, y, x] != fill:
  54 + out[z + 1, y, x] = fill
  55 + stack.append((x, y, z + 1))
  56 +
  57 + if z - 1 >= 0 and data[z - 1, y, x] == v and out[z - 1, y, x] != fill:
  58 + out[z - 1, y, x] = fill
  59 + stack.append((x, y, z - 1))
  60 +
  61 + if y + 1 < h and data[z, y + 1, x] == v and out[z, y + 1, x] != fill:
  62 + out[z, y + 1, x] = fill
  63 + stack.append((x, y + 1, z))
  64 +
  65 + if y - 1 >= 0 and data[z, y - 1, x] == v and out[z, y - 1, x] != fill:
  66 + out[z, y - 1, x] = fill
  67 + stack.append((x, y - 1, z))
  68 +
  69 + if x + 1 < w and data[z, y, x + 1] == v and out[z, y, x + 1] != fill:
  70 + out[z, y, x + 1] = fill
  71 + stack.append((x + 1, y, z))
  72 +
  73 + if x - 1 >= 0 and data[z, y, x - 1] == v and out[z, y, x - 1] != fill:
  74 + out[z, y, x - 1] = fill
  75 + stack.append((x - 1, y, z))
  76 +
  77 + if to_return:
  78 + return out
  79 +
  80 +
  81 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  82 +@cython.wraparound(False)
  83 +@cython.nonecheck(False)
  84 +def floodfill_threshold(np.ndarray[image_t, ndim=3] data, list seeds, int t0, int t1, int fill, np.ndarray[mask_t, ndim=3] out):
  85 +
  86 + cdef int to_return = 0
  87 + if out is None:
  88 + out = np.zeros_like(data)
  89 + to_return = 1
  90 +
  91 + cdef int x, y, z
  92 + cdef int w, h, d
  93 + cdef int xo, yo, zo
  94 +
  95 + d = data.shape[0]
  96 + h = data.shape[1]
  97 + w = data.shape[2]
  98 +
  99 + stack = deque()
  100 +
  101 + for i, j, k in seeds:
  102 + if data[k, j, i] >= t0 and data[k, j, i] <= t1:
  103 + stack.append((i, j, k))
  104 + out[k, j, i] = fill
  105 +
  106 + while stack:
  107 + x, y, z = stack.pop()
  108 +
  109 + xo = x
  110 + yo = y
  111 + zo = z
  112 +
  113 + if z + 1 < d and data[z + 1, y, x] >= t0 and data[z + 1, y, x] <= t1 and out[zo + 1, yo, xo] != fill:
  114 + out[zo + 1, yo, xo] = fill
  115 + stack.append((x, y, z + 1))
  116 +
  117 + if z - 1 >= 0 and data[z - 1, y, x] >= t0 and data[z - 1, y, x] <= t1 and out[zo - 1, yo, xo] != fill:
  118 + out[zo - 1, yo, xo] = fill
  119 + stack.append((x, y, z - 1))
  120 +
  121 + if y + 1 < h and data[z, y + 1, x] >= t0 and data[z, y + 1, x] <= t1 and out[zo, yo + 1, xo] != fill:
  122 + out[zo, yo + 1, xo] = fill
  123 + stack.append((x, y + 1, z))
  124 +
  125 + if y - 1 >= 0 and data[z, y - 1, x] >= t0 and data[z, y - 1, x] <= t1 and out[zo, yo - 1, xo] != fill:
  126 + out[zo, yo - 1, xo] = fill
  127 + stack.append((x, y - 1, z))
  128 +
  129 + if x + 1 < w and data[z, y, x + 1] >= t0 and data[z, y, x + 1] <= t1 and out[zo, yo, xo + 1] != fill:
  130 + out[zo, yo, xo + 1] = fill
  131 + stack.append((x + 1, y, z))
  132 +
  133 + if x - 1 >= 0 and data[z, y, x - 1] >= t0 and data[z, y, x - 1] <= t1 and out[zo, yo, xo - 1] != fill:
  134 + out[zo, yo, xo - 1] = fill
  135 + stack.append((x - 1, y, z))
  136 +
  137 + if to_return:
  138 + return out
  139 +
  140 +
  141 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  142 +@cython.wraparound(False)
  143 +@cython.nonecheck(False)
  144 +def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float p, int fill, np.ndarray[mask_t, ndim=3] out):
  145 +
  146 + cdef int to_return = 0
  147 + if out is None:
  148 + out = np.zeros_like(data)
  149 + to_return = 1
  150 +
  151 + cdef cdeque[int] stack
  152 + cdef int x, y, z
  153 + cdef int w, h, d
  154 + cdef int xo, yo, zo
  155 + cdef int t0, t1
  156 +
  157 + cdef int i, j, k
  158 +
  159 + d = data.shape[0]
  160 + h = data.shape[1]
  161 + w = data.shape[2]
  162 +
  163 +
  164 + # stack = deque()
  165 +
  166 + x = 0
  167 + y = 0
  168 + z = 0
  169 +
  170 +
  171 + for i, j, k in seeds:
  172 + append_queue(stack, i, j, k, d, h, w)
  173 + out[k, j, i] = fill
  174 + print i, j, k, d, h, w
  175 +
  176 + with nogil:
  177 + while stack.size():
  178 + pop_queue(stack, &x, &y, &z, d, h, w)
  179 +
  180 + # print x, y, z, d, h, w
  181 +
  182 + xo = x
  183 + yo = y
  184 + zo = z
  185 +
  186 + t0 = <int>ceil(data[z, y, x] * (1 - p))
  187 + t1 = <int>floor(data[z, y, x] * (1 + p))
  188 +
  189 + if z + 1 < d and data[z + 1, y, x] >= t0 and data[z + 1, y, x] <= t1 and out[zo + 1, yo, xo] != fill:
  190 + out[zo + 1, yo, xo] = fill
  191 + append_queue(stack, x, y, z+1, d, h, w)
  192 +
  193 + if z - 1 >= 0 and data[z - 1, y, x] >= t0 and data[z - 1, y, x] <= t1 and out[zo - 1, yo, xo] != fill:
  194 + out[zo - 1, yo, xo] = fill
  195 + append_queue(stack, x, y, z-1, d, h, w)
  196 +
  197 + if y + 1 < h and data[z, y + 1, x] >= t0 and data[z, y + 1, x] <= t1 and out[zo, yo + 1, xo] != fill:
  198 + out[zo, yo + 1, xo] = fill
  199 + append_queue(stack, x, y+1, z, d, h, w)
  200 +
  201 + if y - 1 >= 0 and data[z, y - 1, x] >= t0 and data[z, y - 1, x] <= t1 and out[zo, yo - 1, xo] != fill:
  202 + out[zo, yo - 1, xo] = fill
  203 + append_queue(stack, x, y-1, z, d, h, w)
  204 +
  205 + if x + 1 < w and data[z, y, x + 1] >= t0 and data[z, y, x + 1] <= t1 and out[zo, yo, xo + 1] != fill:
  206 + out[zo, yo, xo + 1] = fill
  207 + append_queue(stack, x+1, y, z, d, h, w)
  208 +
  209 + if x - 1 >= 0 and data[z, y, x - 1] >= t0 and data[z, y, x - 1] <= t1 and out[zo, yo, xo - 1] != fill:
  210 + out[zo, yo, xo - 1] = fill
  211 + append_queue(stack, x-1, y, z, d, h, w)
  212 +
  213 + if to_return:
  214 + return out
... ...
invesalius/data/styles.py
... ... @@ -42,6 +42,8 @@ from skimage import filter
42 42  
43 43 from .measures import MeasureData
44 44  
  45 +from . import floodfill
  46 +
45 47 import watershed_process
46 48  
47 49 import utils
... ... @@ -1747,6 +1749,92 @@ class ReorientImageInteractorStyle(DefaultInteractorStyle):
1747 1749 buffer_.discard_vtk_image()
1748 1750 buffer_.discard_image()
1749 1751  
  1752 +
  1753 +class FlooFillMaskInteractorStyle(DefaultInteractorStyle):
  1754 + def __init__(self, viewer):
  1755 + DefaultInteractorStyle.__init__(self, viewer)
  1756 +
  1757 + self.viewer = viewer
  1758 + self.orientation = self.viewer.orientation
  1759 +
  1760 + self.picker = vtk.vtkWorldPointPicker()
  1761 + self.slice_actor = viewer.slice_data.actor
  1762 + self.slice_data = viewer.slice_data
  1763 +
  1764 + self.viewer.slice_.do_threshold_to_all_slices()
  1765 +
  1766 + self.AddObserver("LeftButtonPressEvent", self.OnFFClick)
  1767 +
  1768 + def OnFFClick(self, obj, evt):
  1769 + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None):
  1770 + return
  1771 +
  1772 +
  1773 + viewer = self.viewer
  1774 + iren = viewer.interactor
  1775 +
  1776 + mouse_x, mouse_y = iren.GetEventPosition()
  1777 + render = iren.FindPokedRenderer(mouse_x, mouse_y)
  1778 + slice_data = viewer.get_slice_data(render)
  1779 +
  1780 + self.picker.Pick(mouse_x, mouse_y, 0, render)
  1781 +
  1782 + coord = self.get_coordinate_cursor()
  1783 + position = slice_data.actor.GetInput().FindPoint(coord)
  1784 +
  1785 + if position != -1:
  1786 + coord = slice_data.actor.GetInput().GetPoint(position)
  1787 +
  1788 + if position < 0:
  1789 + position = viewer.calculate_matrix_position(coord)
  1790 +
  1791 + x, y, z = self.calcultate_scroll_position(position)
  1792 + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:]
  1793 +
  1794 + cp_mask = mask.copy()
  1795 +
  1796 + floodfill.floodfill_threshold(cp_mask, [[x, y, z]], 0, 1, 254, mask)
  1797 +
  1798 + viewer.OnScrollBar()
  1799 +
  1800 + def get_coordinate_cursor(self):
  1801 + # Find position
  1802 + x, y, z = self.picker.GetPickPosition()
  1803 + bounds = self.viewer.slice_data.actor.GetBounds()
  1804 + if bounds[0] == bounds[1]:
  1805 + x = bounds[0]
  1806 + elif bounds[2] == bounds[3]:
  1807 + y = bounds[2]
  1808 + elif bounds[4] == bounds[5]:
  1809 + z = bounds[4]
  1810 + return x, y, z
  1811 +
  1812 + def calcultate_scroll_position(self, position):
  1813 + # Based in the given coord (x, y, z), returns a list with the scroll positions for each
  1814 + # orientation, being the first position the sagital, second the coronal
  1815 + # and the last, axial.
  1816 +
  1817 + if self.orientation == 'AXIAL':
  1818 + image_width = self.slice_actor.GetInput().GetDimensions()[0]
  1819 + axial = self.slice_data.number
  1820 + coronal = position / image_width
  1821 + sagital = position % image_width
  1822 +
  1823 + elif self.orientation == 'CORONAL':
  1824 + image_width = self.slice_actor.GetInput().GetDimensions()[0]
  1825 + axial = position / image_width
  1826 + coronal = self.slice_data.number
  1827 + sagital = position % image_width
  1828 +
  1829 + elif self.orientation == 'SAGITAL':
  1830 + image_width = self.slice_actor.GetInput().GetDimensions()[1]
  1831 + axial = position / image_width
  1832 + coronal = position % image_width
  1833 + sagital = self.slice_data.number
  1834 +
  1835 + return sagital, coronal, axial
  1836 +
  1837 +
1750 1838 def get_style(style):
1751 1839 STYLES = {
1752 1840 const.STATE_DEFAULT: DefaultInteractorStyle,
... ... @@ -1762,5 +1850,8 @@ def get_style(style):
1762 1850 const.SLICE_STATE_EDITOR: EditorInteractorStyle,
1763 1851 const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle,
1764 1852 const.SLICE_STATE_REORIENT: ReorientImageInteractorStyle,
  1853 + const.SLICE_STATE_MASK_FFILL: FlooFillMaskInteractorStyle,
1765 1854 }
1766 1855 return STYLES[style]
  1856 +
  1857 +
... ...
invesalius/gui/frame.py
... ... @@ -439,6 +439,9 @@ class Frame(wx.Frame):
439 439 elif id == const.ID_REORIENT_IMG:
440 440 self.OnReorientImg()
441 441  
  442 + elif id == const.ID_FLOODFILL_MASK:
  443 + self.OnFillHolesManually()
  444 +
442 445 def OnSize(self, evt):
443 446 """
444 447 Refresh GUI when frame is resized.
... ... @@ -559,6 +562,9 @@ class Frame(wx.Frame):
559 562 rdlg = dlg.ReorientImageDialog()
560 563 rdlg.Show()
561 564  
  565 + def OnFillHolesManually(self):
  566 + Publisher.sendMessage('Enable style', const.SLICE_STATE_MASK_FFILL)
  567 +
562 568 # ------------------------------------------------------------------
563 569 # ------------------------------------------------------------------
564 570 # ------------------------------------------------------------------
... ... @@ -578,7 +584,8 @@ class MenuBar(wx.MenuBar):
578 584 self.enable_items = [const.ID_PROJECT_SAVE,
579 585 const.ID_PROJECT_SAVE_AS,
580 586 const.ID_PROJECT_CLOSE,
581   - const.ID_REORIENT_IMG]
  587 + const.ID_REORIENT_IMG,
  588 + const.ID_FLOODFILL_MASK]
582 589 self.__init_items()
583 590 self.__bind_events()
584 591  
... ... @@ -689,6 +696,9 @@ class MenuBar(wx.MenuBar):
689 696 self.clean_mask_menu = mask_menu.Append(const.ID_CLEAN_MASK, _(u"Clean Mask\tCtrl+Shift+A"))
690 697 self.clean_mask_menu.Enable(False)
691 698  
  699 + self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill mask holes manually"))
  700 + self.fill_hole_mask_menu.Enable(False)
  701 +
692 702 tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu)
693 703  
694 704 # Image menu
... ...
setup.py
... ... @@ -25,6 +25,12 @@ if sys.platform == &#39;linux2&#39;:
25 25 include_dirs=[numpy.get_include()],
26 26 extra_compile_args=['-fopenmp',],
27 27 extra_link_args=['-fopenmp',]),
  28 +
  29 + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"],
  30 + include_dirs=[numpy.get_include()],
  31 + extra_compile_args=['-fopenmp',],
  32 + extra_link_args=['-fopenmp',],
  33 + language='c++',),
28 34 ])
29 35 )
30 36  
... ...