diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..813dadb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git* +.python-version +Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7ac1233 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM ubuntu:16.04 + +RUN apt-get update +RUN apt-get install -y \ + cython \ + locales \ + python-concurrent.futures \ + python-gdcm \ + python-matplotlib \ + python-nibabel \ + python-numpy \ + python-pil \ + python-psutil \ + python-scipy \ + python-serial \ + python-skimage \ + python-vtk6 \ + python-vtkgdcm \ + python-wxgtk3.0 + +RUN locale-gen en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +WORKDIR /usr/local/app + +COPY . . + +RUN python setup.py build_ext --inplace diff --git a/app.py b/app.py index 032858d..4702406 100755 --- a/app.py +++ b/app.py @@ -24,6 +24,9 @@ import optparse as op import os import sys import shutil +import traceback + +import re if sys.platform == 'win32': import _winreg @@ -218,7 +221,8 @@ class SplashScreen(wx.SplashScreen): self.control = Controller(self.main) self.fc = wx.FutureCall(1, self.ShowMain) - wx.FutureCall(1, parse_comand_line) + options, args = parse_comand_line() + wx.FutureCall(1, use_cmd_optargs, options, args) # Check for updates from threading import Thread @@ -244,6 +248,24 @@ class SplashScreen(wx.SplashScreen): if self.fc.IsRunning(): self.Raise() + +def non_gui_startup(options, args): + lang = 'en' + _ = i18n.InstallLanguage(lang) + + from invesalius.control import Controller + from invesalius.project import Project + + session = ses.Session() + if not session.ReadSession(): + session.CreateItens() + session.SetLanguage(lang) + session.WriteSessionFile() + + control = Controller(None) + + use_cmd_optargs(options, args) + # ------------------------------------------------------------------ @@ -262,39 +284,138 @@ def parse_comand_line(): action="store_true", dest="debug") + parser.add_option('--no-gui', + action='store_true', + dest='no_gui') + # -i or --import: import DICOM directory # chooses largest series parser.add_option("-i", "--import", action="store", dest="dicom_dir") + + parser.add_option("--import-all", + action="store") + + parser.add_option("-s", "--save", + help="Save the project after an import.") + + parser.add_option("-t", "--threshold", + help="Define the threshold for the export (e.g. 100-780).") + + parser.add_option("-e", "--export", + help="Export to STL.") + + parser.add_option("-a", "--export-to-all", + help="Export to STL for all mask presets.") + options, args = parser.parse_args() + return options, args + +def use_cmd_optargs(options, args): # If debug argument... if options.debug: Publisher.subscribe(print_events, Publisher.ALL_TOPICS) + session = ses.Session() session.debug = 1 # If import DICOM argument... if options.dicom_dir: import_dir = options.dicom_dir - Publisher.sendMessage('Import directory', import_dir) + Publisher.sendMessage('Import directory', {'directory': import_dir, 'gui': not options.no_gui}) + + if options.save: + Publisher.sendMessage('Save project', os.path.abspath(options.save)) + exit(0) + + check_for_export(options) + + return True + elif options.import_all: + import invesalius.reader.dicom_reader as dcm + for patient in dcm.GetDicomGroups(options.import_all): + for group in patient.GetGroups(): + Publisher.sendMessage('Import group', {'group': group, 'gui': not options.no_gui}) + check_for_export(options, suffix=group.title, remove_surfaces=False) + Publisher.sendMessage('Remove masks', [0]) return True # Check if there is a file path somewhere in what the user wrote # In case there is, try opening as it was a inv3 else: - i = len(args) - while i: - i -= 1 - file = args[i].decode(sys.stdin.encoding) - if os.path.isfile(file): - path = os.path.abspath(file) - Publisher.sendMessage('Open project', path) - i = 0 + for arg in reversed(args): + if os.path.isfile(arg): + path_ = os.path.abspath(arg.decode(sys.stdin.encoding)) + Publisher.sendMessage('Open project', path_) + + check_for_export(options) return True return False +def sanitize(text): + text = str(text).strip().replace(' ', '_') + return re.sub(r'(?u)[^-\w.]', '', text) + + +def check_for_export(options, suffix='', remove_surfaces=False): + suffix = sanitize(suffix) + + if options.export: + if not options.threshold: + print("Need option --threshold when using --export.") + exit(1) + threshold_range = tuple([int(n) for n in options.threshold.split(',')]) + + if suffix: + if options.export.endswith('.stl'): + path_ = '{}-{}.stl'.format(options.export[:-4], suffix) + else: + path_ = '{}-{}.stl'.format(options.export, suffix) + else: + path_ = options.export + + export(path_, threshold_range, remove_surface=remove_surfaces) + elif options.export_to_all: + # noinspection PyBroadException + try: + from invesalius.project import Project + + for threshold_name, threshold_range in Project().presets.thresh_ct.iteritems(): + if isinstance(threshold_range[0], int): + path_ = u'{}-{}-{}.stl'.format(options.export_to_all, suffix, threshold_name) + export(path_, threshold_range, remove_surface=True) + except: + traceback.print_exc() + finally: + exit(0) + + +def export(path_, threshold_range, remove_surface=False): + import invesalius.constants as const + + Publisher.sendMessage('Set threshold values', threshold_range) + + surface_options = { + 'method': { + 'algorithm': 'Default', + 'options': {}, + }, 'options': { + 'index': 0, + 'name': '', + 'quality': _('Optimal *'), + 'fill': False, + 'keep_largest': False, + 'overwrite': False, + } + } + Publisher.sendMessage('Create surface from index', surface_options) + Publisher.sendMessage('Export surface to file', (path_, const.FILETYPE_STL)) + if remove_surface: + Publisher.sendMessage('Remove surfaces', [0]) + + def print_events(data): """ Print pubsub messages @@ -305,8 +426,13 @@ def main(): """ Initialize InVesalius GUI """ - application = InVesalius(0) - application.MainLoop() + options, args = parse_comand_line() + + if options.no_gui: + non_gui_startup(options, args) + else: + application = InVesalius(0) + application.MainLoop() if __name__ == '__main__': #Is needed because of pyinstaller diff --git a/invesalius/constants.py b/invesalius/constants.py index 2fbf23e..7bbfa8d 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -694,19 +694,19 @@ BTNS_IMG_MKS = {IR1: {0: 'LEI'}, IR2: {1: 'REI'}, IR3: {2: 'NAI'}} -TIPS_IMG = [wx.ToolTip(_("Select left ear in image")), - wx.ToolTip(_("Select right ear in image")), - wx.ToolTip(_("Select nasion in image"))] +TIPS_IMG = [_("Select left ear in image"), + _("Select right ear in image"), + _("Select nasion in image")] BTNS_TRK = {TR1: {3: _('LET')}, TR2: {4: _('RET')}, TR3: {5: _('NAT')}, SET: {6: _('SET')}} -TIPS_TRK = [wx.ToolTip(_("Select left ear with spatial tracker")), - wx.ToolTip(_("Select right ear with spatial tracker")), - wx.ToolTip(_("Select nasion with spatial tracker")), - wx.ToolTip(_("Show set coordinates in image"))] +TIPS_TRK = [_("Select left ear with spatial tracker"), + _("Select right ear with spatial tracker"), + _("Select nasion with spatial tracker"), + _("Show set coordinates in image")] CAL_DIR = os.path.abspath(os.path.join(FILE_PATH, '..', 'navigation', 'mtc_files', 'CalibrationFiles')) MAR_DIR = os.path.abspath(os.path.join(FILE_PATH, '..', 'navigation', 'mtc_files', 'Markers')) diff --git a/invesalius/control.py b/invesalius/control.py index b58193f..3e79e08 100644 --- a/invesalius/control.py +++ b/invesalius/control.py @@ -68,6 +68,7 @@ class Controller(): def __bind_events(self): Publisher.subscribe(self.OnImportMedicalImages, 'Import directory') + Publisher.subscribe(self.OnImportGroup, 'Import group') Publisher.subscribe(self.OnShowDialogImportDirectory, 'Show import directory dialog') Publisher.subscribe(self.OnShowDialogImportOtherFiles, @@ -105,6 +106,8 @@ class Controller(): Publisher.subscribe(self.SetBitmapSpacing, 'Set bitmap spacing') + Publisher.subscribe(self.OnSaveProject, 'Save project') + def SetBitmapSpacing(self, pubsub_evt): proj = prj.Project() proj.spacing = pubsub_evt.data @@ -329,6 +332,10 @@ class Controller(): session.OpenProject(filepath) Publisher.sendMessage("Enable state project", True) + def OnSaveProject(self, pubsub_evt): + path = pubsub_evt.data + self.SaveProject(path) + def SaveProject(self, path=None): Publisher.sendMessage('Begin busy cursor') session = ses.Session() @@ -442,10 +449,11 @@ class Controller(): #----------- to import by command line --------------------------------------------------- def OnImportMedicalImages(self, pubsub_evt): - directory = pubsub_evt.data - self.ImportMedicalImages(directory) + directory = pubsub_evt.data['directory'] + gui = pubsub_evt.data['gui'] + self.ImportMedicalImages(directory, gui) - def ImportMedicalImages(self, directory): + def ImportMedicalImages(self, directory, gui=True): patients_groups = dcm.GetDicomGroups(directory) name = directory.rpartition('\\')[-1].split('.') print "patients: ", patients_groups @@ -453,7 +461,7 @@ class Controller(): if len(patients_groups): # OPTION 1: DICOM group = dcm.SelectLargerDicomGroup(patients_groups) - matrix, matrix_filename, dicom = self.OpenDicomGroup(group, 0, [0, 0], gui=True) + matrix, matrix_filename, dicom = self.OpenDicomGroup(group, 0, [0, 0], gui=gui) self.CreateDicomProject(dicom, matrix, matrix_filename) else: # OPTION 2: NIfTI, Analyze or PAR/REC @@ -476,6 +484,18 @@ class Controller(): self.LoadProject() Publisher.sendMessage("Enable state project", True) + def OnImportGroup(self, pubsub_evt): + group = pubsub_evt.data['group'] + gui = pubsub_evt.data['gui'] + self.ImportGroup(group, gui) + + def ImportGroup(self, group, gui=True): + matrix, matrix_filename, dicom = self.OpenDicomGroup(group, 0, [0, 0], gui=gui) + self.CreateDicomProject(dicom, matrix, matrix_filename) + + self.LoadProject() + Publisher.sendMessage("Enable state project", True) + #------------------------------------------------------------------------------------- def LoadProject(self): @@ -785,7 +805,7 @@ class Controller(): n_slices = len(filelist) resolution_percentage = utils.calculate_resizing_tofitmemory(int(sx), int(sy), n_slices, bits/8) - if resolution_percentage < 1.0: + if resolution_percentage < 1.0 and gui: re_dialog = dialog.ResizeImageDialog() re_dialog.SetValue(int(resolution_percentage*100)) re_dialog_value = re_dialog.ShowModal() diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index ce84b1f..b45d452 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -359,19 +359,22 @@ class Slice(object): # TODO: merge this code with apply_slice_buffer_to_mask b_mask = self.buffer_slices["AXIAL"].mask - n = self.buffer_slices["AXIAL"].index + 1 - self.current_mask.matrix[n, 1:, 1:] = b_mask - self.current_mask.matrix[n, 0, 0] = 1 + if b_mask is not None: + n = self.buffer_slices["AXIAL"].index + 1 + self.current_mask.matrix[n, 1:, 1:] = b_mask + self.current_mask.matrix[n, 0, 0] = 1 b_mask = self.buffer_slices["CORONAL"].mask - n = self.buffer_slices["CORONAL"].index + 1 - self.current_mask.matrix[1:, n, 1:] = b_mask - self.current_mask.matrix[0, n, 0] = 1 + if b_mask is not None: + n = self.buffer_slices["CORONAL"].index + 1 + self.current_mask.matrix[1:, n, 1:] = b_mask + self.current_mask.matrix[0, n, 0] = 1 b_mask = self.buffer_slices["SAGITAL"].mask - n = self.buffer_slices["SAGITAL"].index + 1 - self.current_mask.matrix[1:, 1:, n] = b_mask - self.current_mask.matrix[0, 0, n] = 1 + if b_mask is not None: + n = self.buffer_slices["SAGITAL"].index + 1 + self.current_mask.matrix[1:, 1:, n] = b_mask + self.current_mask.matrix[0, 0, n] = 1 if to_reload: Publisher.sendMessage('Reload actual slice') @@ -888,7 +891,8 @@ class Slice(object): self.current_mask.matrix[n+1, 1:, 1:] = m else: slice_ = self.buffer_slices[orientation].image - self.buffer_slices[orientation].mask = (255 * ((slice_ >= thresh_min) & (slice_ <= thresh_max))).astype('uint8') + if slice_ is not None: + self.buffer_slices[orientation].mask = (255 * ((slice_ >= thresh_min) & (slice_ <= thresh_max))).astype('uint8') # Update viewer #Publisher.sendMessage('Update slice viewer') diff --git a/invesalius/data/surface.py b/invesalius/data/surface.py index 5cd3e4f..64d19ae 100644 --- a/invesalius/data/surface.py +++ b/invesalius/data/surface.py @@ -191,14 +191,15 @@ class SurfaceManager(): if selected_items: for index in selected_items: proj.RemoveSurface(index) - actor = old_dict[index] - for i in old_dict: - if i < index: - new_dict[i] = old_dict[i] - if i > index: - new_dict[i-1] = old_dict[i] - old_dict = new_dict - Publisher.sendMessage('Remove surface actor from viewer', actor) + if index in old_dict: + actor = old_dict[index] + for i in old_dict: + if i < index: + new_dict[i] = old_dict[i] + if i > index: + new_dict[i-1] = old_dict[i] + old_dict = new_dict + Publisher.sendMessage('Remove surface actor from viewer', actor) self.actors_dict = new_dict if self.last_surface_index in selected_items: @@ -674,7 +675,6 @@ class SurfaceManager(): # polydata.SetSource(None) del decimation - to_measure = polydata #to_measure.Register(None) # to_measure.SetSource(None) @@ -713,115 +713,133 @@ class SurfaceManager(): # polydata.DebugOn() del filled_polydata - normals = vtk.vtkPolyDataNormals() - # normals.ReleaseDataFlagOn() - normals_ref = weakref.ref(normals) - normals_ref().AddObserver("ProgressEvent", lambda obj,evt: - UpdateProgress(normals_ref(), _("Creating 3D surface..."))) - normals.SetInputData(polydata) - normals.SetFeatureAngle(80) - normals.AutoOrientNormalsOn() - # normals.GetOutput().ReleaseDataFlagOn() - normals.Update() - del polydata - polydata = normals.GetOutput() - #polydata.Register(None) - # polydata.SetSource(None) - del normals - - # Improve performance - stripper = vtk.vtkStripper() - # stripper.ReleaseDataFlagOn() - stripper_ref = weakref.ref(stripper) - stripper_ref().AddObserver("ProgressEvent", lambda obj,evt: - UpdateProgress(stripper_ref(), _("Creating 3D surface..."))) - stripper.SetInputData(polydata) - stripper.PassThroughCellIdsOn() - stripper.PassThroughPointIdsOn() - # stripper.GetOutput().ReleaseDataFlagOn() - stripper.Update() - del polydata - polydata = stripper.GetOutput() - #polydata.Register(None) - # polydata.SetSource(None) - del stripper + to_measure = polydata - # Map polygonal data (vtkPolyData) to graphics primitives. - mapper = vtk.vtkPolyDataMapper() - mapper.SetInputData(polydata) - mapper.ScalarVisibilityOff() - # mapper.ReleaseDataFlagOn() - mapper.ImmediateModeRenderingOn() # improve performance + # If InVesalius is running without GUI + if wx.GetApp() is None: + proj = prj.Project() + #Create Surface instance + if overwrite: + surface = Surface(index = self.last_surface_index) + proj.ChangeSurface(surface) + else: + surface = Surface(name=surface_name) + index = proj.AddSurface(surface) + surface.index = index + self.last_surface_index = index + surface.colour = colour + surface.polydata = polydata - # Represent an object (geometry & properties) in the rendered scene - actor = vtk.vtkActor() - actor.SetMapper(mapper) - del mapper - #Create Surface instance - if overwrite: - surface = Surface(index = self.last_surface_index) + # With GUI else: - surface = Surface(name=surface_name) - surface.colour = colour - surface.polydata = polydata - del polydata + normals = vtk.vtkPolyDataNormals() + # normals.ReleaseDataFlagOn() + normals_ref = weakref.ref(normals) + normals_ref().AddObserver("ProgressEvent", lambda obj,evt: + UpdateProgress(normals_ref(), _("Creating 3D surface..."))) + normals.SetInputData(polydata) + normals.SetFeatureAngle(80) + normals.AutoOrientNormalsOn() + # normals.GetOutput().ReleaseDataFlagOn() + normals.Update() + del polydata + polydata = normals.GetOutput() + #polydata.Register(None) + # polydata.SetSource(None) + del normals - # Set actor colour and transparency - actor.GetProperty().SetColor(colour) - actor.GetProperty().SetOpacity(1-surface.transparency) + # Improve performance + stripper = vtk.vtkStripper() + # stripper.ReleaseDataFlagOn() + stripper_ref = weakref.ref(stripper) + stripper_ref().AddObserver("ProgressEvent", lambda obj,evt: + UpdateProgress(stripper_ref(), _("Creating 3D surface..."))) + stripper.SetInputData(polydata) + stripper.PassThroughCellIdsOn() + stripper.PassThroughPointIdsOn() + # stripper.GetOutput().ReleaseDataFlagOn() + stripper.Update() + del polydata + polydata = stripper.GetOutput() + #polydata.Register(None) + # polydata.SetSource(None) + del stripper - prop = actor.GetProperty() + # Map polygonal data (vtkPolyData) to graphics primitives. + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(polydata) + mapper.ScalarVisibilityOff() + # mapper.ReleaseDataFlagOn() + mapper.ImmediateModeRenderingOn() # improve performance - interpolation = int(ses.Session().surface_interpolation) + # Represent an object (geometry & properties) in the rendered scene + actor = vtk.vtkActor() + actor.SetMapper(mapper) + del mapper + #Create Surface instance + if overwrite: + surface = Surface(index = self.last_surface_index) + else: + surface = Surface(name=surface_name) + surface.colour = colour + surface.polydata = polydata + del polydata - prop.SetInterpolation(interpolation) + # Set actor colour and transparency + actor.GetProperty().SetColor(colour) + actor.GetProperty().SetOpacity(1-surface.transparency) - proj = prj.Project() - if overwrite: - proj.ChangeSurface(surface) - else: - index = proj.AddSurface(surface) - surface.index = index - self.last_surface_index = index + prop = actor.GetProperty() - session = ses.Session() - session.ChangeProject() + interpolation = int(ses.Session().surface_interpolation) - # The following lines have to be here, otherwise all volumes disappear - measured_polydata = vtk.vtkMassProperties() - # measured_polydata.ReleaseDataFlagOn() - measured_polydata.SetInputData(to_measure) - volume = float(measured_polydata.GetVolume()) - area = float(measured_polydata.GetSurfaceArea()) - surface.volume = volume - surface.area = area - self.last_surface_index = surface.index - del measured_polydata - del to_measure + prop.SetInterpolation(interpolation) - Publisher.sendMessage('Load surface actor into viewer', actor) + proj = prj.Project() + if overwrite: + proj.ChangeSurface(surface) + else: + index = proj.AddSurface(surface) + surface.index = index + self.last_surface_index = index - # Send actor by pubsub to viewer's render - if overwrite and self.actors_dict.keys(): - old_actor = self.actors_dict[self.last_surface_index] - Publisher.sendMessage('Remove surface actor from viewer', old_actor) + session = ses.Session() + session.ChangeProject() - # Save actor for future management tasks - self.actors_dict[surface.index] = actor + measured_polydata = vtk.vtkMassProperties() + # measured_polydata.ReleaseDataFlagOn() + measured_polydata.SetInputData(to_measure) + volume = float(measured_polydata.GetVolume()) + area = float(measured_polydata.GetSurfaceArea()) + surface.volume = volume + surface.area = area + self.last_surface_index = surface.index + del measured_polydata + del to_measure - Publisher.sendMessage('Update surface info in GUI', - (surface.index, surface.name, - surface.colour, surface.volume, - surface.area, - surface.transparency)) - - #When you finalize the progress. The bar is cleaned. - UpdateProgress = vu.ShowProgress(1) - UpdateProgress(0, _("Ready")) - Publisher.sendMessage('Update status text in GUI', _("Ready")) - - Publisher.sendMessage('End busy cursor') - del actor + Publisher.sendMessage('Load surface actor into viewer', actor) + + # Send actor by pubsub to viewer's render + if overwrite and self.actors_dict.keys(): + old_actor = self.actors_dict[self.last_surface_index] + Publisher.sendMessage('Remove surface actor from viewer', old_actor) + + # Save actor for future management tasks + self.actors_dict[surface.index] = actor + + Publisher.sendMessage('Update surface info in GUI', + (surface.index, surface.name, + surface.colour, surface.volume, + surface.area, + surface.transparency)) + + #When you finalize the progress. The bar is cleaned. + UpdateProgress = vu.ShowProgress(1) + UpdateProgress(0, _("Ready")) + Publisher.sendMessage('Update status text in GUI', _("Ready")) + + Publisher.sendMessage('End busy cursor') + del actor def UpdateSurfaceInterpolation(self, pub_evt): interpolation = int(ses.Session().surface_interpolation) diff --git a/invesalius/data/vtk_utils.py b/invesalius/data/vtk_utils.py index c24e224..84b1bd9 100644 --- a/invesalius/data/vtk_utils.py +++ b/invesalius/data/vtk_utils.py @@ -44,7 +44,10 @@ def ShowProgress(number_of_filters = 1, progress = [0] last_obj_progress = [0] if (dialog_type == "ProgressDialog"): - dlg = ProgressDialog(100) + try: + dlg = ProgressDialog(100) + except wx._core.PyNoAppError: + return lambda obj, label: 0 # when the pipeline is larger than 1, we have to consider this object diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index e8e2057..6c0b38e 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -247,7 +247,7 @@ class NeuronavigationPanel(wx.Panel): n = btns_img[k].keys()[0] lab = btns_img[k].values()[0] self.btns_coord[n] = wx.ToggleButton(self, k, label=lab, size=wx.Size(45, 23)) - self.btns_coord[n].SetToolTip(tips_img[n]) + self.btns_coord[n].SetToolTip(wx.ToolTip(tips_img[n])) self.btns_coord[n].Bind(wx.EVT_TOGGLEBUTTON, self.OnImageFiducials) # Push buttons for tracker fiducials @@ -258,7 +258,7 @@ class NeuronavigationPanel(wx.Panel): n = btns_trk[k].keys()[0] lab = btns_trk[k].values()[0] self.btns_coord[n] = wx.Button(self, k, label=lab, size=wx.Size(45, 23)) - self.btns_coord[n].SetToolTip(tips_trk[n-3]) + self.btns_coord[n].SetToolTip(wx.ToolTip(tips_trk[n-3])) # Excepetion for event of button that set image coordinates if n == 6: self.btns_coord[n].Bind(wx.EVT_BUTTON, self.OnSetImageCoordinates) diff --git a/invesalius/utils.py b/invesalius/utils.py index eb329c9..6a7a741 100644 --- a/invesalius/utils.py +++ b/invesalius/utils.py @@ -76,6 +76,7 @@ def debug(error_str): from invesalius.session import Session session = Session() #if session.debug: + print(error_str) def next_copy_name(original_name, names_list): """ -- libgit2 0.21.2