Commit 7c1411a5789343d8b58e30f083babdaabe482c88

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

Fill mask holes manually and remove connected mask parts (#47)

These tools use floodfill implemented using Cython c++.
invesalius/constants.py
@@ -481,6 +481,8 @@ ID_BOOLEAN_MASK = wx.NewId() @@ -481,6 +481,8 @@ ID_BOOLEAN_MASK = wx.NewId()
481 ID_CLEAN_MASK = wx.NewId() 481 ID_CLEAN_MASK = wx.NewId()
482 482
483 ID_REORIENT_IMG = wx.NewId() 483 ID_REORIENT_IMG = wx.NewId()
  484 +ID_FLOODFILL_MASK = wx.NewId()
  485 +ID_REMOVE_MASK_PART = wx.NewId()
484 486
485 #--------------------------------------------------------- 487 #---------------------------------------------------------
486 STATE_DEFAULT = 1000 488 STATE_DEFAULT = 1000
@@ -498,6 +500,8 @@ SLICE_STATE_SCROLL = 3007 @@ -498,6 +500,8 @@ SLICE_STATE_SCROLL = 3007
498 SLICE_STATE_EDITOR = 3008 500 SLICE_STATE_EDITOR = 3008
499 SLICE_STATE_WATERSHED = 3009 501 SLICE_STATE_WATERSHED = 3009
500 SLICE_STATE_REORIENT = 3010 502 SLICE_STATE_REORIENT = 3010
  503 +SLICE_STATE_MASK_FFILL = 3011
  504 +SLICE_STATE_REMOVE_MASK_PARTS = 3012
501 505
502 VOLUME_STATE_SEED = 2001 506 VOLUME_STATE_SEED = 2001
503 # STATE_LINEAR_MEASURE = 3001 507 # STATE_LINEAR_MEASURE = 3001
@@ -515,6 +519,8 @@ SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES @@ -515,6 +519,8 @@ SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES
515 SLICE_STYLES.append(STATE_DEFAULT) 519 SLICE_STYLES.append(STATE_DEFAULT)
516 SLICE_STYLES.append(SLICE_STATE_EDITOR) 520 SLICE_STYLES.append(SLICE_STATE_EDITOR)
517 SLICE_STYLES.append(SLICE_STATE_WATERSHED) 521 SLICE_STYLES.append(SLICE_STATE_WATERSHED)
  522 +SLICE_STYLES.append(SLICE_STATE_MASK_FFILL)
  523 +SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS)
518 524
519 VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, 525 VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE,
520 STATE_MEASURE_ANGLE] 526 STATE_MEASURE_ANGLE]
@@ -523,6 +529,8 @@ VOLUME_STYLES.append(STATE_DEFAULT) @@ -523,6 +529,8 @@ VOLUME_STYLES.append(STATE_DEFAULT)
523 529
524 STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, 530 STYLE_LEVEL = {SLICE_STATE_EDITOR: 1,
525 SLICE_STATE_WATERSHED: 1, 531 SLICE_STATE_WATERSHED: 1,
  532 + SLICE_STATE_MASK_FFILL: 2,
  533 + SLICE_STATE_REMOVE_MASK_PARTS: 2,
526 SLICE_STATE_CROSS: 2, 534 SLICE_STATE_CROSS: 2,
527 SLICE_STATE_SCROLL: 2, 535 SLICE_STATE_SCROLL: 2,
528 SLICE_STATE_REORIENT: 2, 536 SLICE_STATE_REORIENT: 2,
@@ -530,7 +538,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, @@ -530,7 +538,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1,
530 STATE_DEFAULT: 0, 538 STATE_DEFAULT: 0,
531 STATE_MEASURE_ANGLE: 2, 539 STATE_MEASURE_ANGLE: 2,
532 STATE_MEASURE_DISTANCE: 2, 540 STATE_MEASURE_DISTANCE: 2,
533 - STATE_WL: 2, 541 + STATE_WL: 3,
534 STATE_SPIN: 2, 542 STATE_SPIN: 2,
535 STATE_ZOOM: 2, 543 STATE_ZOOM: 2,
536 STATE_ZOOM_SL: 2, 544 STATE_ZOOM_SL: 2,
invesalius/data/cy_my_types.pxd
@@ -2,4 +2,11 @@ import numpy as np @@ -2,4 +2,11 @@ import numpy as np
2 cimport numpy as np 2 cimport numpy as np
3 cimport cython 3 cimport cython
4 4
5 -ctypedef np.int16_t image_t 5 +# ctypedef np.uint16_t image_t
  6 +
  7 +ctypedef fused image_t:
  8 + np.float64_t
  9 + np.int16_t
  10 + np.uint8_t
  11 +
  12 +ctypedef np.uint8_t mask_t
invesalius/data/floodfill.pyx 0 → 100644
@@ -0,0 +1,232 @@ @@ -0,0 +1,232 @@
  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 +from libcpp.vector cimport vector
  10 +
  11 +from cy_my_types cimport image_t, mask_t
  12 +
  13 +cdef struct s_coord:
  14 + int x
  15 + int y
  16 + int z
  17 +
  18 +ctypedef s_coord coord
  19 +
  20 +
  21 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  22 +@cython.wraparound(False)
  23 +@cython.nonecheck(False)
  24 +@cython.cdivision(True)
  25 +cdef inline void append_queue(cdeque[int]& stack, int x, int y, int z, int d, int h, int w) nogil:
  26 + stack.push_back(z*h*w + y*w + x)
  27 +
  28 +
  29 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  30 +@cython.wraparound(False)
  31 +@cython.nonecheck(False)
  32 +@cython.cdivision(True)
  33 +cdef inline void pop_queue(cdeque[int]& stack, int* x, int* y, int* z, int d, int h, int w) nogil:
  34 + cdef int i = stack.front()
  35 + stack.pop_front()
  36 + x[0] = i % w
  37 + y[0] = (i / w) % h
  38 + z[0] = i / (h * w)
  39 +
  40 +
  41 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  42 +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):
  43 +
  44 + cdef int to_return = 0
  45 + if out is None:
  46 + out = np.zeros_like(data)
  47 + to_return = 1
  48 +
  49 + cdef int x, y, z
  50 + cdef int w, h, d
  51 +
  52 + d = data.shape[0]
  53 + h = data.shape[1]
  54 + w = data.shape[2]
  55 +
  56 + stack = [(i, j, k), ]
  57 + out[k, j, i] = fill
  58 +
  59 + while stack:
  60 + x, y, z = stack.pop()
  61 +
  62 + if z + 1 < d and data[z + 1, y, x] == v and out[z + 1, y, x] != fill:
  63 + out[z + 1, y, x] = fill
  64 + stack.append((x, y, z + 1))
  65 +
  66 + if z - 1 >= 0 and data[z - 1, y, x] == v and out[z - 1, y, x] != fill:
  67 + out[z - 1, y, x] = fill
  68 + stack.append((x, y, z - 1))
  69 +
  70 + if y + 1 < h and data[z, y + 1, x] == v and out[z, y + 1, x] != fill:
  71 + out[z, y + 1, x] = fill
  72 + stack.append((x, y + 1, z))
  73 +
  74 + if y - 1 >= 0 and data[z, y - 1, x] == v and out[z, y - 1, x] != fill:
  75 + out[z, y - 1, x] = fill
  76 + stack.append((x, y - 1, z))
  77 +
  78 + if x + 1 < w and data[z, y, x + 1] == v and out[z, y, x + 1] != fill:
  79 + out[z, y, x + 1] = fill
  80 + stack.append((x + 1, y, z))
  81 +
  82 + if x - 1 >= 0 and data[z, y, x - 1] == v and out[z, y, x - 1] != fill:
  83 + out[z, y, x - 1] = fill
  84 + stack.append((x - 1, y, z))
  85 +
  86 + if to_return:
  87 + return out
  88 +
  89 +
  90 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  91 +@cython.wraparound(False)
  92 +@cython.nonecheck(False)
  93 +def floodfill_threshold(np.ndarray[image_t, ndim=3] data, list seeds, int t0, int t1, int fill, np.ndarray[mask_t, ndim=3] strct, np.ndarray[mask_t, ndim=3] out):
  94 +
  95 + cdef int to_return = 0
  96 + if out is None:
  97 + out = np.zeros_like(data)
  98 + to_return = 1
  99 +
  100 + cdef int x, y, z
  101 + cdef int dx, dy, dz
  102 + cdef int odx, ody, odz
  103 + cdef int xo, yo, zo
  104 + cdef int i, j, k
  105 + cdef int offset_x, offset_y, offset_z
  106 +
  107 + dz = data.shape[0]
  108 + dy = data.shape[1]
  109 + dx = data.shape[2]
  110 +
  111 + odz = strct.shape[0]
  112 + ody = strct.shape[1]
  113 + odx = strct.shape[2]
  114 +
  115 + cdef cdeque[coord] stack
  116 + cdef coord c
  117 +
  118 + offset_z = odz / 2
  119 + offset_y = ody / 2
  120 + offset_x = odx / 2
  121 +
  122 + for i, j, k in seeds:
  123 + if data[k, j, i] >= t0 and data[k, j, i] <= t1:
  124 + c.x = i
  125 + c.y = j
  126 + c.z = k
  127 + stack.push_back(c)
  128 + out[k, j, i] = fill
  129 +
  130 + with nogil:
  131 + while stack.size():
  132 + c = stack.back()
  133 + stack.pop_back()
  134 +
  135 + x = c.x
  136 + y = c.y
  137 + z = c.z
  138 +
  139 + out[z, y, x] = fill
  140 +
  141 + for k in xrange(odz):
  142 + zo = z + k - offset_z
  143 + for j in xrange(ody):
  144 + yo = y + j - offset_y
  145 + for i in xrange(odx):
  146 + if strct[k, j, i]:
  147 + xo = x + i - offset_x
  148 + if 0 <= xo < dx and 0 <= yo < dy and 0 <= zo < dz and out[zo, yo, xo] != fill and t0 <= data[zo, yo, xo] <= t1:
  149 + out[zo, yo, xo] = fill
  150 + c.x = xo
  151 + c.y = yo
  152 + c.z = zo
  153 + stack.push_back(c)
  154 +
  155 + if to_return:
  156 + return out
  157 +
  158 +
  159 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  160 +@cython.wraparound(False)
  161 +@cython.nonecheck(False)
  162 +def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float p, int fill, np.ndarray[mask_t, ndim=3] out):
  163 +
  164 + cdef int to_return = 0
  165 + if out is None:
  166 + out = np.zeros_like(data)
  167 + to_return = 1
  168 +
  169 + cdef cdeque[int] stack
  170 + cdef int x, y, z
  171 + cdef int w, h, d
  172 + cdef int xo, yo, zo
  173 + cdef int t0, t1
  174 +
  175 + cdef int i, j, k
  176 +
  177 + d = data.shape[0]
  178 + h = data.shape[1]
  179 + w = data.shape[2]
  180 +
  181 +
  182 + # stack = deque()
  183 +
  184 + x = 0
  185 + y = 0
  186 + z = 0
  187 +
  188 +
  189 + for i, j, k in seeds:
  190 + append_queue(stack, i, j, k, d, h, w)
  191 + out[k, j, i] = fill
  192 + print i, j, k, d, h, w
  193 +
  194 + with nogil:
  195 + while stack.size():
  196 + pop_queue(stack, &x, &y, &z, d, h, w)
  197 +
  198 + # print x, y, z, d, h, w
  199 +
  200 + xo = x
  201 + yo = y
  202 + zo = z
  203 +
  204 + t0 = <int>ceil(data[z, y, x] * (1 - p))
  205 + t1 = <int>floor(data[z, y, x] * (1 + p))
  206 +
  207 + 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:
  208 + out[zo + 1, yo, xo] = fill
  209 + append_queue(stack, x, y, z+1, d, h, w)
  210 +
  211 + 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:
  212 + out[zo - 1, yo, xo] = fill
  213 + append_queue(stack, x, y, z-1, d, h, w)
  214 +
  215 + 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:
  216 + out[zo, yo + 1, xo] = fill
  217 + append_queue(stack, x, y+1, z, d, h, w)
  218 +
  219 + 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:
  220 + out[zo, yo - 1, xo] = fill
  221 + append_queue(stack, x, y-1, z, d, h, w)
  222 +
  223 + 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:
  224 + out[zo, yo, xo + 1] = fill
  225 + append_queue(stack, x+1, y, z, d, h, w)
  226 +
  227 + 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:
  228 + out[zo, yo, xo - 1] = fill
  229 + append_queue(stack, x-1, y, z, d, h, w)
  230 +
  231 + if to_return:
  232 + return out
invesalius/data/mask.py
@@ -60,6 +60,8 @@ class EditionHistoryNode(object): @@ -60,6 +60,8 @@ class EditionHistoryNode(object):
60 mvolume[1:, 1:, self.index+1] = array 60 mvolume[1:, 1:, self.index+1] = array
61 if self.clean: 61 if self.clean:
62 mvolume[0, 0, self.index+1] = 1 62 mvolume[0, 0, self.index+1] = 1
  63 + elif self.orientation == 'VOLUME':
  64 + mvolume[:] = array
63 65
64 print "applying to", self.orientation, "at slice", self.index 66 print "applying to", self.orientation, "at slice", self.index
65 67
@@ -106,7 +108,15 @@ class EditionHistory(object): @@ -106,7 +108,15 @@ class EditionHistory(object):
106 ##self.index -= 1 108 ##self.index -= 1
107 ##h[self.index].commit_history(mvolume) 109 ##h[self.index].commit_history(mvolume)
108 #self._reload_slice(self.index - 1) 110 #self._reload_slice(self.index - 1)
109 - if actual_slices and actual_slices[h[self.index - 1].orientation] != h[self.index - 1].index: 111 + if h[self.index - 1].orientation == 'VOLUME':
  112 + self.index -= 1
  113 + print "================================"
  114 + print mvolume.shape
  115 + print "================================"
  116 + h[self.index].commit_history(mvolume)
  117 + self._reload_slice(self.index)
  118 + Publisher.sendMessage("Enable redo", True)
  119 + elif actual_slices and actual_slices[h[self.index - 1].orientation] != h[self.index - 1].index:
110 self._reload_slice(self.index - 1) 120 self._reload_slice(self.index - 1)
111 else: 121 else:
112 self.index -= 1 122 self.index -= 1
@@ -129,7 +139,12 @@ class EditionHistory(object): @@ -129,7 +139,12 @@ class EditionHistory(object):
129 ##h[self.index].commit_history(mvolume) 139 ##h[self.index].commit_history(mvolume)
130 #self._reload_slice(self.index + 1) 140 #self._reload_slice(self.index + 1)
131 141
132 - if actual_slices and actual_slices[h[self.index + 1].orientation] != h[self.index + 1].index: 142 + if h[self.index + 1].orientation == 'VOLUME':
  143 + self.index += 1
  144 + h[self.index].commit_history(mvolume)
  145 + self._reload_slice(self.index)
  146 + Publisher.sendMessage("Enable undo", True)
  147 + elif actual_slices and actual_slices[h[self.index + 1].orientation] != h[self.index + 1].index:
133 self._reload_slice(self.index + 1) 148 self._reload_slice(self.index + 1)
134 else: 149 else:
135 self.index += 1 150 self.index += 1
invesalius/data/slice_.py
@@ -1402,7 +1402,8 @@ class Slice(object): @@ -1402,7 +1402,8 @@ class Slice(object):
1402 buffer_slices = self.buffer_slices 1402 buffer_slices = self.buffer_slices
1403 actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, 1403 actual_slices = {"AXIAL": buffer_slices["AXIAL"].index,
1404 "CORONAL": buffer_slices["CORONAL"].index, 1404 "CORONAL": buffer_slices["CORONAL"].index,
1405 - "SAGITAL": buffer_slices["SAGITAL"].index,} 1405 + "SAGITAL": buffer_slices["SAGITAL"].index,
  1406 + "VOLUME": 0}
1406 self.current_mask.undo_history(actual_slices) 1407 self.current_mask.undo_history(actual_slices)
1407 for o in self.buffer_slices: 1408 for o in self.buffer_slices:
1408 self.buffer_slices[o].discard_mask() 1409 self.buffer_slices[o].discard_mask()
@@ -1413,7 +1414,8 @@ class Slice(object): @@ -1413,7 +1414,8 @@ class Slice(object):
1413 buffer_slices = self.buffer_slices 1414 buffer_slices = self.buffer_slices
1414 actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, 1415 actual_slices = {"AXIAL": buffer_slices["AXIAL"].index,
1415 "CORONAL": buffer_slices["CORONAL"].index, 1416 "CORONAL": buffer_slices["CORONAL"].index,
1416 - "SAGITAL": buffer_slices["SAGITAL"].index,} 1417 + "SAGITAL": buffer_slices["SAGITAL"].index,
  1418 + "VOLUME": 0}
1417 self.current_mask.redo_history(actual_slices) 1419 self.current_mask.redo_history(actual_slices)
1418 for o in self.buffer_slices: 1420 for o in self.buffer_slices:
1419 self.buffer_slices[o].discard_mask() 1421 self.buffer_slices[o].discard_mask()
invesalius/data/styles.py
@@ -40,8 +40,11 @@ from scipy.ndimage import watershed_ift, generate_binary_structure @@ -40,8 +40,11 @@ from scipy.ndimage import watershed_ift, generate_binary_structure
40 from skimage.morphology import watershed 40 from skimage.morphology import watershed
41 from skimage import filter 41 from skimage import filter
42 42
  43 +from gui import dialogs
43 from .measures import MeasureData 44 from .measures import MeasureData
44 45
  46 +from . import floodfill
  47 +
45 import watershed_process 48 import watershed_process
46 49
47 import utils 50 import utils
@@ -1747,6 +1750,170 @@ class ReorientImageInteractorStyle(DefaultInteractorStyle): @@ -1747,6 +1750,170 @@ class ReorientImageInteractorStyle(DefaultInteractorStyle):
1747 buffer_.discard_vtk_image() 1750 buffer_.discard_vtk_image()
1748 buffer_.discard_image() 1751 buffer_.discard_image()
1749 1752
  1753 +
  1754 +class FFillConfig(object):
  1755 + __metaclass__= utils.Singleton
  1756 + def __init__(self):
  1757 + self.dlg_visible = False
  1758 + self.target = "2D"
  1759 + self.con_2d = 4
  1760 + self.con_3d = 6
  1761 +
  1762 +
  1763 +class FloodFillMaskInteractorStyle(DefaultInteractorStyle):
  1764 + def __init__(self, viewer):
  1765 + DefaultInteractorStyle.__init__(self, viewer)
  1766 +
  1767 + self.viewer = viewer
  1768 + self.orientation = self.viewer.orientation
  1769 +
  1770 + self.picker = vtk.vtkWorldPointPicker()
  1771 + self.slice_actor = viewer.slice_data.actor
  1772 + self.slice_data = viewer.slice_data
  1773 +
  1774 + self.config = FFillConfig()
  1775 + self.dlg_ffill = None
  1776 +
  1777 + self.t0 = 0
  1778 + self.t1 = 1
  1779 + self.fill_value = 254
  1780 +
  1781 + self._dlg_title = _(u"Fill holes")
  1782 +
  1783 + self.AddObserver("LeftButtonPressEvent", self.OnFFClick)
  1784 +
  1785 + def SetUp(self):
  1786 + if not self.config.dlg_visible:
  1787 + self.config.dlg_visible = True
  1788 + self.dlg_ffill = dialogs.FFillOptionsDialog(self._dlg_title, self.config)
  1789 + self.dlg_ffill.Show()
  1790 +
  1791 + def CleanUp(self):
  1792 + if (self.dlg_ffill is not None) and (self.config.dlg_visible):
  1793 + self.config.dlg_visible = False
  1794 + self.dlg_ffill.Destroy()
  1795 + self.dlg_ffill = None
  1796 +
  1797 + def OnFFClick(self, obj, evt):
  1798 + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None):
  1799 + return
  1800 +
  1801 + viewer = self.viewer
  1802 + iren = viewer.interactor
  1803 +
  1804 + mouse_x, mouse_y = iren.GetEventPosition()
  1805 + render = iren.FindPokedRenderer(mouse_x, mouse_y)
  1806 + slice_data = viewer.get_slice_data(render)
  1807 +
  1808 + self.picker.Pick(mouse_x, mouse_y, 0, render)
  1809 +
  1810 + coord = self.get_coordinate_cursor()
  1811 + position = slice_data.actor.GetInput().FindPoint(coord)
  1812 +
  1813 + if position != -1:
  1814 + coord = slice_data.actor.GetInput().GetPoint(position)
  1815 +
  1816 + if position < 0:
  1817 + position = viewer.calculate_matrix_position(coord)
  1818 +
  1819 + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:]
  1820 + x, y, z = self.calcultate_scroll_position(position)
  1821 + if mask[z, y, x] < self.t0 or mask[z, y, x] > self.t1:
  1822 + return
  1823 +
  1824 + if self.config.target == "3D":
  1825 + bstruct = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8')
  1826 + self.viewer.slice_.do_threshold_to_all_slices()
  1827 + cp_mask = self.viewer.slice_.current_mask.matrix.copy()
  1828 + else:
  1829 + _bstruct = generate_binary_structure(2, CON2D[self.config.con_2d])
  1830 + if self.orientation == 'AXIAL':
  1831 + bstruct = np.zeros((1, 3, 3), dtype='uint8')
  1832 + bstruct[0] = _bstruct
  1833 + elif self.orientation == 'CORONAL':
  1834 + bstruct = np.zeros((3, 1, 3), dtype='uint8')
  1835 + bstruct[:, 0, :] = _bstruct
  1836 + elif self.orientation == 'SAGITAL':
  1837 + bstruct = np.zeros((3, 3, 1), dtype='uint8')
  1838 + bstruct[:, :, 0] = _bstruct
  1839 +
  1840 +
  1841 + floodfill.floodfill_threshold(mask, [[x, y, z]], self.t0, self.t1, self.fill_value, bstruct, mask)
  1842 +
  1843 + if self.config.target == '2D':
  1844 + b_mask = self.viewer.slice_.buffer_slices[self.orientation].mask
  1845 + index = self.viewer.slice_.buffer_slices[self.orientation].index
  1846 +
  1847 + if self.orientation == 'AXIAL':
  1848 + p_mask = mask[index,:,:].copy()
  1849 + elif self.orientation == 'CORONAL':
  1850 + p_mask = mask[:, index, :].copy()
  1851 + elif self.orientation == 'SAGITAL':
  1852 + p_mask = mask[:, :, index].copy()
  1853 +
  1854 + self.viewer.slice_.current_mask.save_history(index, self.orientation, p_mask, b_mask)
  1855 + else:
  1856 + self.viewer.slice_.current_mask.save_history(0, 'VOLUME', self.viewer.slice_.current_mask.matrix.copy(), cp_mask)
  1857 +
  1858 + self.viewer.slice_.buffer_slices['AXIAL'].discard_mask()
  1859 + self.viewer.slice_.buffer_slices['CORONAL'].discard_mask()
  1860 + self.viewer.slice_.buffer_slices['SAGITAL'].discard_mask()
  1861 +
  1862 + self.viewer.slice_.buffer_slices['AXIAL'].discard_vtk_mask()
  1863 + self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask()
  1864 + self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
  1865 +
  1866 + self.viewer.slice_.current_mask.was_edited = True
  1867 + Publisher.sendMessage('Reload actual slice')
  1868 +
  1869 + def get_coordinate_cursor(self):
  1870 + # Find position
  1871 + x, y, z = self.picker.GetPickPosition()
  1872 + bounds = self.viewer.slice_data.actor.GetBounds()
  1873 + if bounds[0] == bounds[1]:
  1874 + x = bounds[0]
  1875 + elif bounds[2] == bounds[3]:
  1876 + y = bounds[2]
  1877 + elif bounds[4] == bounds[5]:
  1878 + z = bounds[4]
  1879 + return x, y, z
  1880 +
  1881 + def calcultate_scroll_position(self, position):
  1882 + # Based in the given coord (x, y, z), returns a list with the scroll positions for each
  1883 + # orientation, being the first position the sagital, second the coronal
  1884 + # and the last, axial.
  1885 +
  1886 + if self.orientation == 'AXIAL':
  1887 + image_width = self.slice_actor.GetInput().GetDimensions()[0]
  1888 + axial = self.slice_data.number
  1889 + coronal = position / image_width
  1890 + sagital = position % image_width
  1891 +
  1892 + elif self.orientation == 'CORONAL':
  1893 + image_width = self.slice_actor.GetInput().GetDimensions()[0]
  1894 + axial = position / image_width
  1895 + coronal = self.slice_data.number
  1896 + sagital = position % image_width
  1897 +
  1898 + elif self.orientation == 'SAGITAL':
  1899 + image_width = self.slice_actor.GetInput().GetDimensions()[1]
  1900 + axial = position / image_width
  1901 + coronal = position % image_width
  1902 + sagital = self.slice_data.number
  1903 +
  1904 + return sagital, coronal, axial
  1905 +
  1906 +
  1907 +class RemoveMaskPartsInteractorStyle(FloodFillMaskInteractorStyle):
  1908 + def __init__(self, viewer):
  1909 + FloodFillMaskInteractorStyle.__init__(self, viewer)
  1910 + self.t0 = 254
  1911 + self.t1 = 255
  1912 + self.fill_value = 1
  1913 +
  1914 + self._dlg_title = _(u"Remove parts")
  1915 +
  1916 +
1750 def get_style(style): 1917 def get_style(style):
1751 STYLES = { 1918 STYLES = {
1752 const.STATE_DEFAULT: DefaultInteractorStyle, 1919 const.STATE_DEFAULT: DefaultInteractorStyle,
@@ -1762,5 +1929,9 @@ def get_style(style): @@ -1762,5 +1929,9 @@ def get_style(style):
1762 const.SLICE_STATE_EDITOR: EditorInteractorStyle, 1929 const.SLICE_STATE_EDITOR: EditorInteractorStyle,
1763 const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle, 1930 const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle,
1764 const.SLICE_STATE_REORIENT: ReorientImageInteractorStyle, 1931 const.SLICE_STATE_REORIENT: ReorientImageInteractorStyle,
  1932 + const.SLICE_STATE_MASK_FFILL: FloodFillMaskInteractorStyle,
  1933 + const.SLICE_STATE_REMOVE_MASK_PARTS: RemoveMaskPartsInteractorStyle,
1765 } 1934 }
1766 return STYLES[style] 1935 return STYLES[style]
  1936 +
  1937 +
invesalius/data/transforms.pyx
@@ -12,7 +12,7 @@ from cython.parallel import prange @@ -12,7 +12,7 @@ from cython.parallel import prange
12 @cython.boundscheck(False) # turn of bounds-checking for entire function 12 @cython.boundscheck(False) # turn of bounds-checking for entire function
13 @cython.cdivision(True) 13 @cython.cdivision(True)
14 @cython.wraparound(False) 14 @cython.wraparound(False)
15 -cdef inline void mul_mat4_vec4(np.float64_t[:, :] M, 15 +cdef inline void mul_mat4_vec4(double[:, :] M,
16 double* coord, 16 double* coord,
17 double* out) nogil: 17 double* out) nogil:
18 18
@@ -25,7 +25,7 @@ cdef inline void mul_mat4_vec4(np.float64_t[:, :] M, @@ -25,7 +25,7 @@ cdef inline void mul_mat4_vec4(np.float64_t[:, :] M,
25 @cython.boundscheck(False) # turn of bounds-checking for entire function 25 @cython.boundscheck(False) # turn of bounds-checking for entire function
26 @cython.cdivision(True) 26 @cython.cdivision(True)
27 @cython.wraparound(False) 27 @cython.wraparound(False)
28 -cdef image_t coord_transform(image_t[:, :, :] volume, np.float64_t[:, :] M, int x, int y, int z, double sx, double sy, double sz, short minterpol, image_t cval) nogil: 28 +cdef image_t coord_transform(image_t[:, :, :] volume, double[:, :] M, int x, int y, int z, double sx, double sy, double sz, short minterpol, image_t cval) nogil:
29 29
30 cdef double coord[4] 30 cdef double coord[4]
31 coord[0] = z*sz 31 coord[0] = z*sz
@@ -71,7 +71,7 @@ cdef image_t coord_transform(image_t[:, :, :] volume, np.float64_t[:, :] M, int @@ -71,7 +71,7 @@ cdef image_t coord_transform(image_t[:, :, :] volume, np.float64_t[:, :] M, int
71 @cython.wraparound(False) 71 @cython.wraparound(False)
72 def apply_view_matrix_transform(image_t[:, :, :] volume, 72 def apply_view_matrix_transform(image_t[:, :, :] volume,
73 spacing, 73 spacing,
74 - np.float64_t[:, :] M, 74 + double[:, :] M,
75 unsigned int n, str orientation, 75 unsigned int n, str orientation,
76 int minterpol, 76 int minterpol,
77 image_t cval, 77 image_t cval,
invesalius/data/viewer_slice.py
@@ -1344,7 +1344,7 @@ class Viewer(wx.Panel): @@ -1344,7 +1344,7 @@ class Viewer(wx.Panel):
1344 actor = vtk.vtkImageActor() 1344 actor = vtk.vtkImageActor()
1345 # TODO: Create a option to let the user set if he wants to interpolate 1345 # TODO: Create a option to let the user set if he wants to interpolate
1346 # the slice images. 1346 # the slice images.
1347 - #actor.InterpolateOff() 1347 + actor.InterpolateOff()
1348 slice_data = sd.SliceData() 1348 slice_data = sd.SliceData()
1349 slice_data.SetOrientation(self.orientation) 1349 slice_data.SetOrientation(self.orientation)
1350 slice_data.renderer = renderer 1350 slice_data.renderer = renderer
invesalius/gui/dialogs.py
@@ -1838,3 +1838,85 @@ def BitmapNotSameSize(): @@ -1838,3 +1838,85 @@ def BitmapNotSameSize():
1838 1838
1839 dlg.ShowModal() 1839 dlg.ShowModal()
1840 dlg.Destroy() 1840 dlg.Destroy()
  1841 +
  1842 +
  1843 +class FFillOptionsDialog(wx.Dialog):
  1844 + def __init__(self, title, config):
  1845 + pre = wx.PreDialog()
  1846 + pre.Create(wx.GetApp().GetTopWindow(), -1, title, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT)
  1847 + self.PostCreate(pre)
  1848 +
  1849 + self.config = config
  1850 +
  1851 + self._init_gui()
  1852 +
  1853 + def _init_gui(self):
  1854 + sizer = wx.GridBagSizer(5, 6)
  1855 +
  1856 + flag_labels = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL
  1857 +
  1858 + # self.target = wx.RadioBox(self, -1, "",
  1859 + # choices=[_(u"2D - Actual slice"), _(u"3D - Entire volume")],
  1860 + # style=wx.NO_BORDER | wx.VERTICAL)
  1861 + self.target_2d = wx.RadioButton(self, -1, _(u"2D - Actual slice"))
  1862 + self.target_3d = wx.RadioButton(self, -1, _(u"3D - All slices"))
  1863 +
  1864 + if self.config.target == "2D":
  1865 + self.target_2d.SetValue(1)
  1866 + else:
  1867 + self.target_3d.SetValue(1)
  1868 +
  1869 + choices2d = ["4", "8"]
  1870 + choices3d = ["6", "18", "26"]
  1871 + self.conect2D = wx.RadioBox(self, -1, _(u"2D Connectivity"), choices=choices2d, style=wx.NO_BORDER | wx.HORIZONTAL)
  1872 + self.conect3D = wx.RadioBox(self, -1, _(u"3D Connectivity"), choices=choices3d, style=wx.NO_BORDER | wx.HORIZONTAL)
  1873 +
  1874 + try:
  1875 + self.conect2D.SetSelection(choices2d.index(str(self.config.con_2d)))
  1876 + except ValueError:
  1877 + print "ERROR 2D"
  1878 + self.conect2D.SetSelection(0)
  1879 + self.config.con_2d = 4
  1880 +
  1881 + try:
  1882 + self.conect3D.SetSelection(choices3d.index(str(self.config.con_3d)))
  1883 + except ValueError:
  1884 + print "ERROR 3D"
  1885 + self.conect3D.SetSelection(0)
  1886 + self.config.con_3d = 6
  1887 +
  1888 + sizer.Add(wx.StaticText(self, -1, _(u"Parameters")), (0, 0), flag=wx.TOP|wx.LEFT|wx.RIGHT, border=7)
  1889 + sizer.AddStretchSpacer((0, 5))
  1890 + sizer.Add(self.target_2d, (1, 0), (1, 3), flag=wx.LEFT|wx.RIGHT, border=9)
  1891 + sizer.Add(self.target_3d, (2, 0), (1, 3), flag=wx.LEFT|wx.RIGHT, border=9)
  1892 + sizer.Add(self.conect2D, (3, 0), flag=wx.TOP|wx.LEFT|wx.RIGHT, border=9)
  1893 + sizer.Add(self.conect3D, (4, 0), flag=wx.ALL, border=9)
  1894 +
  1895 + self.SetSizer(sizer)
  1896 + sizer.Fit(self)
  1897 + self.Layout()
  1898 +
  1899 + self.Bind(wx.EVT_RADIOBUTTON, self.OnSetTarget)
  1900 + self.conect2D.Bind(wx.EVT_RADIOBOX, self.OnSetCon2D)
  1901 + self.conect3D.Bind(wx.EVT_RADIOBOX, self.OnSetCon3D)
  1902 + self.Bind(wx.EVT_CLOSE, self.OnClose)
  1903 +
  1904 + def OnSetTarget(self, evt):
  1905 + if self.target_2d.GetValue():
  1906 + self.config.target = "2D"
  1907 + else:
  1908 + self.config.target = "3D"
  1909 +
  1910 + def OnSetCon2D(self, evt):
  1911 + self.config.con_2d = int(self.conect2D.GetStringSelection())
  1912 + print self.config.con_2d
  1913 +
  1914 + def OnSetCon3D(self, evt):
  1915 + self.config.con_3d = int(self.conect3D.GetStringSelection())
  1916 + print self.config.con_3d
  1917 +
  1918 + def OnClose(self, evt):
  1919 + if self.config.dlg_visible:
  1920 + Publisher.sendMessage('Disable style', const.SLICE_STATE_MASK_FFILL)
  1921 + evt.Skip()
  1922 + self.Destroy()
invesalius/gui/frame.py
@@ -439,6 +439,12 @@ class Frame(wx.Frame): @@ -439,6 +439,12 @@ class Frame(wx.Frame):
439 elif id == const.ID_REORIENT_IMG: 439 elif id == const.ID_REORIENT_IMG:
440 self.OnReorientImg() 440 self.OnReorientImg()
441 441
  442 + elif id == const.ID_FLOODFILL_MASK:
  443 + self.OnFillHolesManually()
  444 +
  445 + elif id == const.ID_REMOVE_MASK_PART:
  446 + self.OnRemoveMaskParts()
  447 +
442 def OnSize(self, evt): 448 def OnSize(self, evt):
443 """ 449 """
444 Refresh GUI when frame is resized. 450 Refresh GUI when frame is resized.
@@ -559,6 +565,12 @@ class Frame(wx.Frame): @@ -559,6 +565,12 @@ class Frame(wx.Frame):
559 rdlg = dlg.ReorientImageDialog() 565 rdlg = dlg.ReorientImageDialog()
560 rdlg.Show() 566 rdlg.Show()
561 567
  568 + def OnFillHolesManually(self):
  569 + Publisher.sendMessage('Enable style', const.SLICE_STATE_MASK_FFILL)
  570 +
  571 + def OnRemoveMaskParts(self):
  572 + Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS)
  573 +
562 # ------------------------------------------------------------------ 574 # ------------------------------------------------------------------
563 # ------------------------------------------------------------------ 575 # ------------------------------------------------------------------
564 # ------------------------------------------------------------------ 576 # ------------------------------------------------------------------
@@ -578,7 +590,9 @@ class MenuBar(wx.MenuBar): @@ -578,7 +590,9 @@ class MenuBar(wx.MenuBar):
578 self.enable_items = [const.ID_PROJECT_SAVE, 590 self.enable_items = [const.ID_PROJECT_SAVE,
579 const.ID_PROJECT_SAVE_AS, 591 const.ID_PROJECT_SAVE_AS,
580 const.ID_PROJECT_CLOSE, 592 const.ID_PROJECT_CLOSE,
581 - const.ID_REORIENT_IMG] 593 + const.ID_REORIENT_IMG,
  594 + const.ID_FLOODFILL_MASK,
  595 + const.ID_REMOVE_MASK_PART,]
582 self.__init_items() 596 self.__init_items()
583 self.__bind_events() 597 self.__bind_events()
584 598
@@ -689,6 +703,12 @@ class MenuBar(wx.MenuBar): @@ -689,6 +703,12 @@ class MenuBar(wx.MenuBar):
689 self.clean_mask_menu = mask_menu.Append(const.ID_CLEAN_MASK, _(u"Clean Mask\tCtrl+Shift+A")) 703 self.clean_mask_menu = mask_menu.Append(const.ID_CLEAN_MASK, _(u"Clean Mask\tCtrl+Shift+A"))
690 self.clean_mask_menu.Enable(False) 704 self.clean_mask_menu.Enable(False)
691 705
  706 + self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill holes manually"))
  707 + self.fill_hole_mask_menu.Enable(False)
  708 +
  709 + self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts"))
  710 + self.remove_mask_part_menu.Enable(False)
  711 +
692 tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu) 712 tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu)
693 713
694 # Image menu 714 # Image menu
@@ -25,6 +25,10 @@ if sys.platform == &#39;linux2&#39;: @@ -25,6 +25,10 @@ if sys.platform == &#39;linux2&#39;:
25 include_dirs=[numpy.get_include()], 25 include_dirs=[numpy.get_include()],
26 extra_compile_args=['-fopenmp',], 26 extra_compile_args=['-fopenmp',],
27 extra_link_args=['-fopenmp',]), 27 extra_link_args=['-fopenmp',]),
  28 +
  29 + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"],
  30 + include_dirs=[numpy.get_include()],
  31 + language='c++',),
28 ]) 32 ])
29 ) 33 )
30 34
@@ -32,18 +36,22 @@ elif sys.platform == &#39;win32&#39;: @@ -32,18 +36,22 @@ elif sys.platform == &#39;win32&#39;:
32 setup( 36 setup(
33 cmdclass = {'build_ext': build_ext}, 37 cmdclass = {'build_ext': build_ext},
34 ext_modules = cythonize([ Extension("invesalius.data.mips", ["invesalius/data/mips.pyx"], 38 ext_modules = cythonize([ Extension("invesalius.data.mips", ["invesalius/data/mips.pyx"],
35 - include_dirs = [numpy.get_include()],  
36 - extra_compile_args=['/openmp'],), 39 + include_dirs = [numpy.get_include()],
  40 + extra_compile_args=['/openmp'],),
37 41
38 - Extension("invesalius.data.interpolation", ["invesalius/data/interpolation.pyx"],  
39 - include_dirs=[numpy.get_include()],  
40 - extra_compile_args=['/openmp'],), 42 + Extension("invesalius.data.interpolation", ["invesalius/data/interpolation.pyx"],
  43 + include_dirs=[numpy.get_include()],
  44 + extra_compile_args=['/openmp'],),
41 45
42 - Extension("invesalius.data.transforms", ["invesalius/data/transforms.pyx"],  
43 - include_dirs=[numpy.get_include()],  
44 - extra_compile_args=['/openmp'],), 46 + Extension("invesalius.data.transforms", ["invesalius/data/transforms.pyx"],
  47 + include_dirs=[numpy.get_include()],
  48 + extra_compile_args=['/openmp'],),
  49 +
  50 + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"],
  51 + include_dirs=[numpy.get_include()],
  52 + language='c++',),
45 ]) 53 ])
46 - ) 54 + )
47 55
48 else: 56 else:
49 setup( 57 setup(
@@ -63,5 +71,9 @@ else: @@ -63,5 +71,9 @@ else:
63 include_dirs=[numpy.get_include()], 71 include_dirs=[numpy.get_include()],
64 extra_compile_args=['-fopenmp',], 72 extra_compile_args=['-fopenmp',],
65 extra_link_args=['-fopenmp',]), 73 extra_link_args=['-fopenmp',]),
  74 +
  75 + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"],
  76 + include_dirs=[numpy.get_include()],
  77 + language='c++',),
66 ]) 78 ])
67 ) 79 )