From ae89189585488735cc15398f0e362c32daf7c038 Mon Sep 17 00:00:00 2001 From: tfmoraes Date: Wed, 10 Oct 2012 20:04:37 +0000 Subject: [PATCH] ENH: Implemented the undo and redo functionality in the edition tool --- invesalius/data/mask.py | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- invesalius/data/slice_.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- invesalius/data/viewer_slice.py | 2 ++ invesalius/gui/frame.py | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 416 insertions(+), 16 deletions(-) diff --git a/invesalius/data/mask.py b/invesalius/data/mask.py index e1f33c7..3a03a8a 100644 --- a/invesalius/data/mask.py +++ b/invesalius/data/mask.py @@ -22,6 +22,7 @@ import plistlib import random import shutil import tempfile +import weakref import numpy import vtk @@ -31,12 +32,155 @@ import imagedata_utils as iu from wx.lib.pubsub import pub as Publisher + +class EditionHistoryNode(object): + def __init__(self, index, orientation, array, clean=False): + self.index = index + self.orientation = orientation + self.filename = tempfile.mktemp(suffix='.npy') + self.clean = clean + + self._save_array(array) + + def _save_array(self, array): + numpy.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) + if self.orientation == 'AXIAL': + mvolume[self.index+1,1:,1:] = array + if self.clean: + mvolume[self.index+1, 0, 0] = 1 + elif self.orientation == 'CORONAL': + mvolume[1:, self.index+1, 1:] = array + if self.clean: + mvolume[0, self.index+1, 0] = 1 + elif self.orientation == 'SAGITAL': + mvolume[1:, 1:, self.index+1] = array + if self.clean: + mvolume[0, 0, self.index+1] = 1 + + print "applying to", self.orientation, "at slice", self.index + + def __del__(self): + print "Removing", self.filename + os.remove(self.filename) + + +class EditionHistory(object): + def __init__(self, size=50): + self.history = [] + self._copies = weakref.WeakValueDictionary() + self.index = -1 + self.size = size + + Publisher.sendMessage("Enable undo", False) + Publisher.sendMessage("Enable redo", False) + + def new_node(self, index, orientation, array, p_array, clean): + try: + p_node = self.history[self.index] + except IndexError: + p_node = None + + if self.index == -1 or (orientation != p_node.orientation or p_node.index != index): + try: + node = self._copies[(orientation, index)] + except KeyError: + node = EditionHistoryNode(index, orientation, p_array, clean) + self._copies[(orientation, index)] = node + self.add(node) + + node = EditionHistoryNode(index, orientation, array, clean) + self._copies[(orientation, index)] = node + self.add(node) + + def add(self, node): + if self.index == self.size: + self.history.pop(0) + self.index -= 1 + + if self.index < len(self.history): + self.history = self.history[:self.index + 1] + self.history.append(node) + self.index += 1 + + print "INDEX", self.index, len(self.history), self.history + Publisher.sendMessage("Enable undo", True) + Publisher.sendMessage("Enable redo", False) + + def undo(self, mvolume, actual_slices=None): + h = self.history + if self.index > 0: + #if self.index > 0 and h[self.index].clean: + ##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: + self._reload_slice(self.index - 1) + else: + self.index -= 1 + h[self.index].commit_history(mvolume) + if actual_slices and self.index and actual_slices[h[self.index - 1].orientation] == h[self.index - 1].index: + self.index -= 1 + h[self.index].commit_history(mvolume) + self._reload_slice(self.index) + Publisher.sendMessage("Enable redo", True) + + if self.index == 0: + Publisher.sendMessage("Enable undo", False) + print "AT", self.index, len(self.history), self.history[self.index].filename + + def redo(self, mvolume, actual_slices=None): + h = self.history + if self.index < len(h) - 1: + #if self.index < len(h) - 1 and h[self.index].clean: + ##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: + self._reload_slice(self.index + 1) + else: + self.index += 1 + h[self.index].commit_history(mvolume) + if actual_slices and self.index < len(h) - 1 and actual_slices[h[self.index + 1].orientation] == h[self.index + 1].index: + self.index += 1 + h[self.index].commit_history(mvolume) + self._reload_slice(self.index) + Publisher.sendMessage("Enable undo", True) + + if self.index == len(h) - 1: + Publisher.sendMessage("Enable redo", False) + print "AT", self.index, len(h), h[self.index].filename + + def _reload_slice(self, index): + Publisher.sendMessage(('Set scroll position', self.history[index].orientation), + self.history[index].index) + + def _config_undo_redo(self, visible): + v_undo = False + v_redo = False + + if self.history and visible: + v_undo = True + v_redo = True + if self.index == 0: + v_undo = False + elif self.index == len(self.history) - 1: + v_redo = False + + Publisher.sendMessage("Enable undo", v_undo) + Publisher.sendMessage("Enable redo", v_redo) + + class Mask(): general_index = -1 def __init__(self): Mask.general_index += 1 self.index = Mask.general_index - self.imagedata = '' # vtkImageData + self.imagedata = '' self.colour = random.choice(const.MASK_COLOUR) self.opacity = const.MASK_OPACITY self.threshold_range = const.THRESHOLD_RANGE @@ -47,10 +191,24 @@ class Mask(): self.was_edited = False self.__bind_events() + self.history = EditionHistory() + def __bind_events(self): Publisher.subscribe(self.OnFlipVolume, 'Flip volume') Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') + def save_history(self, index, orientation, array, p_array, clean=False): + self.history.new_node(index, orientation, array, p_array, clean) + + def undo_history(self, actual_slices): + self.history.undo(self.matrix, actual_slices) + + def redo_history(self, actual_slices): + self.history.redo(self.matrix, actual_slices) + + def on_show(self): + self.history._config_undo_redo(self.is_shown) + def SavePlist(self, dir_temp, filelist): mask = {} filename = u'mask_%d' % self.index @@ -129,3 +287,6 @@ 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) + + def __del__(self): + self.history._config_undo_redo(False) diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index 14d0472..37f50d7 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -17,6 +17,8 @@ # detalhes. #-------------------------------------------------------------------------- import math +import os +import tempfile import numpy import vtk @@ -64,6 +66,9 @@ class SliceBuffer(object): self.vtk_mask = None + + + class Slice(object): __metaclass__= utils.Singleton # Only one slice will be initialized per time (despite several viewers @@ -135,6 +140,9 @@ class Slice(object): Publisher.subscribe(self.OnFlipVolume, 'Flip volume') Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') + + Publisher.subscribe(self.__undo_edition, 'Undo edition') + Publisher.subscribe(self.__redo_edition, 'Redo edition') def GetMaxSliceNumber(self, orientation): shape = self.matrix.shape @@ -290,7 +298,6 @@ class Slice(object): mask = self.buffer_slices[orientation].mask image = self.buffer_slices[orientation].image thresh_min, thresh_max = self.current_mask.edition_threshold_range - self.current_mask.was_edited = True if hasattr(position, '__iter__'): py, px = position @@ -350,12 +357,6 @@ class Slice(object): roi_m = mask[yi:yf,xi:xf] roi_i = image[yi:yf, xi:xf] - print - print"IMAGE", roi_m.shape - print "BRUSH", index.shape - print "IMAGE[BRUSH]", roi_m[index].shape - print - if operation == const.BRUSH_THRESH: # It's a trick to make points between threshold gets value 254 # (1 * 253 + 1) and out ones gets value 1 (0 * 253 + 1). @@ -558,6 +559,8 @@ class Slice(object): print "Showing Mask" proj = Project() proj.mask_dict[index].is_shown = value + proj.mask_dict[index].on_show() + if (index == self.current_mask.index): for buffer_ in self.buffer_slices.values(): buffer_.discard_vtk_mask() @@ -884,12 +887,37 @@ class Slice(object): """ b_mask = self.buffer_slices[orientation].mask index = self.buffer_slices[orientation].index + + # TODO: Voltar a usar marcacao na mascara if orientation == 'AXIAL': + #if self.current_mask.matrix[index+1, 0, 0] != 2: + #self.current_mask.save_history(index, orientation, + #self.current_mask.matrix[index+1,1:,1:], + #clean=True) + p_mask = self.current_mask.matrix[index+1,1:,1:].copy() self.current_mask.matrix[index+1,1:,1:] = b_mask + self.current_mask.matrix[index+1, 0, 0] = 2 + elif orientation == 'CORONAL': + #if self.current_mask.matrix[0, index+1, 0] != 2: + #self.current_mask.save_history(index, orientation, + #self.current_mask.matrix[1:, index+1, 1:], + #clean=True) + p_mask = self.current_mask.matrix[1:, index+1, 1:].copy() self.current_mask.matrix[1:, index+1, 1:] = b_mask + self.current_mask.matrix[0, index+1, 0] = 2 + elif orientation == 'SAGITAL': + #if self.current_mask.matrix[0, 0, index+1] != 2: + #self.current_mask.save_history(index, orientation, + #self.current_mask.matrix[1:, 1:, index+1], + #clean=True) + p_mask = self.current_mask.matrix[1:, 1:, index+1].copy() self.current_mask.matrix[1:, 1:, index+1] = b_mask + self.current_mask.matrix[0, 0, index+1] = 2 + + self.current_mask.save_history(index, orientation, b_mask, p_mask) + self.current_mask.was_edited = True for o in self.buffer_slices: if o != orientation: @@ -897,6 +925,28 @@ class Slice(object): self.buffer_slices[o].discard_vtk_mask() Publisher.sendMessage('Reload actual slice') + def __undo_edition(self, pub_evt): + buffer_slices = self.buffer_slices + actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, + "CORONAL": buffer_slices["CORONAL"].index, + "SAGITAL": buffer_slices["SAGITAL"].index,} + self.current_mask.undo_history(actual_slices) + for o in self.buffer_slices: + self.buffer_slices[o].discard_mask() + self.buffer_slices[o].discard_vtk_mask() + Publisher.sendMessage('Reload actual slice') + + def __redo_edition(self, pub_evt): + buffer_slices = self.buffer_slices + actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, + "CORONAL": buffer_slices["CORONAL"].index, + "SAGITAL": buffer_slices["SAGITAL"].index,} + self.current_mask.redo_history(actual_slices) + for o in self.buffer_slices: + self.buffer_slices[o].discard_mask() + self.buffer_slices[o].discard_vtk_mask() + Publisher.sendMessage('Reload actual slice') + def __build_mask(self, imagedata, create=True): # create new mask instance and insert it into project if create: diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index 9f9229b..6f37c06 100755 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -20,6 +20,7 @@ #-------------------------------------------------------------------------- import itertools +import tempfile import numpy @@ -803,6 +804,7 @@ class Viewer(wx.Panel): def OnBrushRelease(self, evt, obj): if (self.slice_.buffer_slices[self.orientation].mask is None): return + self.slice_.apply_slice_buffer_to_mask(self.orientation) self._flush_buffer = False diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 6363156..5b3bbfc 100755 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -165,11 +165,13 @@ class Frame(wx.Frame): t2 = LayoutToolBar(self) t3 = ObjectToolBar(self) t4 = SliceToolBar(self) + t5 = HistoryToolBar(self) else: - t4 = ProjectToolBar(self) - t3 = LayoutToolBar(self) - t2 = ObjectToolBar(self) - t1 = SliceToolBar(self) + t5 = ProjectToolBar(self) + t4 = LayoutToolBar(self) + t3 = ObjectToolBar(self) + t2 = SliceToolBar(self) + t1 = HistoryToolBar(self) aui_manager.AddPane(t1, wx.aui.AuiPaneInfo(). Name("General Features Toolbar"). @@ -191,6 +193,11 @@ class Frame(wx.Frame): ToolbarPane().Top().Floatable(False). LeftDockable(False).RightDockable(False)) + aui_manager.AddPane(t5, wx.aui.AuiPaneInfo(). + Name("Slice Toolbar"). + ToolbarPane().Top().Floatable(False). + LeftDockable(False).RightDockable(False)) + aui_manager.Update() self.aui_manager = aui_manager @@ -358,6 +365,11 @@ class Frame(wx.Frame): const.ID_SWAP_XZ: (2, 0), const.ID_SWAP_YZ: (1, 0)}[id] self.SwapAxes(axes) + elif id == wx.ID_UNDO: + self.OnUndo() + elif id == wx.ID_REDO: + self.OnRedo() + def OnSize(self, evt): """ Refresh GUI when frame is resized. @@ -436,6 +448,13 @@ class Frame(wx.Frame): Publisher.sendMessage('Update scroll') Publisher.sendMessage('Reload actual slice') + def OnUndo(self): + print "Undo" + Publisher.sendMessage('Undo edition') + + def OnRedo(self): + print "Redo" + Publisher.sendMessage('Redo edition') @@ -476,6 +495,8 @@ class MenuBar(wx.MenuBar): # mail list in Oct 20 2008 sub = Publisher.subscribe sub(self.OnEnableState, "Enable state project") + sub(self.OnEnableUndo, "Enable undo") + sub(self.OnEnableRedo, "Enable redo") def __init_items(self): """ @@ -523,11 +544,10 @@ class MenuBar(wx.MenuBar): app(const.ID_SWAP_YZ, _("A-P <-> T-B")) file_edit = wx.Menu() - app = file_edit.Append file_edit.AppendMenu(wx.NewId(), _('Flip'), flip_menu) file_edit.AppendMenu(wx.NewId(), _('Swap axes'), swap_axes_menu) - #app(wx.ID_UNDO, "Undo\tCtrl+Z") - #app(wx.ID_REDO, "Redo\tCtrl+Y") + file_edit.Append(wx.ID_UNDO, "Undo\tCtrl+Z").Enable(False) + file_edit.Append(wx.ID_REDO, "Redo\tCtrl+Y").Enable(False) #app(const.ID_EDIT_LIST, "Show Undo List...") ################################################################# @@ -578,7 +598,7 @@ class MenuBar(wx.MenuBar): # Add all menus to menubar self.Append(file_menu, _("File")) - #self.Append(file_edit, _("Edit")) + self.Append(file_edit, _("Edit")) #self.Append(view_menu, "View") #self.Append(tools_menu, "Tools") self.Append(options_menu, _("Options")) @@ -609,6 +629,19 @@ class MenuBar(wx.MenuBar): for item in self.enable_items: self.Enable(item, True) + def OnEnableUndo(self, pubsub_evt): + value = pubsub_evt.data + if value: + self.FindItemById(wx.ID_UNDO).Enable(True) + else: + self.FindItemById(wx.ID_UNDO).Enable(False) + + def OnEnableRedo(self, pubsub_evt): + value = pubsub_evt.data + if value: + self.FindItemById(wx.ID_REDO).Enable(True) + else: + self.FindItemById(wx.ID_REDO).Enable(False) # ------------------------------------------------------------------ # ------------------------------------------------------------------ # ------------------------------------------------------------------ @@ -1397,12 +1430,166 @@ class LayoutToolBar(wx.ToolBar): self.ontool_text = True +class HistoryToolBar(wx.ToolBar): + """ + Toolbar related to general layout/ visualization configuration + e.g: show/hide task panel and show/hide text on viewers. + """ + def __init__(self, parent): + style = wx.TB_FLAT|wx.TB_NODIVIDER | wx.TB_DOCKABLE + wx.ToolBar.__init__(self, parent, -1, wx.DefaultPosition, + wx.DefaultSize, + style) + self.SetToolBitmapSize(wx.Size(32,32)) + self.parent = parent + self.__init_items() + self.__bind_events() + self.__bind_events_wx() + self.ontool_layout = False + self.ontool_text = True + #self.enable_items = [ID_TEXT] + self.Realize() + #self.SetStateProjectClose() + def __bind_events(self): + """ + Bind events related to pubsub. + """ + sub = Publisher.subscribe + #sub(self._EnableState, "Enable state project") + #sub(self._SetLayoutWithTask, "Set layout button data only") + #sub(self._SetLayoutWithoutTask, "Set layout button full") + sub(self.OnEnableUndo, "Enable undo") + sub(self.OnEnableRedo, "Enable redo") + def __bind_events_wx(self): + """ + Bind normal events from wx (except pubsub related). + """ + #self.Bind(wx.EVT_TOOL, self.OnToggle) + wx.EVT_TOOL( self, wx.ID_UNDO, self.OnUndo ) + wx.EVT_TOOL( self, wx.ID_REDO, self.OnRedo ) + def __init_items(self): + """ + Add tools into toolbar. + """ + self.AddSimpleTool(wx.ID_UNDO, wx.ArtProvider_GetBitmap(wx.ART_UNDO, wx.ART_OTHER, wx.Size( 16, 16)), 'Undo', '') + self.AddSimpleTool(wx.ID_REDO, wx.ArtProvider_GetBitmap(wx.ART_REDO, wx.ART_OTHER, wx.Size( 16, 16)), 'Redo', '') + self.EnableTool(wx.ID_UNDO, False) + self.EnableTool(wx.ID_REDO, False) + def _EnableState(self, pubsub_evt): + """ + Based on given state, enable or disable menu items which + depend if project is open or not. + """ + state = pubsub_evt.data + if state: + self.SetStateProjectOpen() + else: + self.SetStateProjectClose() + def _SetLayoutWithoutTask(self, pubsub_evt): + """ + Set item bitmap to task panel hiden. + """ + self.SetToolNormalBitmap(ID_LAYOUT,self.BMP_WITHOUT_MENU) + + def _SetLayoutWithTask(self, pubsub_evt): + """ + Set item bitmap to task panel shown. + """ + self.SetToolNormalBitmap(ID_LAYOUT,self.BMP_WITH_MENU) + + def OnUndo(self, event): + print "Undo" + Publisher.sendMessage('Undo edition') + + def OnRedo(self, event): + print "Redo" + Publisher.sendMessage('Redo edition') + + def OnToggle(self, event): + """ + Update status of toolbar item (bitmap and help) + """ + id = event.GetId() + if id == ID_LAYOUT: + self.ToggleLayout() + elif id== ID_TEXT: + self.ToggleText() + + for item in VIEW_TOOLS: + state = self.GetToolState(item) + if state and (item != id): + self.ToggleTool(item, False) + + def SetStateProjectClose(self): + """ + Disable menu items (e.g. text) when project is closed. + """ + self.ontool_text = True + self.ToggleText() + for tool in self.enable_items: + self.EnableTool(tool, False) + + def SetStateProjectOpen(self): + """ + Disable menu items (e.g. text) when project is closed. + """ + self.ontool_text = False + self.ToggleText() + for tool in self.enable_items: + self.EnableTool(tool, True) + + def ToggleLayout(self): + """ + Based on previous layout item state, toggle it. + """ + if self.ontool_layout: + self.SetToolNormalBitmap(ID_LAYOUT,self.BMP_WITHOUT_MENU) + Publisher.sendMessage('Show task panel') + self.SetToolShortHelp(ID_LAYOUT,_("Hide task panel")) + self.ontool_layout = False + else: + self.bitmap = self.BMP_WITH_MENU + self.SetToolNormalBitmap(ID_LAYOUT,self.BMP_WITH_MENU) + Publisher.sendMessage('Hide task panel') + self.SetToolShortHelp(ID_LAYOUT, _("Show task panel")) + self.ontool_layout = True + + def ToggleText(self): + """ + Based on previous text item state, toggle it. + """ + if self.ontool_text: + self.SetToolNormalBitmap(ID_TEXT,self.BMP_WITH_TEXT) + Publisher.sendMessage('Hide text actors on viewers') + self.SetToolShortHelp(ID_TEXT,_("Show text")) + Publisher.sendMessage('Update AUI') + self.ontool_text = False + else: + self.SetToolNormalBitmap(ID_TEXT, self.BMP_WITHOUT_TEXT) + Publisher.sendMessage('Show text actors on viewers') + self.SetToolShortHelp(ID_TEXT,_("Hide text")) + Publisher.sendMessage('Update AUI') + self.ontool_text = True + + def OnEnableUndo(self, pubsub_evt): + value = pubsub_evt.data + if value: + self.EnableTool(wx.ID_UNDO, True) + else: + self.EnableTool(wx.ID_UNDO, False) + + def OnEnableRedo(self, pubsub_evt): + value = pubsub_evt.data + if value: + self.EnableTool(wx.ID_REDO, True) + else: + self.EnableTool(wx.ID_REDO, False) -- libgit2 0.21.2