diff --git a/invesalius/constants.py b/invesalius/constants.py index ced611b..64745ae 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -517,6 +517,7 @@ ID_CLEAN_MASK = wx.NewId() ID_REORIENT_IMG = wx.NewId() ID_FLOODFILL_MASK = wx.NewId() +ID_FILL_HOLE_AUTO = wx.NewId() ID_REMOVE_MASK_PART = wx.NewId() ID_SELECT_MASK_PART = wx.NewId() ID_FLOODFILL_SEGMENTATION = wx.NewId() diff --git a/invesalius/data/floodfill.pyx b/invesalius/data/floodfill.pyx index 954adc2..2659221 100644 --- a/invesalius/data/floodfill.pyx +++ b/invesalius/data/floodfill.pyx @@ -4,6 +4,7 @@ cimport cython from collections import deque +from cython.parallel import prange from libc.math cimport floor, ceil from libcpp.deque cimport deque as cdeque from libcpp.vector cimport vector @@ -230,3 +231,28 @@ def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float if to_return: return out + + +# @cython.boundscheck(False) +# @cython.wraparound(False) +# @cython.nonecheck(False) +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): + cdef np.ndarray[np.uint32_t, ndim=1] sizes = np.zeros(shape=(nlabels + 1), dtype=np.uint32) + cdef int x, y, z + cdef int dx, dy, dz + + dz = mask.shape[0] + dy = mask.shape[1] + dx = mask.shape[2] + + for z in xrange(dz): + for y in xrange(dy): + for x in xrange(dx): + sizes[labels[z, y, x]] += 1 + + + for z in prange(dz, nogil=True): + for y in xrange(dy): + for x in xrange(dx): + if sizes[labels[z, y, x]] <= max_size: + mask[z, y, x] = 254 diff --git a/invesalius/data/mask.py b/invesalius/data/mask.py index 510b2c4..30b6b78 100644 --- a/invesalius/data/mask.py +++ b/invesalius/data/mask.py @@ -23,15 +23,17 @@ import random import shutil import tempfile -import numpy +import numpy as np import vtk import invesalius.constants as const import invesalius.data.imagedata_utils as iu import invesalius.session as ses -from wx.lib.pubsub import pub as Publisher +from . import floodfill +from wx.lib.pubsub import pub as Publisher +from scipy import ndimage class EditionHistoryNode(object): def __init__(self, index, orientation, array, clean=False): @@ -43,11 +45,11 @@ class EditionHistoryNode(object): self._save_array(array) def _save_array(self, array): - numpy.save(self.filename, array) + np.save(self.filename, array) print "Saving history", self.index, self.orientation, self.filename, self.clean def commit_history(self, mvolume): - array = numpy.load(self.filename) + array = np.load(self.filename) if self.orientation == 'AXIAL': mvolume[self.index+1,1:,1:] = array if self.clean: @@ -295,7 +297,7 @@ class Mask(): def _open_mask(self, filename, shape, dtype='uint8'): print ">>", filename, shape self.temp_file = filename - self.matrix = numpy.memmap(filename, shape=shape, dtype=dtype, mode="r+") + self.matrix = np.memmap(filename, shape=shape, dtype=dtype, mode="r+") def _set_class_index(self, index): Mask.general_index = index @@ -309,7 +311,7 @@ class Mask(): """ self.temp_file = tempfile.mktemp() shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 - self.matrix = numpy.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) + self.matrix = np.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) def clean(self): self.matrix[1:, 1:, 1:] = 0 @@ -340,6 +342,24 @@ class Mask(): def clear_history(self): self.history.clear_history() + def fill_holes_auto(self, idx): + matrix = self.matrix[idx+1, 1:, 1:] + matrix = matrix.reshape(1, matrix.shape[0], matrix.shape[1]) + imask = (~(matrix > 127)) + labels, nlabels = ndimage.label(imask, output=np.uint16) + + floodfill.fill_holes_automatically(matrix, labels, nlabels, 100000) + + # for l in xrange(nlabels): + # trues = (labels == l) + # size = trues.sum() + # print l, size + + # if size <= 1000: + # matrix[trues] = 254 + # self.was_edited = True + + def __del__(self): if self.is_shown: self.history._config_undo_redo(False) diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index da1f752..1673ad6 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -194,6 +194,8 @@ class Slice(object): Publisher.subscribe(self.__undo_edition, 'Undo edition') Publisher.subscribe(self.__redo_edition, 'Redo edition') + + Publisher.subscribe(self._fill_holes_auto, 'Fill holes automatically') def GetMaxSliceNumber(self, orientation): shape = self.matrix.shape @@ -1482,3 +1484,17 @@ class Slice(object): #filename, filetype = pubsub_evt.data #if (filetype == const.FILETYPE_IMAGEDATA): #iu.Export(imagedata, filename) + + def _fill_holes_auto(self, pubsub_evt): + self.do_threshold_to_all_slices() + self.current_mask.fill_holes_auto(self.buffer_slices['AXIAL'].index) + + self.buffer_slices['AXIAL'].discard_mask() + self.buffer_slices['CORONAL'].discard_mask() + self.buffer_slices['SAGITAL'].discard_mask() + + self.buffer_slices['AXIAL'].discard_vtk_mask() + self.buffer_slices['CORONAL'].discard_vtk_mask() + self.buffer_slices['SAGITAL'].discard_vtk_mask() + + Publisher.sendMessage('Reload actual slice') diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 9dd8a90..3f1640e 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -1863,11 +1863,11 @@ class PanelTargeFFill(wx.Panel): self.Layout() class Panel2DConnectivity(wx.Panel): - def __init__(self, parent, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER): + def __init__(self, parent, ID=-1, show_orientation=False, style=wx.TAB_TRAVERSAL|wx.NO_BORDER): wx.Panel.__init__(self, parent, ID, style=style) - self._init_gui() + self._init_gui(show_orientation) - def _init_gui(self): + def _init_gui(self, show_orientation): self.conect2D_4 = wx.RadioButton(self, -1, "4", style=wx.RB_GROUP) self.conect2D_8 = wx.RadioButton(self, -1, "8") @@ -1879,6 +1879,14 @@ class Panel2DConnectivity(wx.Panel): sizer.Add(self.conect2D_8, (2, 1), flag=wx.LEFT, border=7) sizer.AddStretchSpacer((3, 0)) + if show_orientation: + self.cmb_orientation = wx.ComboBox(self, -1, choices=(_(u"Axial"), _(u"Coronal"), _(u"Sagital")), style=wx.CB_READONLY) + self.cmb_orientation.SetSelection(0) + + sizer.Add(wx.StaticText(self, -1, _(u"Orientation")), (4, 0), (1, 6), flag=wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=5) + sizer.Add(self.cmb_orientation, (5, 0), (1, 10), flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) + sizer.AddStretchSpacer((6, 0)) + self.SetSizer(sizer) sizer.Fit(self) self.Layout() @@ -2473,3 +2481,90 @@ class CropOptionsDialog(wx.Dialog): Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK) evt.Skip() self.Destroy() + + +class FillHolesAutoDialog(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._init_gui() + + def _init_gui(self): + if sys.platform == "win32": + border_style = wx.SIMPLE_BORDER + else: + border_style = wx.SUNKEN_BORDER + + self.panel_target = PanelTargeFFill(self, style=border_style|wx.TAB_TRAVERSAL) + self.panel2dcon = Panel2DConnectivity(self, show_orientation=True, style=border_style|wx.TAB_TRAVERSAL) + self.panel3dcon = Panel3DConnectivity(self, style=border_style|wx.TAB_TRAVERSAL) + + self.panel_target.target_2d.SetValue(1) + self.panel2dcon.Enable(1) + self.panel3dcon.Enable(0) + + self.panel2dcon.conect2D_8.SetValue(1) + + self.apply_btn = wx.Button(self, wx.ID_APPLY) + self.close_btn = wx.Button(self, wx.ID_CLOSE) + + # Sizer + sizer = wx.BoxSizer(wx.VERTICAL) + + sizer.AddSpacer(5) + sizer.Add(wx.StaticText(self, -1, _(u"Parameters")), flag=wx.LEFT, border=5) + sizer.AddSpacer(5) + sizer.Add(self.panel_target, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) + sizer.AddSpacer(5) + sizer.Add(self.panel2dcon, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) + sizer.AddSpacer(5) + sizer.Add(self.panel3dcon, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=7) + sizer.AddSpacer(5) + sizer.Add(self.apply_btn, 0, flag=wx.ALIGN_RIGHT|wx.RIGHT, border=7) + sizer.Add(self.close_btn, 0, flag=wx.ALIGN_RIGHT|wx.RIGHT, border=7) + sizer.AddSpacer(5) + + self.SetSizer(sizer) + sizer.Fit(self) + self.Layout() + + self.close_btn.Bind(wx.EVT_BUTTON, self.OnBtnClose) + self.Bind(wx.EVT_RADIOBUTTON, self.OnSetRadio) + self.Bind(wx.EVT_CLOSE, self.OnClose) + + def OnBtnClose(self, evt): + self.Close() + + def OnSetRadio(self, evt): + # Target + if self.panel_target.target_2d.GetValue(): + self.config.target = "2D" + self.panel2dcon.Enable(1) + self.panel3dcon.Enable(0) + else: + self.config.target = "3D" + self.panel3dcon.Enable(1) + self.panel2dcon.Enable(0) + + # 2D + if self.panel2dcon.conect2D_4.GetValue(): + self.config.con_2d = 4 + elif self.panel2dcon.conect2D_8.GetValue(): + self.config.con_2d = 8 + + # 3D + if self.panel3dcon.conect3D_6.GetValue(): + self.config.con_3d = 6 + elif self.panel3dcon.conect3D_18.GetValue(): + self.config.con_3d = 18 + elif self.panel3dcon.conect3D_26.GetValue(): + self.config.con_3d = 26 + + def OnClose(self, evt): + print "ONCLOSE" + 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 3afa1f5..925cc43 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -445,6 +445,9 @@ class Frame(wx.Frame): elif id == const.ID_FLOODFILL_MASK: self.OnFillHolesManually() + elif id == const.ID_FILL_HOLE_AUTO: + self.OnFillHolesAutomatically() + elif id == const.ID_REMOVE_MASK_PART: self.OnRemoveMaskParts() @@ -592,6 +595,11 @@ class Frame(wx.Frame): def OnFillHolesManually(self): Publisher.sendMessage('Enable style', const.SLICE_STATE_MASK_FFILL) + def OnFillHolesAutomatically(self): + # Publisher.sendMessage('Fill holes automatically') + fdlg = dlg.FillHolesAutoDialog() + fdlg.Show() + def OnRemoveMaskParts(self): Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS) @@ -628,6 +636,7 @@ class MenuBar(wx.MenuBar): const.ID_PROJECT_CLOSE, const.ID_REORIENT_IMG, const.ID_FLOODFILL_MASK, + const.ID_FILL_HOLE_AUTO, const.ID_REMOVE_MASK_PART, const.ID_SELECT_MASK_PART, const.ID_FLOODFILL_SEGMENTATION,] @@ -746,6 +755,9 @@ class MenuBar(wx.MenuBar): self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill holes manually")) self.fill_hole_mask_menu.Enable(False) + self.fill_hole_auto_menu = mask_menu.Append(const.ID_FILL_HOLE_AUTO, _(u"Fill holes automatically")) + 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) -- libgit2 0.21.2