diff --git a/invesalius/constants.py b/invesalius/constants.py index a35e9d4..7496faf 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -481,6 +481,7 @@ ID_BOOLEAN_MASK = wx.NewId() ID_CLEAN_MASK = wx.NewId() ID_REORIENT_IMG = wx.NewId() +ID_FLOODFILL_MASK = wx.NewId() #--------------------------------------------------------- STATE_DEFAULT = 1000 @@ -498,6 +499,7 @@ SLICE_STATE_SCROLL = 3007 SLICE_STATE_EDITOR = 3008 SLICE_STATE_WATERSHED = 3009 SLICE_STATE_REORIENT = 3010 +SLICE_STATE_MASK_FFILL = 3011 VOLUME_STATE_SEED = 2001 # STATE_LINEAR_MEASURE = 3001 @@ -515,6 +517,7 @@ SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES SLICE_STYLES.append(STATE_DEFAULT) SLICE_STYLES.append(SLICE_STATE_EDITOR) SLICE_STYLES.append(SLICE_STATE_WATERSHED) +SLICE_STYLES.append(SLICE_STATE_MASK_FFILL) VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, STATE_MEASURE_ANGLE] @@ -523,6 +526,7 @@ VOLUME_STYLES.append(STATE_DEFAULT) STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, SLICE_STATE_WATERSHED: 1, + SLICE_STATE_MASK_FFILL: 1, SLICE_STATE_CROSS: 2, SLICE_STATE_SCROLL: 2, SLICE_STATE_REORIENT: 2, diff --git a/invesalius/data/floodfill.pyx b/invesalius/data/floodfill.pyx new file mode 100644 index 0000000..4892326 --- /dev/null +++ b/invesalius/data/floodfill.pyx @@ -0,0 +1,214 @@ +import numpy as np +cimport numpy as np +cimport cython + +from collections import deque + +from libc.math cimport floor, ceil +from libcpp.deque cimport deque as cdeque + +from cy_my_types cimport image_t, mask_t + +@cython.boundscheck(False) # turn of bounds-checking for entire function +@cython.wraparound(False) +@cython.nonecheck(False) +@cython.cdivision(True) +cdef inline void append_queue(cdeque[int]& stack, int x, int y, int z, int d, int h, int w) nogil: + stack.push_back(z*h*w + y*w + x) + + +@cython.boundscheck(False) # turn of bounds-checking for entire function +@cython.wraparound(False) +@cython.nonecheck(False) +@cython.cdivision(True) +cdef inline void pop_queue(cdeque[int]& stack, int* x, int* y, int* z, int d, int h, int w) nogil: + cdef int i = stack.front() + stack.pop_front() + x[0] = i % w + y[0] = (i / w) % h + z[0] = i / (h * w) + + +@cython.boundscheck(False) # turn of bounds-checking for entire function +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): + + cdef int to_return = 0 + if out is None: + out = np.zeros_like(data) + to_return = 1 + + cdef int x, y, z + cdef int w, h, d + + d = data.shape[0] + h = data.shape[1] + w = data.shape[2] + + stack = [(i, j, k), ] + out[k, j, i] = fill + + while stack: + x, y, z = stack.pop() + + if z + 1 < d and data[z + 1, y, x] == v and out[z + 1, y, x] != fill: + out[z + 1, y, x] = fill + stack.append((x, y, z + 1)) + + if z - 1 >= 0 and data[z - 1, y, x] == v and out[z - 1, y, x] != fill: + out[z - 1, y, x] = fill + stack.append((x, y, z - 1)) + + if y + 1 < h and data[z, y + 1, x] == v and out[z, y + 1, x] != fill: + out[z, y + 1, x] = fill + stack.append((x, y + 1, z)) + + if y - 1 >= 0 and data[z, y - 1, x] == v and out[z, y - 1, x] != fill: + out[z, y - 1, x] = fill + stack.append((x, y - 1, z)) + + if x + 1 < w and data[z, y, x + 1] == v and out[z, y, x + 1] != fill: + out[z, y, x + 1] = fill + stack.append((x + 1, y, z)) + + if x - 1 >= 0 and data[z, y, x - 1] == v and out[z, y, x - 1] != fill: + out[z, y, x - 1] = fill + stack.append((x - 1, y, z)) + + if to_return: + return out + + +@cython.boundscheck(False) # turn of bounds-checking for entire function +@cython.wraparound(False) +@cython.nonecheck(False) +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): + + cdef int to_return = 0 + if out is None: + out = np.zeros_like(data) + to_return = 1 + + cdef int x, y, z + cdef int w, h, d + cdef int xo, yo, zo + + d = data.shape[0] + h = data.shape[1] + w = data.shape[2] + + stack = deque() + + for i, j, k in seeds: + if data[k, j, i] >= t0 and data[k, j, i] <= t1: + stack.append((i, j, k)) + out[k, j, i] = fill + + while stack: + x, y, z = stack.pop() + + xo = x + yo = y + zo = z + + 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: + out[zo + 1, yo, xo] = fill + stack.append((x, y, z + 1)) + + 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: + out[zo - 1, yo, xo] = fill + stack.append((x, y, z - 1)) + + 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: + out[zo, yo + 1, xo] = fill + stack.append((x, y + 1, z)) + + 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: + out[zo, yo - 1, xo] = fill + stack.append((x, y - 1, z)) + + 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: + out[zo, yo, xo + 1] = fill + stack.append((x + 1, y, z)) + + 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: + out[zo, yo, xo - 1] = fill + stack.append((x - 1, y, z)) + + if to_return: + return out + + +@cython.boundscheck(False) # turn of bounds-checking for entire function +@cython.wraparound(False) +@cython.nonecheck(False) +def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float p, int fill, np.ndarray[mask_t, ndim=3] out): + + cdef int to_return = 0 + if out is None: + out = np.zeros_like(data) + to_return = 1 + + cdef cdeque[int] stack + cdef int x, y, z + cdef int w, h, d + cdef int xo, yo, zo + cdef int t0, t1 + + cdef int i, j, k + + d = data.shape[0] + h = data.shape[1] + w = data.shape[2] + + + # stack = deque() + + x = 0 + y = 0 + z = 0 + + + for i, j, k in seeds: + append_queue(stack, i, j, k, d, h, w) + out[k, j, i] = fill + print i, j, k, d, h, w + + with nogil: + while stack.size(): + pop_queue(stack, &x, &y, &z, d, h, w) + + # print x, y, z, d, h, w + + xo = x + yo = y + zo = z + + t0 = ceil(data[z, y, x] * (1 - p)) + t1 = floor(data[z, y, x] * (1 + p)) + + 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: + out[zo + 1, yo, xo] = fill + append_queue(stack, x, y, z+1, d, h, w) + + 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: + out[zo - 1, yo, xo] = fill + append_queue(stack, x, y, z-1, d, h, w) + + 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: + out[zo, yo + 1, xo] = fill + append_queue(stack, x, y+1, z, d, h, w) + + 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: + out[zo, yo - 1, xo] = fill + append_queue(stack, x, y-1, z, d, h, w) + + 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: + out[zo, yo, xo + 1] = fill + append_queue(stack, x+1, y, z, d, h, w) + + 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: + out[zo, yo, xo - 1] = fill + append_queue(stack, x-1, y, z, d, h, w) + + if to_return: + return out diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index e59178b..70e9b47 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -42,6 +42,8 @@ from skimage import filter from .measures import MeasureData +from . import floodfill + import watershed_process import utils @@ -1747,6 +1749,92 @@ class ReorientImageInteractorStyle(DefaultInteractorStyle): buffer_.discard_vtk_image() buffer_.discard_image() + +class FlooFillMaskInteractorStyle(DefaultInteractorStyle): + def __init__(self, viewer): + DefaultInteractorStyle.__init__(self, viewer) + + self.viewer = viewer + self.orientation = self.viewer.orientation + + self.picker = vtk.vtkWorldPointPicker() + self.slice_actor = viewer.slice_data.actor + self.slice_data = viewer.slice_data + + self.viewer.slice_.do_threshold_to_all_slices() + + self.AddObserver("LeftButtonPressEvent", self.OnFFClick) + + def OnFFClick(self, obj, evt): + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + + + viewer = self.viewer + iren = viewer.interactor + + mouse_x, mouse_y = iren.GetEventPosition() + render = iren.FindPokedRenderer(mouse_x, mouse_y) + slice_data = viewer.get_slice_data(render) + + self.picker.Pick(mouse_x, mouse_y, 0, render) + + coord = self.get_coordinate_cursor() + position = slice_data.actor.GetInput().FindPoint(coord) + + if position != -1: + coord = slice_data.actor.GetInput().GetPoint(position) + + if position < 0: + position = viewer.calculate_matrix_position(coord) + + x, y, z = self.calcultate_scroll_position(position) + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + + cp_mask = mask.copy() + + floodfill.floodfill_threshold(cp_mask, [[x, y, z]], 0, 1, 254, mask) + + viewer.OnScrollBar() + + def get_coordinate_cursor(self): + # Find position + x, y, z = self.picker.GetPickPosition() + bounds = self.viewer.slice_data.actor.GetBounds() + if bounds[0] == bounds[1]: + x = bounds[0] + elif bounds[2] == bounds[3]: + y = bounds[2] + elif bounds[4] == bounds[5]: + z = bounds[4] + return x, y, z + + def calcultate_scroll_position(self, position): + # Based in the given coord (x, y, z), returns a list with the scroll positions for each + # orientation, being the first position the sagital, second the coronal + # and the last, axial. + + if self.orientation == 'AXIAL': + image_width = self.slice_actor.GetInput().GetDimensions()[0] + axial = self.slice_data.number + coronal = position / image_width + sagital = position % image_width + + elif self.orientation == 'CORONAL': + image_width = self.slice_actor.GetInput().GetDimensions()[0] + axial = position / image_width + coronal = self.slice_data.number + sagital = position % image_width + + elif self.orientation == 'SAGITAL': + image_width = self.slice_actor.GetInput().GetDimensions()[1] + axial = position / image_width + coronal = position % image_width + sagital = self.slice_data.number + + return sagital, coronal, axial + + def get_style(style): STYLES = { const.STATE_DEFAULT: DefaultInteractorStyle, @@ -1762,5 +1850,8 @@ def get_style(style): const.SLICE_STATE_EDITOR: EditorInteractorStyle, const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle, const.SLICE_STATE_REORIENT: ReorientImageInteractorStyle, + const.SLICE_STATE_MASK_FFILL: FlooFillMaskInteractorStyle, } return STYLES[style] + + diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 8dcb72d..842c5fc 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -439,6 +439,9 @@ class Frame(wx.Frame): elif id == const.ID_REORIENT_IMG: self.OnReorientImg() + elif id == const.ID_FLOODFILL_MASK: + self.OnFillHolesManually() + def OnSize(self, evt): """ Refresh GUI when frame is resized. @@ -559,6 +562,9 @@ class Frame(wx.Frame): rdlg = dlg.ReorientImageDialog() rdlg.Show() + def OnFillHolesManually(self): + Publisher.sendMessage('Enable style', const.SLICE_STATE_MASK_FFILL) + # ------------------------------------------------------------------ # ------------------------------------------------------------------ # ------------------------------------------------------------------ @@ -578,7 +584,8 @@ class MenuBar(wx.MenuBar): self.enable_items = [const.ID_PROJECT_SAVE, const.ID_PROJECT_SAVE_AS, const.ID_PROJECT_CLOSE, - const.ID_REORIENT_IMG] + const.ID_REORIENT_IMG, + const.ID_FLOODFILL_MASK] self.__init_items() self.__bind_events() @@ -689,6 +696,9 @@ class MenuBar(wx.MenuBar): self.clean_mask_menu = mask_menu.Append(const.ID_CLEAN_MASK, _(u"Clean Mask\tCtrl+Shift+A")) self.clean_mask_menu.Enable(False) + self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill mask holes manually")) + self.fill_hole_mask_menu.Enable(False) + tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu) # Image menu diff --git a/setup.py b/setup.py index d9d7acb..021206a 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,12 @@ if sys.platform == 'linux2': include_dirs=[numpy.get_include()], extra_compile_args=['-fopenmp',], extra_link_args=['-fopenmp',]), + + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"], + include_dirs=[numpy.get_include()], + extra_compile_args=['-fopenmp',], + extra_link_args=['-fopenmp',], + language='c++',), ]) ) -- libgit2 0.21.2