From 7c1411a5789343d8b58e30f083babdaabe482c88 Mon Sep 17 00:00:00 2001 From: Thiago Franco de Moraes Date: Fri, 19 Aug 2016 10:19:00 -0300 Subject: [PATCH] Fill mask holes manually and remove connected mask parts (#47) --- invesalius/constants.py | 10 +++++++++- invesalius/data/cy_my_types.pxd | 9 ++++++++- invesalius/data/floodfill.pyx | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/data/mask.py | 19 +++++++++++++++++-- invesalius/data/slice_.py | 6 ++++-- invesalius/data/styles.py | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/data/transforms.pyx | 6 +++--- invesalius/data/viewer_slice.py | 2 +- invesalius/gui/dialogs.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/gui/frame.py | 22 +++++++++++++++++++++- setup.py | 30 +++++++++++++++++++++--------- 11 files changed, 569 insertions(+), 20 deletions(-) create mode 100644 invesalius/data/floodfill.pyx diff --git a/invesalius/constants.py b/invesalius/constants.py index a35e9d4..d9a1717 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -481,6 +481,8 @@ ID_BOOLEAN_MASK = wx.NewId() ID_CLEAN_MASK = wx.NewId() ID_REORIENT_IMG = wx.NewId() +ID_FLOODFILL_MASK = wx.NewId() +ID_REMOVE_MASK_PART = wx.NewId() #--------------------------------------------------------- STATE_DEFAULT = 1000 @@ -498,6 +500,8 @@ SLICE_STATE_SCROLL = 3007 SLICE_STATE_EDITOR = 3008 SLICE_STATE_WATERSHED = 3009 SLICE_STATE_REORIENT = 3010 +SLICE_STATE_MASK_FFILL = 3011 +SLICE_STATE_REMOVE_MASK_PARTS = 3012 VOLUME_STATE_SEED = 2001 # STATE_LINEAR_MEASURE = 3001 @@ -515,6 +519,8 @@ 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) +SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS) VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, STATE_MEASURE_ANGLE] @@ -523,6 +529,8 @@ VOLUME_STYLES.append(STATE_DEFAULT) STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, SLICE_STATE_WATERSHED: 1, + SLICE_STATE_MASK_FFILL: 2, + SLICE_STATE_REMOVE_MASK_PARTS: 2, SLICE_STATE_CROSS: 2, SLICE_STATE_SCROLL: 2, SLICE_STATE_REORIENT: 2, @@ -530,7 +538,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, STATE_DEFAULT: 0, STATE_MEASURE_ANGLE: 2, STATE_MEASURE_DISTANCE: 2, - STATE_WL: 2, + STATE_WL: 3, STATE_SPIN: 2, STATE_ZOOM: 2, STATE_ZOOM_SL: 2, diff --git a/invesalius/data/cy_my_types.pxd b/invesalius/data/cy_my_types.pxd index f834f26..36bd784 100644 --- a/invesalius/data/cy_my_types.pxd +++ b/invesalius/data/cy_my_types.pxd @@ -2,4 +2,11 @@ import numpy as np cimport numpy as np cimport cython -ctypedef np.int16_t image_t +# ctypedef np.uint16_t image_t + +ctypedef fused image_t: + np.float64_t + np.int16_t + np.uint8_t + +ctypedef np.uint8_t mask_t diff --git a/invesalius/data/floodfill.pyx b/invesalius/data/floodfill.pyx new file mode 100644 index 0000000..954adc2 --- /dev/null +++ b/invesalius/data/floodfill.pyx @@ -0,0 +1,232 @@ +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 libcpp.vector cimport vector + +from cy_my_types cimport image_t, mask_t + +cdef struct s_coord: + int x + int y + int z + +ctypedef s_coord coord + + +@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] strct, 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 dx, dy, dz + cdef int odx, ody, odz + cdef int xo, yo, zo + cdef int i, j, k + cdef int offset_x, offset_y, offset_z + + dz = data.shape[0] + dy = data.shape[1] + dx = data.shape[2] + + odz = strct.shape[0] + ody = strct.shape[1] + odx = strct.shape[2] + + cdef cdeque[coord] stack + cdef coord c + + offset_z = odz / 2 + offset_y = ody / 2 + offset_x = odx / 2 + + for i, j, k in seeds: + if data[k, j, i] >= t0 and data[k, j, i] <= t1: + c.x = i + c.y = j + c.z = k + stack.push_back(c) + out[k, j, i] = fill + + with nogil: + while stack.size(): + c = stack.back() + stack.pop_back() + + x = c.x + y = c.y + z = c.z + + out[z, y, x] = fill + + for k in xrange(odz): + zo = z + k - offset_z + for j in xrange(ody): + yo = y + j - offset_y + for i in xrange(odx): + if strct[k, j, i]: + xo = x + i - offset_x + 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: + out[zo, yo, xo] = fill + c.x = xo + c.y = yo + c.z = zo + stack.push_back(c) + + 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/mask.py b/invesalius/data/mask.py index 63c04c4..96df501 100644 --- a/invesalius/data/mask.py +++ b/invesalius/data/mask.py @@ -60,6 +60,8 @@ class EditionHistoryNode(object): mvolume[1:, 1:, self.index+1] = array if self.clean: mvolume[0, 0, self.index+1] = 1 + elif self.orientation == 'VOLUME': + mvolume[:] = array print "applying to", self.orientation, "at slice", self.index @@ -106,7 +108,15 @@ class EditionHistory(object): ##self.index -= 1 ##h[self.index].commit_history(mvolume) #self._reload_slice(self.index - 1) - if actual_slices and actual_slices[h[self.index - 1].orientation] != h[self.index - 1].index: + if h[self.index - 1].orientation == 'VOLUME': + self.index -= 1 + print "================================" + print mvolume.shape + print "================================" + h[self.index].commit_history(mvolume) + self._reload_slice(self.index) + Publisher.sendMessage("Enable redo", True) + elif actual_slices and actual_slices[h[self.index - 1].orientation] != h[self.index - 1].index: self._reload_slice(self.index - 1) else: self.index -= 1 @@ -129,7 +139,12 @@ class EditionHistory(object): ##h[self.index].commit_history(mvolume) #self._reload_slice(self.index + 1) - if actual_slices and actual_slices[h[self.index + 1].orientation] != h[self.index + 1].index: + if h[self.index + 1].orientation == 'VOLUME': + self.index += 1 + h[self.index].commit_history(mvolume) + self._reload_slice(self.index) + Publisher.sendMessage("Enable undo", True) + elif actual_slices and actual_slices[h[self.index + 1].orientation] != h[self.index + 1].index: self._reload_slice(self.index + 1) else: self.index += 1 diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index 8690ada..1adead7 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -1402,7 +1402,8 @@ class Slice(object): buffer_slices = self.buffer_slices actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, "CORONAL": buffer_slices["CORONAL"].index, - "SAGITAL": buffer_slices["SAGITAL"].index,} + "SAGITAL": buffer_slices["SAGITAL"].index, + "VOLUME": 0} self.current_mask.undo_history(actual_slices) for o in self.buffer_slices: self.buffer_slices[o].discard_mask() @@ -1413,7 +1414,8 @@ class Slice(object): buffer_slices = self.buffer_slices actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, "CORONAL": buffer_slices["CORONAL"].index, - "SAGITAL": buffer_slices["SAGITAL"].index,} + "SAGITAL": buffer_slices["SAGITAL"].index, + "VOLUME": 0} self.current_mask.redo_history(actual_slices) for o in self.buffer_slices: self.buffer_slices[o].discard_mask() diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index e59178b..f5d6755 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -40,8 +40,11 @@ from scipy.ndimage import watershed_ift, generate_binary_structure from skimage.morphology import watershed from skimage import filter +from gui import dialogs from .measures import MeasureData +from . import floodfill + import watershed_process import utils @@ -1747,6 +1750,170 @@ class ReorientImageInteractorStyle(DefaultInteractorStyle): buffer_.discard_vtk_image() buffer_.discard_image() + +class FFillConfig(object): + __metaclass__= utils.Singleton + def __init__(self): + self.dlg_visible = False + self.target = "2D" + self.con_2d = 4 + self.con_3d = 6 + + +class FloodFillMaskInteractorStyle(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.config = FFillConfig() + self.dlg_ffill = None + + self.t0 = 0 + self.t1 = 1 + self.fill_value = 254 + + self._dlg_title = _(u"Fill holes") + + self.AddObserver("LeftButtonPressEvent", self.OnFFClick) + + def SetUp(self): + if not self.config.dlg_visible: + self.config.dlg_visible = True + self.dlg_ffill = dialogs.FFillOptionsDialog(self._dlg_title, self.config) + self.dlg_ffill.Show() + + def CleanUp(self): + if (self.dlg_ffill is not None) and (self.config.dlg_visible): + self.config.dlg_visible = False + self.dlg_ffill.Destroy() + self.dlg_ffill = None + + 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) + + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + x, y, z = self.calcultate_scroll_position(position) + if mask[z, y, x] < self.t0 or mask[z, y, x] > self.t1: + return + + if self.config.target == "3D": + bstruct = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') + self.viewer.slice_.do_threshold_to_all_slices() + cp_mask = self.viewer.slice_.current_mask.matrix.copy() + else: + _bstruct = generate_binary_structure(2, CON2D[self.config.con_2d]) + if self.orientation == 'AXIAL': + bstruct = np.zeros((1, 3, 3), dtype='uint8') + bstruct[0] = _bstruct + elif self.orientation == 'CORONAL': + bstruct = np.zeros((3, 1, 3), dtype='uint8') + bstruct[:, 0, :] = _bstruct + elif self.orientation == 'SAGITAL': + bstruct = np.zeros((3, 3, 1), dtype='uint8') + bstruct[:, :, 0] = _bstruct + + + floodfill.floodfill_threshold(mask, [[x, y, z]], self.t0, self.t1, self.fill_value, bstruct, mask) + + if self.config.target == '2D': + b_mask = self.viewer.slice_.buffer_slices[self.orientation].mask + index = self.viewer.slice_.buffer_slices[self.orientation].index + + if self.orientation == 'AXIAL': + p_mask = mask[index,:,:].copy() + elif self.orientation == 'CORONAL': + p_mask = mask[:, index, :].copy() + elif self.orientation == 'SAGITAL': + p_mask = mask[:, :, index].copy() + + self.viewer.slice_.current_mask.save_history(index, self.orientation, p_mask, b_mask) + else: + self.viewer.slice_.current_mask.save_history(0, 'VOLUME', self.viewer.slice_.current_mask.matrix.copy(), cp_mask) + + self.viewer.slice_.buffer_slices['AXIAL'].discard_mask() + self.viewer.slice_.buffer_slices['CORONAL'].discard_mask() + self.viewer.slice_.buffer_slices['SAGITAL'].discard_mask() + + self.viewer.slice_.buffer_slices['AXIAL'].discard_vtk_mask() + self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask() + self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() + + self.viewer.slice_.current_mask.was_edited = True + Publisher.sendMessage('Reload actual slice') + + 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 + + +class RemoveMaskPartsInteractorStyle(FloodFillMaskInteractorStyle): + def __init__(self, viewer): + FloodFillMaskInteractorStyle.__init__(self, viewer) + self.t0 = 254 + self.t1 = 255 + self.fill_value = 1 + + self._dlg_title = _(u"Remove parts") + + def get_style(style): STYLES = { const.STATE_DEFAULT: DefaultInteractorStyle, @@ -1762,5 +1929,9 @@ def get_style(style): const.SLICE_STATE_EDITOR: EditorInteractorStyle, const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle, const.SLICE_STATE_REORIENT: ReorientImageInteractorStyle, + const.SLICE_STATE_MASK_FFILL: FloodFillMaskInteractorStyle, + const.SLICE_STATE_REMOVE_MASK_PARTS: RemoveMaskPartsInteractorStyle, } return STYLES[style] + + diff --git a/invesalius/data/transforms.pyx b/invesalius/data/transforms.pyx index 28a19d5..9bc8da3 100644 --- a/invesalius/data/transforms.pyx +++ b/invesalius/data/transforms.pyx @@ -12,7 +12,7 @@ from cython.parallel import prange @cython.boundscheck(False) # turn of bounds-checking for entire function @cython.cdivision(True) @cython.wraparound(False) -cdef inline void mul_mat4_vec4(np.float64_t[:, :] M, +cdef inline void mul_mat4_vec4(double[:, :] M, double* coord, double* out) nogil: @@ -25,7 +25,7 @@ cdef inline void mul_mat4_vec4(np.float64_t[:, :] M, @cython.boundscheck(False) # turn of bounds-checking for entire function @cython.cdivision(True) @cython.wraparound(False) -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: +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: cdef double coord[4] coord[0] = z*sz @@ -71,7 +71,7 @@ cdef image_t coord_transform(image_t[:, :, :] volume, np.float64_t[:, :] M, int @cython.wraparound(False) def apply_view_matrix_transform(image_t[:, :, :] volume, spacing, - np.float64_t[:, :] M, + double[:, :] M, unsigned int n, str orientation, int minterpol, image_t cval, diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index d0ffea0..c9cbead 100755 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -1344,7 +1344,7 @@ class Viewer(wx.Panel): actor = vtk.vtkImageActor() # TODO: Create a option to let the user set if he wants to interpolate # the slice images. - #actor.InterpolateOff() + actor.InterpolateOff() slice_data = sd.SliceData() slice_data.SetOrientation(self.orientation) slice_data.renderer = renderer diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 083227a..839cdc8 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -1838,3 +1838,85 @@ def BitmapNotSameSize(): dlg.ShowModal() dlg.Destroy() + + +class FFillOptionsDialog(wx.Dialog): + def __init__(self, title, config): + pre = wx.PreDialog() + pre.Create(wx.GetApp().GetTopWindow(), -1, title, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) + self.PostCreate(pre) + + self.config = config + + self._init_gui() + + def _init_gui(self): + sizer = wx.GridBagSizer(5, 6) + + flag_labels = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL + + # self.target = wx.RadioBox(self, -1, "", + # choices=[_(u"2D - Actual slice"), _(u"3D - Entire volume")], + # style=wx.NO_BORDER | wx.VERTICAL) + self.target_2d = wx.RadioButton(self, -1, _(u"2D - Actual slice")) + self.target_3d = wx.RadioButton(self, -1, _(u"3D - All slices")) + + if self.config.target == "2D": + self.target_2d.SetValue(1) + else: + self.target_3d.SetValue(1) + + choices2d = ["4", "8"] + choices3d = ["6", "18", "26"] + self.conect2D = wx.RadioBox(self, -1, _(u"2D Connectivity"), choices=choices2d, style=wx.NO_BORDER | wx.HORIZONTAL) + self.conect3D = wx.RadioBox(self, -1, _(u"3D Connectivity"), choices=choices3d, style=wx.NO_BORDER | wx.HORIZONTAL) + + try: + self.conect2D.SetSelection(choices2d.index(str(self.config.con_2d))) + except ValueError: + print "ERROR 2D" + self.conect2D.SetSelection(0) + self.config.con_2d = 4 + + try: + self.conect3D.SetSelection(choices3d.index(str(self.config.con_3d))) + except ValueError: + print "ERROR 3D" + self.conect3D.SetSelection(0) + self.config.con_3d = 6 + + sizer.Add(wx.StaticText(self, -1, _(u"Parameters")), (0, 0), flag=wx.TOP|wx.LEFT|wx.RIGHT, border=7) + sizer.AddStretchSpacer((0, 5)) + sizer.Add(self.target_2d, (1, 0), (1, 3), flag=wx.LEFT|wx.RIGHT, border=9) + sizer.Add(self.target_3d, (2, 0), (1, 3), flag=wx.LEFT|wx.RIGHT, border=9) + sizer.Add(self.conect2D, (3, 0), flag=wx.TOP|wx.LEFT|wx.RIGHT, border=9) + sizer.Add(self.conect3D, (4, 0), flag=wx.ALL, border=9) + + self.SetSizer(sizer) + sizer.Fit(self) + self.Layout() + + self.Bind(wx.EVT_RADIOBUTTON, self.OnSetTarget) + self.conect2D.Bind(wx.EVT_RADIOBOX, self.OnSetCon2D) + self.conect3D.Bind(wx.EVT_RADIOBOX, self.OnSetCon3D) + self.Bind(wx.EVT_CLOSE, self.OnClose) + + def OnSetTarget(self, evt): + if self.target_2d.GetValue(): + self.config.target = "2D" + else: + self.config.target = "3D" + + def OnSetCon2D(self, evt): + self.config.con_2d = int(self.conect2D.GetStringSelection()) + print self.config.con_2d + + def OnSetCon3D(self, evt): + self.config.con_3d = int(self.conect3D.GetStringSelection()) + print self.config.con_3d + + def OnClose(self, evt): + if self.config.dlg_visible: + Publisher.sendMessage('Disable style', const.SLICE_STATE_MASK_FFILL) + evt.Skip() + self.Destroy() diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 8dcb72d..90e10f3 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -439,6 +439,12 @@ class Frame(wx.Frame): elif id == const.ID_REORIENT_IMG: self.OnReorientImg() + elif id == const.ID_FLOODFILL_MASK: + self.OnFillHolesManually() + + elif id == const.ID_REMOVE_MASK_PART: + self.OnRemoveMaskParts() + def OnSize(self, evt): """ Refresh GUI when frame is resized. @@ -559,6 +565,12 @@ class Frame(wx.Frame): rdlg = dlg.ReorientImageDialog() rdlg.Show() + def OnFillHolesManually(self): + Publisher.sendMessage('Enable style', const.SLICE_STATE_MASK_FFILL) + + def OnRemoveMaskParts(self): + Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS) + # ------------------------------------------------------------------ # ------------------------------------------------------------------ # ------------------------------------------------------------------ @@ -578,7 +590,9 @@ 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, + const.ID_REMOVE_MASK_PART,] self.__init_items() self.__bind_events() @@ -689,6 +703,12 @@ 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 holes manually")) + self.fill_hole_mask_menu.Enable(False) + + self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts")) + self.remove_mask_part_menu.Enable(False) + tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu) # Image menu diff --git a/setup.py b/setup.py index d9d7acb..c9223db 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,10 @@ 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()], + language='c++',), ]) ) @@ -32,18 +36,22 @@ elif sys.platform == 'win32': setup( cmdclass = {'build_ext': build_ext}, ext_modules = cythonize([ Extension("invesalius.data.mips", ["invesalius/data/mips.pyx"], - include_dirs = [numpy.get_include()], - extra_compile_args=['/openmp'],), + include_dirs = [numpy.get_include()], + extra_compile_args=['/openmp'],), - Extension("invesalius.data.interpolation", ["invesalius/data/interpolation.pyx"], - include_dirs=[numpy.get_include()], - extra_compile_args=['/openmp'],), + Extension("invesalius.data.interpolation", ["invesalius/data/interpolation.pyx"], + include_dirs=[numpy.get_include()], + extra_compile_args=['/openmp'],), - Extension("invesalius.data.transforms", ["invesalius/data/transforms.pyx"], - include_dirs=[numpy.get_include()], - extra_compile_args=['/openmp'],), + Extension("invesalius.data.transforms", ["invesalius/data/transforms.pyx"], + include_dirs=[numpy.get_include()], + extra_compile_args=['/openmp'],), + + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"], + include_dirs=[numpy.get_include()], + language='c++',), ]) - ) + ) else: setup( @@ -63,5 +71,9 @@ else: 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()], + language='c++',), ]) ) -- libgit2 0.21.2