diff --git a/app.py b/app.py index 795684f..dc25472 100644 --- a/app.py +++ b/app.py @@ -312,6 +312,8 @@ def parse_comand_line(): parser.add_option("--import-all", action="store") + parser.add_option("--import-folder", action="store", dest="import_folder") + parser.add_option("-s", "--save", help="Save the project after an import.") @@ -354,6 +356,13 @@ def use_cmd_optargs(options, args): check_for_export(options) return True + elif options.import_folder: + Publisher.sendMessage('Import folder', folder=options.import_folder) + if options.save: + Publisher.sendMessage('Save project', filepath=os.path.abspath(options.save)) + exit(0) + check_for_export(options) + elif options.import_all: import invesalius.reader.dicom_reader as dcm for patient in dcm.GetDicomGroups(options.import_all): diff --git a/invesalius/constants.py b/invesalius/constants.py index 82497ee..257e475 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -486,6 +486,7 @@ ID_ABOUT = wx.ID_ABOUT [wx.NewId() for number in range(3)] ID_START = wx.NewId() +ID_PLUGINS_SHOW_PATH = wx.NewId() ID_FLIP_X = wx.NewId() ID_FLIP_Y = wx.NewId() diff --git a/invesalius/control.py b/invesalius/control.py index b4f3387..2c9211f 100644 --- a/invesalius/control.py +++ b/invesalius/control.py @@ -18,7 +18,12 @@ #-------------------------------------------------------------------------- import os import plistlib +import tempfile +import textwrap + import wx +import numpy as np + from wx.lib.pubsub import pub as Publisher import invesalius.constants as const @@ -43,6 +48,7 @@ import subprocess import sys from invesalius import inv_paths +from invesalius import plugins DEFAULT_THRESH_MODE = 0 @@ -51,6 +57,7 @@ class Controller(): def __init__(self, frame): self.surface_manager = srf.SurfaceManager() self.volume = volume.Volume() + self.plugin_manager = plugins.PluginManager() self.__bind_events() self.frame = frame self.progress_dialog = None @@ -69,9 +76,12 @@ class Controller(): Publisher.sendMessage('Load Preferences') + self.plugin_manager.find_plugins() + def __bind_events(self): Publisher.subscribe(self.OnImportMedicalImages, 'Import directory') Publisher.subscribe(self.OnImportGroup, 'Import group') + Publisher.subscribe(self.OnImportFolder, 'Import folder') Publisher.subscribe(self.OnShowDialogImportDirectory, 'Show import directory dialog') Publisher.subscribe(self.OnShowDialogImportOtherFiles, @@ -113,6 +123,8 @@ class Controller(): Publisher.subscribe(self.Send_affine, 'Get affine matrix') + Publisher.subscribe(self.create_project_from_matrix, 'Create project from matrix') + def SetBitmapSpacing(self, spacing): proj = prj.Project() proj.spacing = spacing @@ -492,7 +504,32 @@ class Controller(): self.LoadProject() Publisher.sendMessage("Enable state project", state=True) - #------------------------------------------------------------------------------------- + def OnImportFolder(self, folder): + Publisher.sendMessage('Begin busy cursor') + folder = os.path.abspath(folder) + + proj = prj.Project() + proj.load_from_folder(folder) + + self.Slice = sl.Slice() + self.Slice._open_image_matrix(proj.matrix_filename, + tuple(proj.matrix_shape), + proj.matrix_dtype) + + self.Slice.window_level = proj.level + self.Slice.window_width = proj.window + + Publisher.sendMessage('Update threshold limits list', + threshold_range=proj.threshold_range) + + session = ses.Session() + filename = proj.name+".inv3" + filename = filename.replace("/", "") #Fix problem case other/Skull_DICOM + dirpath = session.CreateProject(filename) + self.LoadProject() + Publisher.sendMessage("Enable state project", state=True) + + Publisher.sendMessage('End busy cursor') def LoadProject(self): proj = prj.Project() @@ -673,6 +710,83 @@ class Controller(): dirpath = session.CreateProject(filename) + + def create_project_from_matrix(self, name, matrix, orientation="AXIAL", spacing=(1.0, 1.0, 1.0), modality="CT", window_width=None, window_level=None, new_instance=False): + """ + Creates a new project from a Numpy 3D array. + + name: Name of the project. + matrix: A Numpy 3D array. It only works with int16 arrays. + spacing: The spacing between the center of the voxels in X, Y and Z direction. + modality: Imaging modality. + """ + if window_width is None: + window_width = (matrix.max() - matrix.min()) + if window_level is None: + window_level = (matrix.max() + matrix.min()) // 2 + + window_width = int(window_width) + window_level = int(window_level) + + name_to_const = {"AXIAL": const.AXIAL, + "CORONAL": const.CORONAL, + "SAGITTAL": const.SAGITAL} + + if new_instance: + self.start_new_inv_instance(matrix, name, spacing, modality, name_to_const[orientation], window_width, window_level) + else: + # Verifying if there is a project open + s = ses.Session() + if s.IsOpen(): + Publisher.sendMessage('Close Project') + Publisher.sendMessage('Disconnect tracker') + + # Check if user really closed the project, if not, stop project creation + if s.IsOpen(): + return + + mmap_matrix = image_utils.array2memmap(matrix) + + self.Slice = sl.Slice() + self.Slice.matrix = mmap_matrix + self.Slice.matrix_filename = mmap_matrix.filename + self.Slice.spacing = spacing + + self.Slice.window_width = window_width + self.Slice.window_level = window_level + + proj = prj.Project() + proj.name = name + proj.modality = modality + proj.SetAcquisitionModality(modality) + proj.matrix_shape = matrix.shape + proj.matrix_dtype = matrix.dtype.name + proj.matrix_filename = self.Slice.matrix_filename + proj.window = window_width + proj.level = window_level + + + proj.original_orientation =\ + name_to_const[orientation] + + proj.threshold_range = int(matrix.min()), int(matrix.max()) + proj.spacing = self.Slice.spacing + + Publisher.sendMessage('Update threshold limits list', + threshold_range=proj.threshold_range) + + ###### + session = ses.Session() + filename = proj.name + ".inv3" + + filename = filename.replace("/", "") + + dirpath = session.CreateProject(filename) + + self.LoadProject() + Publisher.sendMessage("Enable state project", state=True) + + def OnOpenBitmapFiles(self, rec_data): bmp_data = bmp.BitmapData() @@ -954,3 +1068,24 @@ class Controller(): def ApplyReorientation(self): self.Slice.apply_reorientation() + + def start_new_inv_instance(self, image, name, spacing, modality, orientation, window_width, window_level): + p = prj.Project() + project_folder = tempfile.mkdtemp() + p.create_project_file(name, spacing, modality, orientation, window_width, window_level, image, folder=project_folder) + err_msg = '' + try: + sp = subprocess.Popen([sys.executable, sys.argv[0], '--import-folder', project_folder], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=os.getcwd()) + except Exception as err: + err_msg = str(err) + else: + try: + if sp.wait(2): + err_msg = sp.stderr.read().decode('utf8') + sp.terminate() + except subprocess.TimeoutExpired: + pass + + if err_msg: + dialog.MessageBox(None, "It was not possible to launch new instance of InVesalius3 dsfa dfdsfa sdfas fdsaf asdfasf dsaa", err_msg) diff --git a/invesalius/data/imagedata_utils.py b/invesalius/data/imagedata_utils.py index cc2d59c..2fd9acb 100644 --- a/invesalius/data/imagedata_utils.py +++ b/invesalius/data/imagedata_utils.py @@ -288,6 +288,16 @@ def create_dicom_thumbnails(image, window=None, level=None): return thumbnail_path + +def array2memmap(arr, filename=None): + if filename is None: + filename = tempfile.mktemp(prefix='inv3_', suffix='.dat') + matrix = numpy.memmap(filename, mode='w+', dtype=arr.dtype, shape=arr.shape) + matrix[:] = arr[:] + matrix.flush() + return matrix + + def bitmap2memmap(files, slice_size, orientation, spacing, resolution_percentage): """ From a list of dicom files it creates memmap file in the temp folder and diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index a6ac533..33096c8 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -1,10 +1,10 @@ -#-------------------------------------------------------------------------- +# -------------------------------------------------------------------------- # Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas # Copyright: (C) 2001 Centro de Pesquisas Renato Archer # Homepage: http://www.softwarepublico.gov.br # Contact: invesalius@cti.gov.br # License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) -#-------------------------------------------------------------------------- +# -------------------------------------------------------------------------- # Este programa e software livre; voce pode redistribui-lo e/ou # modifica-lo sob os termos da Licenca Publica Geral GNU, conforme # publicada pela Free Software Foundation; de acordo com a versao 2 @@ -15,40 +15,38 @@ # COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM # PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais # detalhes. -#-------------------------------------------------------------------------- -from six import with_metaclass - +# -------------------------------------------------------------------------- import os import tempfile import numpy as np import vtk - from scipy import ndimage +from six import with_metaclass from wx.lib.pubsub import pub as Publisher import invesalius.constants as const import invesalius.data.converters as converters import invesalius.data.imagedata_utils as iu -import invesalius.style as st +import invesalius.data.transformations as transformations import invesalius.session as ses +import invesalius.style as st import invesalius.utils as utils +from invesalius.data import mips, transforms from invesalius.data.mask import Mask from invesalius.project import Project -from invesalius.data import mips -from invesalius.data import transforms -import invesalius.data.transformations as transformations -OTHER=0 -PLIST=1 -WIDGET=2 +OTHER = 0 +PLIST = 1 +WIDGET = 2 class SliceBuffer(object): - """ + """ This class is used as buffer that mantains the vtkImageData and numpy array from actual slices from each orientation. """ + def __init__(self): self.index = -1 self.image = None @@ -86,9 +84,10 @@ class Slice(with_metaclass(utils.Singleton, object)): self.histogram = None self._matrix = None self.aux_matrices = {} + self.aux_matrices_colours = {} self.state = const.STATE_DEFAULT - self.to_show_aux = '' + self.to_show_aux = "" self._type_projection = const.PROJECTION_NORMAL self.n_border = const.PROJECTION_BORDER_SIZE @@ -105,9 +104,11 @@ class Slice(with_metaclass(utils.Singleton, object)): self.hue_range = (0, 0) self.value_range = (0, 1) - self.buffer_slices = {"AXIAL": SliceBuffer(), - "CORONAL": SliceBuffer(), - "SAGITAL": SliceBuffer()} + self.buffer_slices = { + "AXIAL": SliceBuffer(), + "CORONAL": SliceBuffer(), + "SAGITAL": SliceBuffer(), + } self.num_gradient = 0 self.interaction_style = st.StyleStateManager() @@ -129,7 +130,9 @@ class Slice(with_metaclass(utils.Singleton, object)): i, e = value.min(), value.max() r = int(e) - int(i) self.histogram = np.histogram(self._matrix, r, (i, e))[0] - self.center = [(s * d/2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing)] + self.center = [ + (s * d / 2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing) + ] @property def spacing(self): @@ -138,85 +141,93 @@ class Slice(with_metaclass(utils.Singleton, object)): @spacing.setter def spacing(self, value): self._spacing = value - self.center = [(s * d/2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing)] + self.center = [ + (s * d / 2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing) + ] def __bind_events(self): # General slice control - Publisher.subscribe(self.CreateSurfaceFromIndex, - 'Create surface from index') + Publisher.subscribe(self.CreateSurfaceFromIndex, "Create surface from index") # Mask control - Publisher.subscribe(self.__add_mask_thresh, 'Create new mask') - Publisher.subscribe(self.__select_current_mask, - 'Change mask selected') + Publisher.subscribe(self.__add_mask_thresh, "Create new mask") + Publisher.subscribe(self.__select_current_mask, "Change mask selected") # Mask properties - Publisher.subscribe(self.__set_current_mask_edition_threshold, - 'Set edition threshold values') - Publisher.subscribe(self.__set_current_mask_threshold, - 'Set threshold values') - Publisher.subscribe(self.__set_current_mask_threshold_actual_slice, - 'Changing threshold values') - Publisher.subscribe(self.__set_current_mask_colour, - 'Change mask colour') - Publisher.subscribe(self.__set_mask_name, 'Change mask name') - Publisher.subscribe(self.__show_mask, 'Show mask') - Publisher.subscribe(self.__hide_current_mask, 'Hide current mask') - Publisher.subscribe(self.__show_current_mask, 'Show current mask') - Publisher.subscribe(self.__clean_current_mask, 'Clean current mask') + Publisher.subscribe( + self.__set_current_mask_edition_threshold, "Set edition threshold values" + ) + Publisher.subscribe(self.__set_current_mask_threshold, "Set threshold values") + Publisher.subscribe( + self.__set_current_mask_threshold_actual_slice, "Changing threshold values" + ) + Publisher.subscribe(self.__set_current_mask_colour, "Change mask colour") + Publisher.subscribe(self.__set_mask_name, "Change mask name") + Publisher.subscribe(self.__show_mask, "Show mask") + Publisher.subscribe(self.__hide_current_mask, "Hide current mask") + Publisher.subscribe(self.__show_current_mask, "Show current mask") + Publisher.subscribe(self.__clean_current_mask, "Clean current mask") - Publisher.subscribe(self.__export_slice, 'Export slice') - Publisher.subscribe(self.__export_actual_mask, 'Export actual mask') + Publisher.subscribe(self.__export_slice, "Export slice") + Publisher.subscribe(self.__export_actual_mask, "Export actual mask") - Publisher.subscribe(self.__set_current_mask_threshold_limits, - 'Update threshold limits') + Publisher.subscribe( + self.__set_current_mask_threshold_limits, "Update threshold limits" + ) - Publisher.subscribe(self.UpdateWindowLevelBackground,\ - 'Bright and contrast adjustment image') + Publisher.subscribe( + self.UpdateWindowLevelBackground, "Bright and contrast adjustment image" + ) - Publisher.subscribe(self.UpdateColourTableBackground,\ - 'Change colour table from background image') + Publisher.subscribe( + self.UpdateColourTableBackground, + "Change colour table from background image", + ) - Publisher.subscribe(self.UpdateColourTableBackgroundPlist,\ - 'Change colour table from background image from plist') + Publisher.subscribe( + self.UpdateColourTableBackgroundPlist, + "Change colour table from background image from plist", + ) - Publisher.subscribe(self.UpdateColourTableBackgroundWidget,\ - 'Change colour table from background image from widget') + Publisher.subscribe( + self.UpdateColourTableBackgroundWidget, + "Change colour table from background image from widget", + ) - Publisher.subscribe(self._set_projection_type, 'Set projection type') + Publisher.subscribe(self._set_projection_type, "Set projection type") - Publisher.subscribe(self._do_boolean_op, 'Do boolean operation') + Publisher.subscribe(self._do_boolean_op, "Do boolean operation") - Publisher.subscribe(self.OnExportMask,'Export mask to file') + Publisher.subscribe(self.OnExportMask, "Export mask to file") - Publisher.subscribe(self.OnCloseProject, 'Close project data') + Publisher.subscribe(self.OnCloseProject, "Close project data") - Publisher.subscribe(self.OnEnableStyle, 'Enable style') - Publisher.subscribe(self.OnDisableStyle, 'Disable style') - Publisher.subscribe(self.OnDisableActualStyle, 'Disable actual style') + Publisher.subscribe(self.OnEnableStyle, "Enable style") + Publisher.subscribe(self.OnDisableStyle, "Disable style") + Publisher.subscribe(self.OnDisableActualStyle, "Disable actual style") - Publisher.subscribe(self.OnRemoveMasks, 'Remove masks') - Publisher.subscribe(self.OnDuplicateMasks, 'Duplicate masks') - Publisher.subscribe(self.UpdateSlice3D,'Update slice 3D') + Publisher.subscribe(self.OnRemoveMasks, "Remove masks") + Publisher.subscribe(self.OnDuplicateMasks, "Duplicate masks") + Publisher.subscribe(self.UpdateSlice3D, "Update slice 3D") - Publisher.subscribe(self.OnFlipVolume, 'Flip volume') - Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') + 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') + Publisher.subscribe(self.__undo_edition, "Undo edition") + Publisher.subscribe(self.__redo_edition, "Redo edition") - Publisher.subscribe(self._fill_holes_auto, 'Fill holes automatically') + Publisher.subscribe(self._fill_holes_auto, "Fill holes automatically") - Publisher.subscribe(self._set_interpolation_method, 'Set interpolation method') + Publisher.subscribe(self._set_interpolation_method, "Set interpolation method") def GetMaxSliceNumber(self, orientation): shape = self.matrix.shape # Because matrix indexing starts with 0 so the last slice is the shape # minu 1. - if orientation == 'AXIAL': + if orientation == "AXIAL": return shape[0] - 1 - elif orientation == 'CORONAL': + elif orientation == "CORONAL": return shape[1] - 1 - elif orientation == 'SAGITAL': + elif orientation == "SAGITAL": return shape[2] - 1 def discard_all_buffers(self): @@ -233,13 +244,13 @@ class Slice(with_metaclass(utils.Singleton, object)): # and discard from buffer all datas related to mask. if self.current_mask is not None and item == self.current_mask.index: self.current_mask = None - + for buffer_ in self.buffer_slices.values(): buffer_.discard_vtk_mask() buffer_.discard_mask() - Publisher.sendMessage('Show mask', index=item, value=False) - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Show mask", index=item, value=False) + Publisher.sendMessage("Reload actual slice") def OnDuplicateMasks(self, mask_indexes): proj = Project() @@ -255,28 +266,28 @@ class Slice(with_metaclass(utils.Singleton, object)): self._add_mask_into_proj(copy_mask) def OnEnableStyle(self, style): - if (style in const.SLICE_STYLES): + if style in const.SLICE_STYLES: new_state = self.interaction_style.AddState(style) - Publisher.sendMessage('Set slice interaction style', style=new_state) + Publisher.sendMessage("Set slice interaction style", style=new_state) self.state = style def OnDisableStyle(self, style): - if (style in const.SLICE_STYLES): + if style in const.SLICE_STYLES: new_state = self.interaction_style.RemoveState(style) - Publisher.sendMessage('Set slice interaction style', style=new_state) + Publisher.sendMessage("Set slice interaction style", style=new_state) - if (style == const.SLICE_STATE_EDITOR): - Publisher.sendMessage('Set interactor default cursor') + if style == const.SLICE_STATE_EDITOR: + Publisher.sendMessage("Set interactor default cursor") self.state = new_state def OnDisableActualStyle(self): actual_state = self.interaction_style.GetActualState() if actual_state != const.STATE_DEFAULT: new_state = self.interaction_style.RemoveState(actual_state) - Publisher.sendMessage('Set slice interaction style', style=new_state) + Publisher.sendMessage("Set slice interaction style", style=new_state) # if (actual_state == const.SLICE_STATE_EDITOR): - # Publisher.sendMessage('Set interactor default cursor') + # Publisher.sendMessage('Set interactor default cursor') self.state = new_state def OnCloseProject(self): @@ -285,7 +296,7 @@ class Slice(with_metaclass(utils.Singleton, object)): def CloseProject(self): f = self._matrix.filename self._matrix._mmap.close() - self._matrix = None + self._matrix = None os.remove(f) self.current_mask = None @@ -299,7 +310,7 @@ class Slice(with_metaclass(utils.Singleton, object)): self.values = None self.nodes = None - self.from_= OTHER + self.from_ = OTHER self.state = const.STATE_DEFAULT self.number_of_colours = 256 @@ -309,7 +320,7 @@ class Slice(with_metaclass(utils.Singleton, object)): self.interaction_style.Reset() - Publisher.sendMessage('Select first item from slice menu') + Publisher.sendMessage("Select first item from slice menu") def __set_current_mask_threshold_limits(self, threshold_range): thresh_min = threshold_range[0] @@ -326,11 +337,11 @@ class Slice(with_metaclass(utils.Singleton, object)): self.create_new_mask(name=mask_name, threshold_range=thresh, colour=colour) self.SetMaskColour(self.current_mask.index, self.current_mask.colour) self.SelectCurrentMask(self.current_mask.index) - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def __select_current_mask(self, index): self.SelectCurrentMask(index) - + def __set_current_mask_edition_threshold(self, threshold_range): if self.current_mask: index = self.current_mask.index @@ -347,9 +358,12 @@ class Slice(with_metaclass(utils.Singleton, object)): to_reload = True for orientation in self.buffer_slices: self.buffer_slices[orientation].discard_vtk_mask() - self.SetMaskThreshold(index, threshold_range, - self.buffer_slices[orientation].index, - orientation) + self.SetMaskThreshold( + index, + threshold_range, + self.buffer_slices[orientation].index, + orientation, + ) # TODO: merge this code with apply_slice_buffer_to_mask b_mask = self.buffer_slices["AXIAL"].mask @@ -371,24 +385,27 @@ class Slice(with_metaclass(utils.Singleton, object)): self.current_mask.matrix[0, 0, n] = 1 if to_reload: - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def __set_current_mask_threshold_actual_slice(self, threshold_range): index = self.current_mask.index for orientation in self.buffer_slices: self.buffer_slices[orientation].discard_vtk_mask() - self.SetMaskThreshold(index, threshold_range, - self.buffer_slices[orientation].index, - orientation) + self.SetMaskThreshold( + index, + threshold_range, + self.buffer_slices[orientation].index, + orientation, + ) self.num_gradient += 1 - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def __set_current_mask_colour(self, colour): # "if" is necessary because wx events are calling this before any mask # has been created if self.current_mask: - colour_vtk = [c/255.0 for c in colour] + colour_vtk = [c / 255.0 for c in colour] self.SetMaskColour(self.current_mask.index, colour_vtk) def __set_mask_name(self, index, name): @@ -400,23 +417,23 @@ class Slice(with_metaclass(utils.Singleton, object)): if self.current_mask: self.ShowMask(index, value) if not value: - Publisher.sendMessage('Select mask name in combo', index=-1) + Publisher.sendMessage("Select mask name in combo", index=-1) if self._type_projection != const.PROJECTION_NORMAL: self.SetTypeProjection(const.PROJECTION_NORMAL) - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def __hide_current_mask(self): if self.current_mask: index = self.current_mask.index value = False - Publisher.sendMessage('Show mask', index=index, value=value) + Publisher.sendMessage("Show mask", index=index, value=value) def __show_current_mask(self): if self.current_mask: index = self.current_mask.index value = True - Publisher.sendMessage('Show mask', index=index, value=value) + Publisher.sendMessage("Show mask", index=index, value=value) def __clean_current_mask(self): if self.current_mask: @@ -433,25 +450,27 @@ class Slice(with_metaclass(utils.Singleton, object)): def __export_slice(self, filename): import h5py - f = h5py.File(filename, 'w') - f['data'] = self.matrix - f['spacing'] = self.spacing + + f = h5py.File(filename, "w") + f["data"] = self.matrix + f["spacing"] = self.spacing f.flush() f.close() def __export_actual_mask(self, filename): import h5py - f = h5py.File(filename, 'w') + + f = h5py.File(filename, "w") self.do_threshold_to_all_slices() - f['data'] = self.current_mask.matrix[1:, 1:, 1:] - f['spacing'] = self.spacing + f["data"] = self.current_mask.matrix[1:, 1:, 1:] + f["spacing"] = self.spacing f.flush() f.close() def create_temp_mask(self): temp_file = tempfile.mktemp() shape = self.matrix.shape - matrix = np.memmap(temp_file, mode='w+', dtype='uint8', shape=shape) + matrix = np.memmap(temp_file, mode="w+", dtype="uint8", shape=shape) return temp_file, matrix def edit_mask_pixel(self, operation, index, position, radius, orientation): @@ -459,32 +478,30 @@ class Slice(with_metaclass(utils.Singleton, object)): image = self.buffer_slices[orientation].image thresh_min, thresh_max = self.current_mask.edition_threshold_range - print("Threshold", thresh_min, thresh_max) - - if hasattr(position, '__iter__'): + if hasattr(position, "__iter__"): px, py = position - if orientation == 'AXIAL': + if orientation == "AXIAL": sx = self.spacing[0] sy = self.spacing[1] - elif orientation == 'CORONAL': + elif orientation == "CORONAL": sx = self.spacing[0] sy = self.spacing[2] - elif orientation == 'SAGITAL': + elif orientation == "SAGITAL": sx = self.spacing[2] sy = self.spacing[1] else: - if orientation == 'AXIAL': + if orientation == "AXIAL": sx = self.spacing[0] sy = self.spacing[1] py = position / mask.shape[1] px = position % mask.shape[1] - elif orientation == 'CORONAL': + elif orientation == "CORONAL": sx = self.spacing[0] sy = self.spacing[2] py = position / mask.shape[1] px = position % mask.shape[1] - elif orientation == 'SAGITAL': + elif orientation == "SAGITAL": sx = self.spacing[2] sy = self.spacing[1] py = position / mask.shape[1] @@ -498,26 +515,26 @@ class Slice(with_metaclass(utils.Singleton, object)): yf = int(yi + index.shape[0]) if yi < 0: - index = index[abs(yi):,:] + index = index[abs(yi) :, :] yi = 0 if yf > image.shape[0]: - index = index[:index.shape[0]-(yf-image.shape[0]), :] + index = index[: index.shape[0] - (yf - image.shape[0]), :] yf = image.shape[0] if xi < 0: - index = index[:,abs(xi):] + index = index[:, abs(xi) :] xi = 0 if xf > image.shape[1]: - index = index[:,:index.shape[1]-(xf-image.shape[1])] + index = index[:, : index.shape[1] - (xf - image.shape[1])] xf = image.shape[1] # Verifying if the points is over the image array. - if (not 0 <= xi <= image.shape[1] and not 0 <= xf <= image.shape[1]) or \ - (not 0 <= yi <= image.shape[0] and not 0 <= yf <= image.shape[0]): + if (not 0 <= xi <= image.shape[1] and not 0 <= xf <= image.shape[1]) or ( + not 0 <= yi <= image.shape[0] and not 0 <= yf <= image.shape[0] + ): return - - roi_m = mask[yi:yf,xi:xf] + roi_m = mask[yi:yf, xi:xf] roi_i = image[yi:yf, xi:xf] # Checking if roi_i has at least one element. @@ -525,11 +542,13 @@ class Slice(with_metaclass(utils.Singleton, object)): 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). - roi_m[index] = (((roi_i[index] >= thresh_min) - & (roi_i[index] <= thresh_max)) * 253) + 1 + roi_m[index] = ( + ((roi_i[index] >= thresh_min) & (roi_i[index] <= thresh_max)) * 253 + ) + 1 elif operation == const.BRUSH_THRESH_ERASE: - roi_m[index] = (((roi_i[index] < thresh_min) - | (roi_i[index] > thresh_max)) * 253) + 1 + roi_m[index] = ( + ((roi_i[index] < thresh_min) | (roi_i[index] > thresh_max)) * 253 + ) + 1 elif operation == const.BRUSH_THRESH_ADD_ONLY: roi_m[((index) & (roi_i >= thresh_min) & (roi_i <= thresh_max))] = 254 elif operation == const.BRUSH_THRESH_ERASE_ONLY: @@ -544,18 +563,22 @@ class Slice(with_metaclass(utils.Singleton, object)): session = ses.Session() session.ChangeProject() - - def GetSlices(self, orientation, slice_number, number_slices, - inverted=False, border_size=1.0): - if self.buffer_slices[orientation].index == slice_number and \ - self._type_projection == const.PROJECTION_NORMAL: + def GetSlices( + self, orientation, slice_number, number_slices, inverted=False, border_size=1.0 + ): + if ( + self.buffer_slices[orientation].index == slice_number + and self._type_projection == const.PROJECTION_NORMAL + ): if self.buffer_slices[orientation].vtk_image: image = self.buffer_slices[orientation].vtk_image else: - n_image = self.get_image_slice(orientation, slice_number, - number_slices, inverted, - border_size) - image = converters.to_vtk(n_image, self.spacing, slice_number, orientation) + n_image = self.get_image_slice( + orientation, slice_number, number_slices, inverted, border_size + ) + image = converters.to_vtk( + n_image, self.spacing, slice_number, orientation + ) ww_wl_image = self.do_ww_wl(image) image = self.do_colour_image(ww_wl_image) if self.current_mask and self.current_mask.is_shown: @@ -567,7 +590,9 @@ class Slice(with_metaclass(utils.Singleton, object)): # Prints that during navigation causes delay in update # print "Do not getting from buffer" n_mask = self.get_mask_slice(orientation, slice_number) - mask = converters.to_vtk(n_mask, self.spacing, slice_number, orientation) + mask = converters.to_vtk( + n_mask, self.spacing, slice_number, orientation + ) mask = self.do_colour_mask(mask, self.opacity) self.buffer_slices[orientation].mask = n_mask final_image = self.do_blend(image, mask) @@ -576,15 +601,18 @@ class Slice(with_metaclass(utils.Singleton, object)): final_image = image self.buffer_slices[orientation].vtk_image = image else: - n_image = self.get_image_slice(orientation, slice_number, - number_slices, inverted, border_size) + n_image = self.get_image_slice( + orientation, slice_number, number_slices, inverted, border_size + ) image = converters.to_vtk(n_image, self.spacing, slice_number, orientation) ww_wl_image = self.do_ww_wl(image) image = self.do_colour_image(ww_wl_image) if self.current_mask and self.current_mask.is_shown: n_mask = self.get_mask_slice(orientation, slice_number) - mask = converters.to_vtk(n_mask, self.spacing, slice_number, orientation) + mask = converters.to_vtk( + n_mask, self.spacing, slice_number, orientation + ) mask = self.do_colour_mask(mask, self.opacity) final_image = self.do_blend(image, mask) else: @@ -597,20 +625,34 @@ class Slice(with_metaclass(utils.Singleton, object)): self.buffer_slices[orientation].vtk_image = image self.buffer_slices[orientation].vtk_mask = mask - if self.to_show_aux == 'watershed' and self.current_mask.is_shown: - m = self.get_aux_slice('watershed', orientation, slice_number) + if self.to_show_aux == "watershed" and self.current_mask.is_shown: + m = self.get_aux_slice("watershed", orientation, slice_number) tmp_vimage = converters.to_vtk(m, self.spacing, slice_number, orientation) - cimage = self.do_custom_colour(tmp_vimage, {0: (0.0, 0.0, 0.0, 0.0), - 1: (0.0, 1.0, 0.0, 1.0), - 2: (1.0, 0.0, 0.0, 1.0)}) + cimage = self.do_custom_colour( + tmp_vimage, + { + 0: (0.0, 0.0, 0.0, 0.0), + 1: (0.0, 1.0, 0.0, 1.0), + 2: (1.0, 0.0, 0.0, 1.0), + }, + ) final_image = self.do_blend(final_image, cimage) elif self.to_show_aux and self.current_mask: m = self.get_aux_slice(self.to_show_aux, orientation, slice_number) tmp_vimage = converters.to_vtk(m, self.spacing, slice_number, orientation) - aux_image = self.do_custom_colour(tmp_vimage, {0: (0.0, 0.0, 0.0, 0.0), - 1: (0.0, 0.0, 0.0, 0.0), - 254: (1.0, 0.0, 0.0, 1.0), - 255: (1.0, 0.0, 0.0, 1.0)}) + try: + colour_table = self.aux_matrices_colours[self.to_show_aux] + except KeyError: + colour_table = { + 0: (0.0, 0.0, 0.0, 0.0), + 1: (0.0, 0.0, 0.0, 0.0), + 254: (1.0, 0.0, 0.0, 1.0), + 255: (1.0, 0.0, 0.0, 1.0), + } + aux_image = self.do_custom_colour( + tmp_vimage, + colour_table + ) final_image = self.do_blend(final_image, aux_image) return final_image @@ -644,11 +686,21 @@ class Slice(with_metaclass(utils.Singleton, object)): T1 = transformations.translation_matrix((cz, cy, cx)) M = transformations.concatenate_matrices(T1, R.T, T0) - - if orientation == 'AXIAL': - tmp_array = np.array(self.matrix[slice_number:slice_number + number_slices]) + if orientation == "AXIAL": + tmp_array = np.array( + self.matrix[slice_number : slice_number + number_slices] + ) if np.any(self.q_orientation[1::]): - transforms.apply_view_matrix_transform(self.matrix, self.spacing, M, slice_number, orientation, self.interp_method, self.matrix.min(), tmp_array) + transforms.apply_view_matrix_transform( + self.matrix, + self.spacing, + M, + slice_number, + orientation, + self.interp_method, + self.matrix.min(), + tmp_array, + ) if self._type_projection == const.PROJECTION_NORMAL: n_image = tmp_array.reshape(dy, dx) else: @@ -662,48 +714,89 @@ class Slice(with_metaclass(utils.Singleton, object)): elif self._type_projection == const.PROJECTION_MeanIP: n_image = np.array(tmp_array).mean(0) elif self._type_projection == const.PROJECTION_LMIP: - n_image = np.empty(shape=(tmp_array.shape[1], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.lmip(tmp_array, 0, self.window_level, self.window_level, n_image) + n_image = np.empty( + shape=(tmp_array.shape[1], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.lmip( + tmp_array, 0, self.window_level, self.window_level, n_image + ) elif self._type_projection == const.PROJECTION_MIDA: - n_image = np.empty(shape=(tmp_array.shape[1], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.mida(tmp_array, 0, self.window_level, self.window_level, n_image) + n_image = np.empty( + shape=(tmp_array.shape[1], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.mida( + tmp_array, 0, self.window_level, self.window_level, n_image + ) elif self._type_projection == const.PROJECTION_CONTOUR_MIP: - n_image = np.empty(shape=(tmp_array.shape[1], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 0, self.window_level, - self.window_level, 0, n_image) + n_image = np.empty( + shape=(tmp_array.shape[1], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 0, + self.window_level, + self.window_level, + 0, + n_image, + ) elif self._type_projection == const.PROJECTION_CONTOUR_LMIP: - n_image = np.empty(shape=(tmp_array.shape[1], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 0, self.window_level, - self.window_level, 1, n_image) + n_image = np.empty( + shape=(tmp_array.shape[1], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 0, + self.window_level, + self.window_level, + 1, + n_image, + ) elif self._type_projection == const.PROJECTION_CONTOUR_MIDA: - n_image = np.empty(shape=(tmp_array.shape[1], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 0, self.window_level, - self.window_level, 2, n_image) + n_image = np.empty( + shape=(tmp_array.shape[1], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 0, + self.window_level, + self.window_level, + 2, + n_image, + ) else: n_image = np.array(self.matrix[slice_number]) - elif orientation == 'CORONAL': - tmp_array = np.array(self.matrix[:, slice_number: slice_number + number_slices, :]) + elif orientation == "CORONAL": + tmp_array = np.array( + self.matrix[:, slice_number : slice_number + number_slices, :] + ) if np.any(self.q_orientation[1::]): - transforms.apply_view_matrix_transform(self.matrix, self.spacing, M, slice_number, orientation, self.interp_method, self.matrix.min(), tmp_array) + transforms.apply_view_matrix_transform( + self.matrix, + self.spacing, + M, + slice_number, + orientation, + self.interp_method, + self.matrix.min(), + tmp_array, + ) if self._type_projection == const.PROJECTION_NORMAL: n_image = tmp_array.reshape(dz, dx) else: - #if slice_number == 0: - #slice_number = 1 - #if slice_number - number_slices < 0: - #number_slices = slice_number + # if slice_number == 0: + # slice_number = 1 + # if slice_number - number_slices < 0: + # number_slices = slice_number if inverted: tmp_array = tmp_array[:, ::-1, :] if self._type_projection == const.PROJECTION_MaxIP: @@ -713,39 +806,80 @@ class Slice(with_metaclass(utils.Singleton, object)): elif self._type_projection == const.PROJECTION_MeanIP: n_image = np.array(tmp_array).mean(1) elif self._type_projection == const.PROJECTION_LMIP: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.lmip(tmp_array, 1, self.window_level, self.window_level, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.lmip( + tmp_array, 1, self.window_level, self.window_level, n_image + ) elif self._type_projection == const.PROJECTION_MIDA: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.mida(tmp_array, 1, self.window_level, self.window_level, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.mida( + tmp_array, 1, self.window_level, self.window_level, n_image + ) elif self._type_projection == const.PROJECTION_CONTOUR_MIP: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 1, self.window_level, - self.window_level, 0, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 1, + self.window_level, + self.window_level, + 0, + n_image, + ) elif self._type_projection == const.PROJECTION_CONTOUR_LMIP: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 1, self.window_level, - self.window_level, 1, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 1, + self.window_level, + self.window_level, + 1, + n_image, + ) elif self._type_projection == const.PROJECTION_CONTOUR_MIDA: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[2]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 1, self.window_level, - self.window_level, 2, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[2]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 1, + self.window_level, + self.window_level, + 2, + n_image, + ) else: n_image = np.array(self.matrix[:, slice_number, :]) - elif orientation == 'SAGITAL': - tmp_array = np.array(self.matrix[:, :, slice_number: slice_number + number_slices]) + elif orientation == "SAGITAL": + tmp_array = np.array( + self.matrix[:, :, slice_number : slice_number + number_slices] + ) if np.any(self.q_orientation[1::]): - transforms.apply_view_matrix_transform(self.matrix, self.spacing, M, slice_number, orientation, self.interp_method, self.matrix.min(), tmp_array) + transforms.apply_view_matrix_transform( + self.matrix, + self.spacing, + M, + slice_number, + orientation, + self.interp_method, + self.matrix.min(), + tmp_array, + ) if self._type_projection == const.PROJECTION_NORMAL: n_image = tmp_array.reshape(dz, dy) @@ -759,34 +893,64 @@ class Slice(with_metaclass(utils.Singleton, object)): elif self._type_projection == const.PROJECTION_MeanIP: n_image = np.array(tmp_array).mean(2) elif self._type_projection == const.PROJECTION_LMIP: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[1]), - dtype=tmp_array.dtype) - mips.lmip(tmp_array, 2, self.window_level, self.window_level, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[1]), + dtype=tmp_array.dtype, + ) + mips.lmip( + tmp_array, 2, self.window_level, self.window_level, n_image + ) elif self._type_projection == const.PROJECTION_MIDA: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[1]), - dtype=tmp_array.dtype) - mips.mida(tmp_array, 2, self.window_level, self.window_level, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[1]), + dtype=tmp_array.dtype, + ) + mips.mida( + tmp_array, 2, self.window_level, self.window_level, n_image + ) elif self._type_projection == const.PROJECTION_CONTOUR_MIP: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[1]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 2, self.window_level, - self.window_level, 0, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[1]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 2, + self.window_level, + self.window_level, + 0, + n_image, + ) elif self._type_projection == const.PROJECTION_CONTOUR_LMIP: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[1]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 2, self.window_level, - self.window_level, 1, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[1]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 2, + self.window_level, + self.window_level, + 1, + n_image, + ) elif self._type_projection == const.PROJECTION_CONTOUR_MIDA: - n_image = np.empty(shape=(tmp_array.shape[0], - tmp_array.shape[1]), - dtype=tmp_array.dtype) - mips.fast_countour_mip(tmp_array, border_size, 2, self.window_level, - self.window_level, 2, n_image) + n_image = np.empty( + shape=(tmp_array.shape[0], tmp_array.shape[1]), + dtype=tmp_array.dtype, + ) + mips.fast_countour_mip( + tmp_array, + border_size, + 2, + self.window_level, + self.window_level, + 2, + n_image, + ) else: n_image = np.array(self.matrix[:, :, slice_number]) @@ -794,63 +958,71 @@ class Slice(with_metaclass(utils.Singleton, object)): return n_image def get_mask_slice(self, orientation, slice_number): - """ + """ It gets the from actual mask the given slice from given orientation """ # It's necessary because the first position for each dimension from # mask matrix is used as flags to control if the mask in the # slice_number position has been generated. - if self.buffer_slices[orientation].index == slice_number \ - and self.buffer_slices[orientation].mask is not None: + if ( + self.buffer_slices[orientation].index == slice_number + and self.buffer_slices[orientation].mask is not None + ): return self.buffer_slices[orientation].mask n = slice_number + 1 - if orientation == 'AXIAL': + if orientation == "AXIAL": if self.current_mask.matrix[n, 0, 0] == 0: mask = self.current_mask.matrix[n, 1:, 1:] - mask[:] = self.do_threshold_to_a_slice(self.get_image_slice(orientation, - slice_number), - mask) + mask[:] = self.do_threshold_to_a_slice( + self.get_image_slice(orientation, slice_number), mask + ) self.current_mask.matrix[n, 0, 0] = 1 - n_mask = np.array(self.current_mask.matrix[n, 1:, 1:], - dtype=self.current_mask.matrix.dtype) + n_mask = np.array( + self.current_mask.matrix[n, 1:, 1:], + dtype=self.current_mask.matrix.dtype, + ) - elif orientation == 'CORONAL': + elif orientation == "CORONAL": if self.current_mask.matrix[0, n, 0] == 0: mask = self.current_mask.matrix[1:, n, 1:] - mask[:] = self.do_threshold_to_a_slice(self.get_image_slice(orientation, - slice_number), - mask) + mask[:] = self.do_threshold_to_a_slice( + self.get_image_slice(orientation, slice_number), mask + ) self.current_mask.matrix[0, n, 0] = 1 - n_mask = np.array(self.current_mask.matrix[1:, n, 1:], - dtype=self.current_mask.matrix.dtype) + n_mask = np.array( + self.current_mask.matrix[1:, n, 1:], + dtype=self.current_mask.matrix.dtype, + ) - elif orientation == 'SAGITAL': + elif orientation == "SAGITAL": if self.current_mask.matrix[0, 0, n] == 0: mask = self.current_mask.matrix[1:, 1:, n] - mask[:] = self.do_threshold_to_a_slice(self.get_image_slice(orientation, - slice_number), - mask) + mask[:] = self.do_threshold_to_a_slice( + self.get_image_slice(orientation, slice_number), mask + ) self.current_mask.matrix[0, 0, n] = 1 - n_mask = np.array(self.current_mask.matrix[1:, 1:, n], - dtype=self.current_mask.matrix.dtype) + n_mask = np.array( + self.current_mask.matrix[1:, 1:, n], + dtype=self.current_mask.matrix.dtype, + ) return n_mask def get_aux_slice(self, name, orientation, n): m = self.aux_matrices[name] - if orientation == 'AXIAL': + if orientation == "AXIAL": return np.array(m[n]) - elif orientation == 'CORONAL': + elif orientation == "CORONAL": return np.array(m[:, n, :]) - elif orientation == 'SAGITAL': + elif orientation == "SAGITAL": return np.array(m[:, :, n]) def GetNumberOfSlices(self, orientation): - if orientation == 'AXIAL': + if orientation == "AXIAL": return self.matrix.shape[0] - elif orientation == 'CORONAL': + elif orientation == "CORONAL": return self.matrix.shape[1] - elif orientation == 'SAGITAL': + elif orientation == "SAGITAL": return self.matrix.shape[2] def SetMaskColour(self, index, colour, update=True): @@ -858,16 +1030,17 @@ class Slice(with_metaclass(utils.Singleton, object)): proj = Project() proj.mask_dict[index].colour = colour - (r,g,b) = colour[:3] - colour_wx = [r*255, g*255, b*255] - Publisher.sendMessage('Change mask colour in notebook', - index=index, colour=(r,g,b)) - Publisher.sendMessage('Set GUI items colour', colour=colour_wx) + (r, g, b) = colour[:3] + colour_wx = [r * 255, g * 255, b * 255] + Publisher.sendMessage( + "Change mask colour in notebook", index=index, colour=(r, g, b) + ) + Publisher.sendMessage("Set GUI items colour", colour=colour_wx) if update: # Updating mask colour on vtkimagedata. for buffer_ in self.buffer_slices.values(): buffer_.discard_vtk_mask() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") session = ses.Session() session.ChangeProject() @@ -885,8 +1058,9 @@ class Slice(with_metaclass(utils.Singleton, object)): proj = Project() proj.mask_dict[index].edition_threshold_range = threshold_range - def SetMaskThreshold(self, index, threshold_range, slice_number=None, - orientation=None): + def SetMaskThreshold( + self, index, threshold_range, slice_number=None, orientation=None + ): """ Set a mask threshold range given its index and tuple of min and max threshold values. @@ -905,19 +1079,23 @@ class Slice(with_metaclass(utils.Singleton, object)): m[slice_ < thresh_min] = 0 m[slice_ > thresh_max] = 0 m[m == 1] = 255 - self.current_mask.matrix[n+1, 1:, 1:] = m + self.current_mask.matrix[n + 1, 1:, 1:] = m else: slice_ = self.buffer_slices[orientation].image if slice_ is not None: - self.buffer_slices[orientation].mask = (255 * ((slice_ >= thresh_min) & (slice_ <= thresh_max))).astype('uint8') + self.buffer_slices[orientation].mask = ( + 255 * ((slice_ >= thresh_min) & (slice_ <= thresh_max)) + ).astype("uint8") # Update viewer - #Publisher.sendMessage('Update slice viewer') + # Publisher.sendMessage('Update slice viewer') # Update data notebook (GUI) - Publisher.sendMessage('Set mask threshold in notebook', - index=self.current_mask.index, - threshold_range=self.current_mask.threshold_range) + Publisher.sendMessage( + "Set mask threshold in notebook", + index=self.current_mask.index, + threshold_range=self.current_mask.threshold_range, + ) else: proj = Project() proj.mask_dict[index].threshold_range = threshold_range @@ -932,15 +1110,18 @@ class Slice(with_metaclass(utils.Singleton, object)): proj.mask_dict[index].on_show() if value: - threshold_range = proj.mask_dict[index].edition_threshold_range - Publisher.sendMessage('Set edition threshold gui', threshold_range=threshold_range) + threshold_range = proj.mask_dict[index].threshold_range + Publisher.sendMessage( + "Set edition threshold gui", threshold_range=threshold_range + ) - if (index == self.current_mask.index): + if index == self.current_mask.index: for buffer_ in self.buffer_slices.values(): buffer_.discard_vtk_mask() buffer_.discard_mask() - Publisher.sendMessage('Reload actual slice') - #--------------------------------------------------------------------------- + Publisher.sendMessage("Reload actual slice") + + # --------------------------------------------------------------------------- def SelectCurrentMask(self, index): "Insert mask data, based on given index, into pipeline." @@ -952,27 +1133,37 @@ class Slice(with_metaclass(utils.Singleton, object)): colour = future_mask.colour self.SetMaskColour(index, colour, update=False) - self.buffer_slices = {"AXIAL": SliceBuffer(), - "CORONAL": SliceBuffer(), - "SAGITAL": SliceBuffer()} - - Publisher.sendMessage('Set mask threshold in notebook', - index=index, - threshold_range=self.current_mask.threshold_range) - Publisher.sendMessage('Set threshold values in gradient', - threshold_range=self.current_mask.threshold_range) - Publisher.sendMessage('Select mask name in combo', index=index) - Publisher.sendMessage('Update slice viewer') - #--------------------------------------------------------------------------- + self.buffer_slices = { + "AXIAL": SliceBuffer(), + "CORONAL": SliceBuffer(), + "SAGITAL": SliceBuffer(), + } + + Publisher.sendMessage( + "Set mask threshold in notebook", + index=index, + threshold_range=self.current_mask.threshold_range, + ) + Publisher.sendMessage( + "Set threshold values in gradient", + threshold_range=self.current_mask.threshold_range, + ) + Publisher.sendMessage("Select mask name in combo", index=index) + Publisher.sendMessage("Update slice viewer") + + # --------------------------------------------------------------------------- def CreateSurfaceFromIndex(self, surface_parameters): proj = Project() - mask = proj.mask_dict[surface_parameters['options']['index']] + mask = proj.mask_dict[surface_parameters["options"]["index"]] self.do_threshold_to_all_slices(mask) - Publisher.sendMessage('Create surface', - slice_=self, mask=mask, - surface_parameters=surface_parameters) + Publisher.sendMessage( + "Create surface", + slice_=self, + mask=mask, + surface_parameters=surface_parameters, + ) def GetOutput(self): return self.blend_filter.GetOutput() @@ -986,69 +1177,73 @@ class Slice(with_metaclass(utils.Singleton, object)): def SetTypeProjection(self, tprojection): if self._type_projection != tprojection: if self._type_projection == const.PROJECTION_NORMAL: - Publisher.sendMessage('Hide current mask') + Publisher.sendMessage("Hide current mask") if tprojection == const.PROJECTION_NORMAL: - Publisher.sendMessage('Show MIP interface', flag=False) + Publisher.sendMessage("Show MIP interface", flag=False) else: - Publisher.sendMessage('Show MIP interface', flag=True) + Publisher.sendMessage("Show MIP interface", flag=True) self._type_projection = tprojection for buffer_ in self.buffer_slices.values(): buffer_.discard_buffer() - Publisher.sendMessage('Check projection menu', projection_id=tprojection) + Publisher.sendMessage("Check projection menu", projection_id=tprojection) def SetInterpolationMethod(self, interp_method): if self.interp_method != interp_method: self.interp_method = interp_method for buffer_ in self.buffer_slices.values(): buffer_.discard_buffer() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def UpdateWindowLevelBackground(self, window, level): self.window_width = window self.window_level = level for buffer_ in self.buffer_slices.values(): - if self._type_projection in (const.PROJECTION_NORMAL, - const.PROJECTION_MaxIP, - const.PROJECTION_MinIP, - const.PROJECTION_MeanIP, - const.PROJECTION_LMIP): + if self._type_projection in ( + const.PROJECTION_NORMAL, + const.PROJECTION_MaxIP, + const.PROJECTION_MinIP, + const.PROJECTION_MeanIP, + const.PROJECTION_LMIP, + ): buffer_.discard_vtk_image() else: buffer_.discard_buffer() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def UpdateColourTableBackground(self, values): - self.from_= OTHER - self.number_of_colours= values[0] + self.from_ = OTHER + self.number_of_colours = values[0] self.saturation_range = values[1] self.hue_range = values[2] self.value_range = values[3] for buffer_ in self.buffer_slices.values(): buffer_.discard_vtk_image() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def UpdateColourTableBackgroundPlist(self, values): self.values = values - self.from_= PLIST + self.from_ = PLIST for buffer_ in self.buffer_slices.values(): buffer_.discard_vtk_image() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def UpdateColourTableBackgroundWidget(self, nodes): self.nodes = nodes - self.from_= WIDGET + self.from_ = WIDGET for buffer_ in self.buffer_slices.values(): - if self._type_projection in (const.PROJECTION_NORMAL, - const.PROJECTION_MaxIP, - const.PROJECTION_MinIP, - const.PROJECTION_MeanIP, - const.PROJECTION_LMIP): + if self._type_projection in ( + const.PROJECTION_NORMAL, + const.PROJECTION_MaxIP, + const.PROJECTION_MinIP, + const.PROJECTION_MeanIP, + const.PROJECTION_LMIP, + ): buffer_.discard_vtk_image() else: buffer_.discard_buffer() @@ -1060,34 +1255,37 @@ class Slice(with_metaclass(utils.Singleton, object)): self.window_width = pn - p0 self.window_level = (pn + p0) / 2 - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def UpdateSlice3D(self, widget, orientation): img = self.buffer_slices[orientation].vtk_image original_orientation = Project().original_orientation cast = vtk.vtkImageCast() cast.SetInputData(img) - cast.SetOutputScalarTypeToDouble() + cast.SetOutputScalarTypeToDouble() cast.ClampOverflowOn() cast.Update() - #if (original_orientation == const.AXIAL): + # if (original_orientation == const.AXIAL): flip = vtk.vtkImageFlip() flip.SetInputConnection(cast.GetOutputPort()) flip.SetFilteredAxis(1) flip.FlipAboutOriginOn() flip.Update() widget.SetInputConnection(flip.GetOutputPort()) - #else: - #widget.SetInput(cast.GetOutput()) - - def create_new_mask(self, name=None, - colour=None, - opacity=None, - threshold_range=None, - edition_threshold_range=None, - add_to_project=True, - show=True): + # else: + # widget.SetInput(cast.GetOutput()) + + def create_new_mask( + self, + name=None, + colour=None, + opacity=None, + threshold_range=None, + edition_threshold_range=None, + add_to_project=True, + show=True, + ): """ Creates a new mask and add it to project. @@ -1126,7 +1324,6 @@ class Slice(with_metaclass(utils.Singleton, object)): return future_mask - def _add_mask_into_proj(self, mask, show=True): """ Insert a new mask into project and retrieve its index. @@ -1140,14 +1337,13 @@ class Slice(with_metaclass(utils.Singleton, object)): mask.index = index ## update gui related to mask - Publisher.sendMessage('Add mask', - mask=mask) + Publisher.sendMessage("Add mask", mask=mask) if show: self.current_mask = mask - Publisher.sendMessage('Show mask', index=mask.index, value=True) - Publisher.sendMessage('Change mask selected', index=mask.index) - Publisher.sendMessage('Update slice viewer') + Publisher.sendMessage("Show mask", index=mask.index, value=True) + Publisher.sendMessage("Change mask selected", index=mask.index) + Publisher.sendMessage("Update slice viewer") def do_ww_wl(self, image): if self.from_ == PLIST: @@ -1158,7 +1354,7 @@ class Slice(with_metaclass(utils.Singleton, object)): i = 0 for r, g, b in self.values: - lut.SetTableValue(i, r/255.0, g/255.0, b/255.0, 1.0) + lut.SetTableValue(i, r / 255.0, g / 255.0, b / 255.0, 1.0) i += 1 colorer = vtk.vtkImageMapToColors() @@ -1167,11 +1363,11 @@ class Slice(with_metaclass(utils.Singleton, object)): colorer.SetOutputFormatToRGB() colorer.Update() elif self.from_ == WIDGET: - lut = vtk.vtkColorTransferFunction() + lut = vtk.vtkColorTransferFunction() for n in self.nodes: r, g, b = n.colour - lut.AddRGBPoint(n.value, r/255.0, g/255.0, b/255.0) + lut.AddRGBPoint(n.value, r / 255.0, g / 255.0, b / 255.0) lut.Build() @@ -1191,7 +1387,7 @@ class Slice(with_metaclass(utils.Singleton, object)): return colorer.GetOutput() def _update_wwwl_widget_nodes(self, ww, wl): - if self.from_ == WIDGET: + if self.from_ == WIDGET: knodes = sorted(self.nodes) p1 = knodes[0] @@ -1217,7 +1413,7 @@ class Slice(with_metaclass(utils.Singleton, object)): node.value += shiftWW * factor def do_threshold_to_a_slice(self, slice_matrix, mask, threshold=None): - """ + """ Based on the current threshold bounds generates a threshold mask to given slice_matrix. """ @@ -1226,12 +1422,12 @@ class Slice(with_metaclass(utils.Singleton, object)): else: thresh_min, thresh_max = self.current_mask.threshold_range - m = (((slice_matrix >= thresh_min) & (slice_matrix <= thresh_max)) * 255) + m = ((slice_matrix >= thresh_min) & (slice_matrix <= thresh_max)) * 255 m[mask == 1] = 1 m[mask == 2] = 2 m[mask == 253] = 253 m[mask == 254] = 254 - return m.astype('uint8') + return m.astype("uint8") def do_threshold_to_all_slices(self, mask=None): """ @@ -1246,7 +1442,9 @@ class Slice(with_metaclass(utils.Singleton, object)): for n in range(1, mask.matrix.shape[0]): if mask.matrix[n, 0, 0] == 0: m = mask.matrix[n, 1:, 1:] - mask.matrix[n, 1:, 1:] = self.do_threshold_to_a_slice(self.matrix[n-1], m, mask.threshold_range) + mask.matrix[n, 1:, 1:] = self.do_threshold_to_a_slice( + self.matrix[n - 1], m, mask.threshold_range + ) mask.matrix.flush() @@ -1318,7 +1516,7 @@ class Slice(with_metaclass(utils.Singleton, object)): lut_mask.SetNumberOfTableValues(ncolours) for v in map_colours: - r,g, b,a = map_colours[v] + r, g, b, a = map_colours[v] lut_mask.SetTableValue(v, r, g, b, a) lut_mask.SetRampToLinear() @@ -1352,13 +1550,13 @@ class Slice(with_metaclass(utils.Singleton, object)): def _do_boolean_op(self, operation, mask1, mask2): self.do_boolean_op(operation, mask1, mask2) - def do_boolean_op(self, op, m1, m2): - name_ops = {const.BOOLEAN_UNION: _(u"Union"), - const.BOOLEAN_DIFF: _(u"Diff"), - const.BOOLEAN_AND: _(u"Intersection"), - const.BOOLEAN_XOR: _(u"XOR")} - + name_ops = { + const.BOOLEAN_UNION: _(u"Union"), + const.BOOLEAN_DIFF: _(u"Diff"), + const.BOOLEAN_AND: _(u"Intersection"), + const.BOOLEAN_XOR: _(u"XOR"), + } name = u"%s_%s_%s" % (name_ops[op], m1.name, m2.name) proj = Project() @@ -1406,32 +1604,32 @@ class Slice(with_metaclass(utils.Singleton, object)): 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 + 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 @@ -1440,11 +1638,13 @@ class Slice(with_metaclass(utils.Singleton, object)): if o != orientation: self.buffer_slices[o].discard_mask() self.buffer_slices[o].discard_vtk_mask() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def apply_reorientation(self): temp_file = tempfile.mktemp() - mcopy = np.memmap(temp_file, shape=self.matrix.shape, dtype=self.matrix.dtype, mode='w+') + mcopy = np.memmap( + temp_file, shape=self.matrix.shape, dtype=self.matrix.dtype, mode="w+" + ) mcopy[:] = self.matrix cx, cy, cz = self.center @@ -1453,13 +1653,24 @@ class Slice(with_metaclass(utils.Singleton, object)): T1 = transformations.translation_matrix((cz, cy, cx)) M = transformations.concatenate_matrices(T1, R.T, T0) - transforms.apply_view_matrix_transform(mcopy, self.spacing, M, 0, 'AXIAL', self.interp_method, mcopy.min(), self.matrix) + transforms.apply_view_matrix_transform( + mcopy, + self.spacing, + M, + 0, + "AXIAL", + self.interp_method, + mcopy.min(), + self.matrix, + ) del mcopy os.remove(temp_file) self.q_orientation = np.array((1, 0, 0, 0)) - self.center = [(s * d/2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing)] + self.center = [ + (s * d / 2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing) + ] self.__clean_current_mask() if self.current_mask: @@ -1469,35 +1680,39 @@ class Slice(with_metaclass(utils.Singleton, object)): for o in self.buffer_slices: self.buffer_slices[o].discard_buffer() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def __undo_edition(self): buffer_slices = self.buffer_slices - actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, - "CORONAL": buffer_slices["CORONAL"].index, - "SAGITAL": buffer_slices["SAGITAL"].index, - "VOLUME": 0} + actual_slices = { + "AXIAL": buffer_slices["AXIAL"].index, + "CORONAL": buffer_slices["CORONAL"].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() self.buffer_slices[o].discard_vtk_mask() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def __redo_edition(self): buffer_slices = self.buffer_slices - actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, - "CORONAL": buffer_slices["CORONAL"].index, - "SAGITAL": buffer_slices["SAGITAL"].index, - "VOLUME": 0} + actual_slices = { + "AXIAL": buffer_slices["AXIAL"].index, + "CORONAL": buffer_slices["CORONAL"].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() self.buffer_slices[o].discard_vtk_mask() - Publisher.sendMessage('Reload actual slice') + Publisher.sendMessage("Reload actual slice") def _open_image_matrix(self, filename, shape, dtype): self.matrix_filename = filename - self.matrix = np.memmap(filename, shape=shape, dtype=dtype, mode='r+') + self.matrix = np.memmap(filename, shape=shape, dtype=dtype, mode="r+") def OnFlipVolume(self, axis): if axis == 0: @@ -1526,16 +1741,16 @@ class Slice(with_metaclass(utils.Singleton, object)): def OnExportMask(self, filename, filetype): imagedata = self.current_mask.imagedata # imagedata = self.imagedata - if (filetype == const.FILETYPE_IMAGEDATA): + if filetype == const.FILETYPE_IMAGEDATA: iu.Export(imagedata, filename) def _fill_holes_auto(self, parameters): - target = parameters['target'] - conn = parameters['conn'] - orientation = parameters['orientation'] - size = parameters['size'] + target = parameters["target"] + conn = parameters["conn"] + orientation = parameters["orientation"] + size = parameters["size"] - if target == '2D': + if target == "2D": index = self.buffer_slices[orientation].index else: index = 0 @@ -1543,15 +1758,15 @@ class Slice(with_metaclass(utils.Singleton, object)): self.current_mask.fill_holes_auto(target, conn, orientation, index, size) - self.buffer_slices['AXIAL'].discard_mask() - self.buffer_slices['CORONAL'].discard_mask() - self.buffer_slices['SAGITAL'].discard_mask() + 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() + 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') + Publisher.sendMessage("Reload actual slice") def calc_image_density(self, mask=None): if mask is None: @@ -1573,26 +1788,27 @@ class Slice(with_metaclass(utils.Singleton, object)): mask = self.current_mask self.do_threshold_to_all_slices(mask) - bin_img = (mask.matrix[1:, 1:, 1:] > 127) + bin_img = mask.matrix[1:, 1:, 1:] > 127 sx, sy, sz = self.spacing kernel = np.zeros((3, 3, 3)) kernel[1, 1, 1] = 2 * sx * sy + 2 * sx * sz + 2 * sy * sz - kernel[0, 1, 1] = - (sx * sy) - kernel[2, 1, 1] = - (sx * sy) + kernel[0, 1, 1] = -(sx * sy) + kernel[2, 1, 1] = -(sx * sy) - kernel[1, 0, 1] = - (sx * sz) - kernel[1, 2, 1] = - (sx * sz) + kernel[1, 0, 1] = -(sx * sz) + kernel[1, 2, 1] = -(sx * sz) - kernel[1, 1, 0] = - (sy * sz) - kernel[1, 1, 2] = - (sy * sz) + kernel[1, 1, 0] = -(sy * sz) + kernel[1, 1, 2] = -(sy * sz) # area = ndimage.generic_filter(bin_img * 1.0, _conv_area, size=(3, 3, 3), mode='constant', cval=1, extra_arguments=(sx, sy, sz)).sum() area = transforms.convolve_non_zero(bin_img * 1.0, kernel, 1).sum() return area + def _conv_area(x, sx, sy, sz): x = x.reshape((3, 3, 3)) if x[1, 1, 1]: diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index 040b403..5cfadf0 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -17,43 +17,37 @@ # detalhes. #-------------------------------------------------------------------------- -from six import with_metaclass - -import os +import math import multiprocessing +import os import tempfile import time -import math - from concurrent import futures +import numpy as np import vtk import wx - +from scipy import ndimage +from imageio import imsave +from scipy.ndimage import generate_binary_structure, watershed_ift +from six import with_metaclass +from skimage.morphology import watershed from wx.lib.pubsub import pub as Publisher import invesalius.constants as const import invesalius.data.converters as converters import invesalius.data.cursor_actors as ca -import invesalius.session as ses - -import numpy as np - -from scipy import ndimage -from imageio import imsave -from scipy.ndimage import watershed_ift, generate_binary_structure -from skimage.morphology import watershed - +import invesalius.data.geometry as geom +import invesalius.data.transformations as transformations +import invesalius.data.watershed_process as watershed_process import invesalius.gui.dialogs as dialogs -from invesalius.data.measures import MeasureData, CircleDensityMeasure, PolygonDensityMeasure +import invesalius.session as ses +import invesalius.utils as utils +from invesalius.data.measures import (CircleDensityMeasure, MeasureData, + PolygonDensityMeasure) from . import floodfill -import invesalius.data.watershed_process as watershed_process -import invesalius.utils as utils -import invesalius.data.transformations as transformations -import invesalius.data.geometry as geom - ORIENTATIONS = { "AXIAL": const.AXIAL, "CORONAL": const.CORONAL, @@ -198,6 +192,266 @@ class DefaultInteractorStyle(BaseImageInteractorStyle): self.viewer.OnScrollBackward() +class BaseImageEditionInteractorStyle(DefaultInteractorStyle): + def __init__(self, viewer): + super().__init__(viewer) + + self.viewer = viewer + self.orientation = self.viewer.orientation + + self.picker = vtk.vtkWorldPointPicker() + self.matrix = None + + self.cursor = None + self.brush_size = const.BRUSH_SIZE + self.brush_format = const.DEFAULT_BRUSH_FORMAT + self.brush_colour = const.BRUSH_COLOUR + self._set_cursor() + + self.fill_value = 254 + + self.AddObserver("EnterEvent", self.OnBIEnterInteractor) + self.AddObserver("LeaveEvent", self.OnBILeaveInteractor) + + self.AddObserver("LeftButtonPressEvent", self.OnBIBrushClick) + self.AddObserver("LeftButtonReleaseEvent", self.OnBIBrushRelease) + self.AddObserver("MouseMoveEvent", self.OnBIBrushMove) + + self.RemoveObservers("MouseWheelForwardEvent") + self.RemoveObservers("MouseWheelBackwardEvent") + self.AddObserver("MouseWheelForwardEvent", self.OnBIScrollForward) + self.AddObserver("MouseWheelBackwardEvent", self.OnBIScrollBackward) + + def _set_cursor(self): + if const.DEFAULT_BRUSH_FORMAT == const.BRUSH_SQUARE: + self.cursor = ca.CursorRectangle() + elif const.DEFAULT_BRUSH_FORMAT == const.BRUSH_CIRCLE: + self.cursor = ca.CursorCircle() + + self.cursor.SetOrientation(self.orientation) + n = self.viewer.slice_data.number + coordinates = {"SAGITAL": [n, 0, 0], + "CORONAL": [0, n, 0], + "AXIAL": [0, 0, n]} + self.cursor.SetPosition(coordinates[self.orientation]) + spacing = self.viewer.slice_.spacing + self.cursor.SetSpacing(spacing) + self.cursor.SetColour(self.viewer._brush_cursor_colour) + self.cursor.SetSize(self.brush_size) + self.viewer.slice_data.SetCursor(self.cursor) + + def set_brush_size(self, size): + self.brush_size = size + self._set_cursor() + + def set_brush_format(self, format): + self.brush_format = format + self._set_cursor() + + def set_brush_operation(self, operation): + self.brush_operation = operation + self._set_cursor() + + def set_fill_value(self, fill_value): + self.fill_value = fill_value + + def set_matrix(self, matrix): + self.matrix = matrix + + def OnBIEnterInteractor(self, obj, evt): + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + self.viewer.slice_data.cursor.Show() + self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) + self.viewer.interactor.Render() + + def OnBILeaveInteractor(self, obj, evt): + self.viewer.slice_data.cursor.Show(0) + self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) + self.viewer.interactor.Render() + + def OnBIBrushClick(self, obj, evt): + try: + self.before_brush_click() + except AttributeError: + pass + + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + + viewer = self.viewer + iren = viewer.interactor + operation = self.config.operation + + viewer._set_editor_cursor_visibility(1) + + mouse_x, mouse_y = iren.GetEventPosition() + render = iren.FindPokedRenderer(mouse_x, mouse_y) + slice_data = viewer.get_slice_data(render) + + slice_data.cursor.Show() + + wx, wy, wz = viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) + position = viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) + index = slice_data.number + + cursor = slice_data.cursor + radius = cursor.radius + + slice_data.cursor.SetPosition((wx, wy, wz)) + + self.edit_mask_pixel(self.fill_value, index, cursor.GetPixels(), + position, radius, viewer.orientation) + + try: + self.after_brush_click() + except AttributeError: + pass + + viewer.OnScrollBar() + + def OnBIBrushMove(self, obj, evt): + try: + self.before_brush_move() + except AttributeError: + pass + + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + + viewer = self.viewer + iren = viewer.interactor + + viewer._set_editor_cursor_visibility(1) + + mouse_x, mouse_y = iren.GetEventPosition() + render = iren.FindPokedRenderer(mouse_x, mouse_y) + slice_data = viewer.get_slice_data(render) + operation = self.config.operation + + wx, wy, wz = viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) + slice_data.cursor.SetPosition((wx, wy, wz)) + + if (self.left_pressed): + cursor = slice_data.cursor + radius = cursor.radius + + position = viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) + index = slice_data.number + + slice_data.cursor.SetPosition((wx, wy, wz)) + self.edit_mask_pixel(self.fill_value, index, cursor.GetPixels(), + position, radius, viewer.orientation) + try: + self.after_brush_move() + except AttributeError: + pass + viewer.OnScrollBar(update3D=False) + else: + viewer.interactor.Render() + + def OnBIBrushRelease(self, evt, obj): + try: + self.before_brush_release() + except AttributeError: + pass + + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + + self.after_brush_release() + self.viewer.discard_mask_cache(all_orientations=True, vtk_cache=True) + Publisher.sendMessage('Reload actual slice') + + def edit_mask_pixel(self, fill_value, n, index, position, radius, orientation): + if orientation == 'AXIAL': + matrix = self.matrix[n, :, :] + elif orientation == 'CORONAL': + matrix = self.matrix[:, n, :] + elif orientation == 'SAGITAL': + matrix = self.matrix[:, :, n] + + spacing = self.viewer.slice_.spacing + if hasattr(position, '__iter__'): + px, py = position + if orientation == 'AXIAL': + sx = spacing[0] + sy = spacing[1] + elif orientation == 'CORONAL': + sx = spacing[0] + sy = spacing[2] + elif orientation == 'SAGITAL': + sx = spacing[2] + sy = spacing[1] + + else: + if orientation == 'AXIAL': + sx = spacing[0] + sy = spacing[1] + py = position / matrix.shape[1] + px = position % matrix.shape[1] + elif orientation == 'CORONAL': + sx = spacing[0] + sy = spacing[2] + py = position / matrix.shape[1] + px = position % matrix.shape[1] + elif orientation == 'SAGITAL': + sx = spacing[2] + sy = spacing[1] + py = position / matrix.shape[1] + px = position % matrix.shape[1] + + cx = index.shape[1] / 2 + 1 + cy = index.shape[0] / 2 + 1 + xi = int(px - index.shape[1] + cx) + xf = int(xi + index.shape[1]) + yi = int(py - index.shape[0] + cy) + yf = int(yi + index.shape[0]) + + if yi < 0: + index = index[abs(yi):,:] + yi = 0 + if yf > matrix.shape[0]: + index = index[:index.shape[0]-(yf-matrix.shape[0]), :] + yf = matrix.shape[0] + + if xi < 0: + index = index[:,abs(xi):] + xi = 0 + if xf > matrix.shape[1]: + index = index[:,:index.shape[1]-(xf-matrix.shape[1])] + xf = matrix.shape[1] + + # Verifying if the points is over the image array. + if (not 0 <= xi <= matrix.shape[1] and not 0 <= xf <= matrix.shape[1]) or \ + (not 0 <= yi <= matrix.shape[0] and not 0 <= yf <= matrix.shape[0]): + return + + roi_m = matrix[yi:yf,xi:xf] + + # Checking if roi_i has at least one element. + if roi_m.size: + roi_m[index] = self.fill_value + + def OnBIScrollForward(self, evt, obj): + iren = self.viewer.interactor + if iren.GetControlKey(): + size = self.brush_size + 1 + if size <= 100: + self.set_brush_size(size) + else: + self.OnScrollForward(obj, evt) + + def OnBIScrollBackward(self, evt, obj): + iren = self.viewer.interactor + if iren.GetControlKey(): + size = self.brush_size - 1 + if size > 0: + self.set_brush_size(size) + else: + self.OnScrollBackward(obj, evt) + + class CrossInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for the Cross. @@ -2509,10 +2763,8 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): - - -def get_style(style): - STYLES = { +class Styles: + styles = { const.STATE_DEFAULT: DefaultInteractorStyle, const.SLICE_STATE_CROSS: CrossInteractorStyle, const.STATE_WL: WWWLInteractorStyle, @@ -2534,4 +2786,26 @@ def get_style(style): const.SLICE_STATE_FFILL_SEGMENTATION: FloodFillSegmentInteractorStyle, const.SLICE_STATE_CROP_MASK: CropMaskInteractorStyle, } - return STYLES[style] + + @classmethod + def add_style(cls, style_cls, level=1): + if style_cls in cls.styles.values(): + for style_id in cls.styles: + if cls.styles[style_id] == style_cls: + const.SLICE_STYLES.append(style_id) + const.STYLE_LEVEL[style_id] = level + return style_id + + new_style_id = max(cls.styles) + 1 + cls.styles[new_style_id] = style_cls + const.SLICE_STYLES.append(new_style_id) + const.STYLE_LEVEL[new_style_id] = level + return new_style_id + + @classmethod + def remove_style(cls, style_id): + del cls.styles[style_id] + + @classmethod + def get_style(cls, style): + return cls.styles[style] diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index eae233e..2964444 100644 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -303,8 +303,8 @@ class Viewer(wx.Panel): self.style.CleanUp() del self.style - - style = styles.get_style(state)(self) + + style = styles.Styles.get_style(state)(self) setup = getattr(style, 'SetUp', None) if setup: @@ -1539,3 +1539,39 @@ class Viewer(wx.Panel): renderer.RemoveActor(actor) # and remove the actor from the actor's list self.actors_by_slice_number[slice_number].remove(actor) + + def get_actual_mask(self): + # Returns actual mask. Returns None if there is not a mask or no mask + # visible. + mask = self.slice_.current_mask + return mask + + def get_slice(self): + return self.slice_ + + def discard_slice_cache(self, all_orientations=False, vtk_cache=True): + if all_orientations: + for orientation in self.slice_.buffer_slices: + buffer_ = self.slice_.buffer_slices[orientation] + buffer_.discard_image() + if vtk_cache: + buffer_.discard_vtk_image() + else: + buffer_ = self.slice_.buffer_slices[self.orientation] + buffer_.discard_image() + if vtk_cache: + buffer_.discard_vtk_image() + + def discard_mask_cache(self, all_orientations=False, vtk_cache=True): + if all_orientations: + for orientation in self.slice_.buffer_slices: + buffer_ = self.slice_.buffer_slices[orientation] + buffer_.discard_mask() + if vtk_cache: + buffer_.discard_vtk_mask() + + else: + buffer_ = self.slice_.buffer_slices[self.orientation] + buffer_.discard_mask() + if vtk_cache: + buffer_.discard_vtk_mask() diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index c253c01..e7ec0ff 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -755,6 +755,34 @@ class UpdateMessageDialog(wx.Dialog): self.Destroy() +class MessageBox(wx.Dialog): + def __init__(self, parent, title, message, caption="InVesalius3 Error"): + wx.Dialog.__init__(self, parent, title=caption, style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) + + title_label = wx.StaticText(self, -1, title) + + text = wx.TextCtrl(self, style=wx.TE_MULTILINE|wx.TE_READONLY|wx.BORDER_NONE) + text.SetValue(message) + text.SetBackgroundColour(wx.SystemSettings.GetColour(4)) + + width, height = text.GetTextExtent("O"*30) + text.SetMinSize((width, -1)) + + btn_ok = wx.Button(self, wx.ID_OK) + btnsizer = wx.StdDialogButtonSizer() + btnsizer.AddButton(btn_ok) + btnsizer.Realize() + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(title_label, 0, wx.ALIGN_CENTRE|wx.ALL|wx.EXPAND, 5) + sizer.Add(text, 1, wx.ALIGN_CENTRE|wx.ALL|wx.EXPAND, 5) + sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5) + self.SetSizer(sizer) + sizer.Fit(self) + self.Center() + self.ShowModal() + + def SaveChangesDialog__Old(filename): message = _("The project %s has been modified.\nSave changes?")%filename dlg = MessageDialog(message) diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 68ce16d..4063e00 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -20,6 +20,7 @@ import math import os.path import platform +import subprocess import sys import webbrowser @@ -534,6 +535,9 @@ class Frame(wx.Frame): elif id == const.ID_CREATE_MASK: Publisher.sendMessage('New mask from shortcut') + elif id == const.ID_PLUGINS_SHOW_PATH: + self.ShowPluginsFolder() + def OnDbsMode(self): st = self.actived_dbs_mode.IsChecked() Publisher.sendMessage('Deactive target button') @@ -742,6 +746,19 @@ class Frame(wx.Frame): def OnCropMask(self): Publisher.sendMessage('Enable style', style=const.SLICE_STATE_CROP_MASK) + def ShowPluginsFolder(self): + """ + Show getting started window. + """ + inv_paths.create_conf_folders() + path = str(inv_paths.USER_PLUGINS_DIRECTORY) + if platform.system() == "Windows": + os.startfile(path) + elif platform.system() == "Darwin": + subprocess.Popen(["open", path]) + else: + subprocess.Popen(["xdg-open", path]) + # ------------------------------------------------------------------ # ------------------------------------------------------------------ # ------------------------------------------------------------------ @@ -755,6 +772,7 @@ class MenuBar(wx.MenuBar): wx.MenuBar.__init__(self) self.parent = parent + self._plugins_menu_ids = {} # Used to enable/disable menu items if project is opened or # not. Eg. save should only be available if a project is open @@ -809,6 +827,8 @@ class MenuBar(wx.MenuBar): sub(self.OnUpdateSliceInterpolation, "Update Slice Interpolation MenuBar") sub(self.OnUpdateNavigationMode, "Update Navigation Mode MenuBar") + sub(self.AddPluginsItems, "Add plugins menu items") + self.num_masks = 0 def __init_items(self): @@ -1003,6 +1023,10 @@ class MenuBar(wx.MenuBar): self.actived_navigation_mode = self.mode_menu + plugins_menu = wx.Menu() + plugins_menu.Append(const.ID_PLUGINS_SHOW_PATH, _("Open Plugins folder")) + self.plugins_menu = plugins_menu + # HELP help_menu = wx.Menu() help_menu.Append(const.ID_START, _("Getting started...")) @@ -1020,11 +1044,24 @@ class MenuBar(wx.MenuBar): self.Append(file_edit, _("Edit")) self.Append(view_menu, _(u"View")) self.Append(tools_menu, _(u"Tools")) + self.Append(plugins_menu, _(u"Plugins")) #self.Append(tools_menu, "Tools") self.Append(options_menu, _("Options")) self.Append(mode_menu, _("Mode")) self.Append(help_menu, _("Help")) + plugins_menu.Bind(wx.EVT_MENU, self.OnPluginMenu) + + def OnPluginMenu(self, evt): + id = evt.GetId() + if id != const.ID_PLUGINS_SHOW_PATH: + try: + plugin_name = self._plugins_menu_ids[id]["name"] + print("Loading plugin:", plugin_name) + Publisher.sendMessage("Load plugin", plugin_name=plugin_name) + except KeyError: + print("Invalid plugin") + evt.Skip() def SliceInterpolationStatus(self): @@ -1052,6 +1089,18 @@ class MenuBar(wx.MenuBar): v = self.NavigationModeStatus() self.mode_menu.Check(const.ID_MODE_NAVIGATION, v) + def AddPluginsItems(self, items): + for menu_item in self.plugins_menu.GetMenuItems(): + if menu_item.GetId() != const.ID_PLUGINS_SHOW_PATH: + self.plugins_menu.DestroyItem(menu_item) + + for item in items: + _new_id = wx.NewId() + self._plugins_menu_ids[_new_id] = items[item] + menu_item = self.plugins_menu.Append(_new_id, item, items[item]["description"]) + menu_item.Enable(items[item]["enable_startup"]) + print(">>> menu", item) + def OnEnableState(self, state): """ Based on given state, enables or disables menu items which @@ -1069,6 +1118,11 @@ class MenuBar(wx.MenuBar): for item in self.enable_items: self.Enable(item, False) + # Disabling plugins menus that needs a project open + for item in self._plugins_menu_ids: + if not self._plugins_menu_ids[item]["enable_startup"]: + self.Enable(item, False) + def SetStateProjectOpen(self): """ Enable menu items (e.g. save) when project is opened. @@ -1076,6 +1130,11 @@ class MenuBar(wx.MenuBar): for item in self.enable_items: self.Enable(item, True) + # Enabling plugins menus that needs a project open + for item in self._plugins_menu_ids: + if not self._plugins_menu_ids[item]["enable_startup"]: + self.Enable(item, True) + def OnEnableUndo(self, value): if value: self.FindItemById(wx.ID_UNDO).Enable(True) diff --git a/invesalius/inv_paths.py b/invesalius/inv_paths.py index 5e53dba..188e305 100644 --- a/invesalius/inv_paths.py +++ b/invesalius/inv_paths.py @@ -1,3 +1,21 @@ +# -------------------------------------------------------------------- +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer +# Homepage: http://www.softwarepublico.gov.br +# Contact: invesalius@cti.gov.br +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) +# -------------------------------------------------------------------- +# Este programa e software livre; voce pode redistribui-lo e/ou +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme +# publicada pela Free Software Foundation; de acordo com a versao 2 +# da Licenca. +# +# Este programa eh distribuido na expectativa de ser util, mas SEM +# QUALQUER GARANTIA; sem mesmo a garantia implicita de +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais +# detalhes. +# -------------------------------------------------------------------- import os import pathlib import shutil @@ -12,6 +30,8 @@ USER_LOG_DIR = USER_INV_DIR.joinpath("logs") USER_RAYCASTING_PRESETS_DIRECTORY = USER_PRESET_DIR.joinpath("raycasting") TEMP_DIR = tempfile.gettempdir() +USER_PLUGINS_DIRECTORY = USER_INV_DIR.joinpath("plugins") + OLD_USER_INV_DIR = USER_DIR.joinpath(".invesalius") OLD_USER_PRESET_DIR = OLD_USER_INV_DIR.joinpath("presets") OLD_USER_LOG_DIR = OLD_USER_INV_DIR.joinpath("logs") @@ -26,6 +46,7 @@ RAYCASTING_PRESETS_COLOR_DIRECTORY = INV_TOP_DIR.joinpath( "presets", "raycasting", "color_list" ) + # Inside the windows executable if hasattr(sys, "frozen") and ( sys.frozen == "windows_exe" or sys.frozen == "console_exe" @@ -71,6 +92,7 @@ def create_conf_folders(): USER_INV_DIR.mkdir(parents=True, exist_ok=True) USER_PRESET_DIR.mkdir(parents=True, exist_ok=True) USER_LOG_DIR.mkdir(parents=True, exist_ok=True) + USER_PLUGINS_DIRECTORY.mkdir(parents=True, exist_ok=True) def copy_old_files(): diff --git a/invesalius/plugins.py b/invesalius/plugins.py new file mode 100644 index 0000000..18ace3f --- /dev/null +++ b/invesalius/plugins.py @@ -0,0 +1,74 @@ +# -------------------------------------------------------------------- +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer +# Homepage: http://www.softwarepublico.gov.br +# Contact: invesalius@cti.gov.br +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) +# -------------------------------------------------------------------- +# Este programa e software livre; voce pode redistribui-lo e/ou +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme +# publicada pela Free Software Foundation; de acordo com a versao 2 +# da Licenca. +# +# Este programa eh distribuido na expectativa de ser util, mas SEM +# QUALQUER GARANTIA; sem mesmo a garantia implicita de +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais +# detalhes. +# -------------------------------------------------------------------- + +import importlib.util +import json +import sys + +from wx.lib.pubsub import pub as Publisher + +import invesalius.constants as consts +from invesalius import inv_paths + + +def import_source(module_name, module_file_path): + module_spec = importlib.util.spec_from_file_location(module_name, module_file_path) + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + return module + + +class PluginManager: + def __init__(self): + self.plugins = {} + self.__bind_pubsub_evt() + + def __bind_pubsub_evt(self): + Publisher.subscribe(self.load_plugin, "Load plugin") + + def find_plugins(self): + self.plugins = {} + for p in sorted(inv_paths.USER_PLUGINS_DIRECTORY.glob("*")): + if p.is_dir(): + try: + with p.joinpath("plugin.json").open() as f: + jdict = json.load(f) + plugin_name = jdict["name"] + plugin_description = jdict["description"] + enable_startup = jdict.get("enable-startup", False) + + self.plugins[plugin_name] = { + "name": plugin_name, + "description": plugin_description, + "folder": p, + "enable_startup": enable_startup, + } + except Exception as err: + print("It was not possible to load plugin. Error: {}".format(err)) + + Publisher.sendMessage("Add plugins menu items", items=self.plugins) + + def load_plugin(self, plugin_name): + if plugin_name in self.plugins: + plugin_module = import_source( + plugin_name, self.plugins[plugin_name]["folder"].joinpath("__init__.py") + ) + sys.modules[plugin_name] = plugin_module + main = importlib.import_module(plugin_name + '.main') + main.load() diff --git a/invesalius/project.py b/invesalius/project.py index 478db42..209a079 100644 --- a/invesalius/project.py +++ b/invesalius/project.py @@ -17,8 +17,6 @@ # detalhes. #-------------------------------------------------------------------------- -from six import with_metaclass - import datetime import glob import os @@ -29,17 +27,18 @@ import tarfile import tempfile import numpy as np +import vtk import wx +from six import with_metaclass from wx.lib.pubsub import pub as Publisher -import vtk import invesalius.constants as const import invesalius.data.polydata_utils as pu -from invesalius.presets import Presets -from invesalius.utils import Singleton, debug, touch, decode import invesalius.version as version - from invesalius import inv_paths +from invesalius.data import imagedata_utils +from invesalius.presets import Presets +from invesalius.utils import Singleton, debug, decode, touch if sys.platform == 'win32': try: @@ -277,20 +276,24 @@ class Project(with_metaclass(Singleton, object)): os.remove(f) def OpenPlistProject(self, filename): - import invesalius.data.measures as ms - import invesalius.data.mask as msk - import invesalius.data.surface as srf - if not const.VTK_WARNING: log_path = os.path.join(inv_paths.USER_LOG_DIR, 'vtkoutput.txt') fow = vtk.vtkFileOutputWindow() fow.SetFileName(log_path.encode(const.FS_ENCODE)) ow = vtk.vtkOutputWindow() ow.SetInstance(fow) - + filelist = Extract(filename, tempfile.mkdtemp()) dirpath = os.path.abspath(os.path.split(filelist[0])[0]) + self.load_from_folder(dirpath) + def load_from_folder(self, dirpath): + """ + Loads invesalius3 project files from dipath. + """ + import invesalius.data.measures as ms + import invesalius.data.mask as msk + import invesalius.data.surface as srf # Opening the main file from invesalius 3 project main_plist = os.path.join(dirpath ,'main.plist') project = plistlib.readPlist(main_plist) @@ -308,7 +311,7 @@ class Project(with_metaclass(Singleton, object)): self.level = project["window_level"] self.threshold_range = project["scalar_range"] self.spacing = project["spacing"] - if project.get("affine"): + if project.get("affine", ""): self.affine = project["affine"] Publisher.sendMessage('Update affine matrix', affine=self.affine, status=True) @@ -323,7 +326,7 @@ class Project(with_metaclass(Singleton, object)): # Opening the masks self.mask_dict = {} - for index in project["masks"]: + for index in project.get("masks", []): filename = project["masks"][index] filepath = os.path.join(dirpath, filename) m = msk.Mask() @@ -332,7 +335,7 @@ class Project(with_metaclass(Singleton, object)): # Opening the surfaces self.surface_dict = {} - for index in project["surfaces"]: + for index in project.get("surfaces", []): filename = project["surfaces"][index] filepath = os.path.join(dirpath, filename) s = srf.Surface(int(index)) @@ -341,15 +344,50 @@ class Project(with_metaclass(Singleton, object)): # Opening the measurements self.measurement_dict = {} - measurements = plistlib.readPlist(os.path.join(dirpath, - project["measurements"])) - for index in measurements: - if measurements[index]["type"] in (const.DENSITY_ELLIPSE, const.DENSITY_POLYGON): - measure = ms.DensityMeasurement() - else: - measure = ms.Measurement() - measure.Load(measurements[index]) - self.measurement_dict[int(index)] = measure + measures_file = os.path.join(dirpath, project.get("measurements", "measurements.plist")) + if os.path.exists(measures_file): + measurements = plistlib.readPlist(measures_file) + for index in measurements: + if measurements[index]["type"] in (const.DENSITY_ELLIPSE, const.DENSITY_POLYGON): + measure = ms.DensityMeasurement() + else: + measure = ms.Measurement() + measure.Load(measurements[index]) + self.measurement_dict[int(index)] = measure + + def create_project_file(self, name, spacing, modality, orientation, window_width, window_level, image, affine='', folder=None): + if folder is None: + folder = tempfile.mkdtemp() + if not os.path.exists(folder): + os.mkdir(folder) + image_file = os.path.join(folder, 'matrix.dat') + image_mmap = imagedata_utils.array2memmap(image, image_file) + matrix = { + 'filename': 'matrix.dat', + 'shape': image.shape, + 'dtype': str(image.dtype) + } + project = { + # Format info + "format_version": const.INVESALIUS_ACTUAL_FORMAT_VERSION, + "invesalius_version": const.INVESALIUS_VERSION, + "date": datetime.datetime.now().isoformat(), + "compress": True, + + # case info + "name": name, # patient's name + "modality": modality, # CT, RMI, ... + "orientation": orientation, + "window_width": window_width, + "window_level": window_level, + "scalar_range": (int(image.min()), int(image.max())), + "spacing": spacing, + "affine": affine, + + "matrix": matrix, + } + plistlib.writePlist(project, os.path.join(folder, 'main.plist')) + def export_project(self, filename, save_masks=True): if filename.lower().endswith('.hdf5') or filename.lower().endswith('.h5'): -- libgit2 0.21.2