def __copyOverlay(self): """Creates a copy of the currently selected overlay, and inserts it into the :class:`.OverlayList`. """ import wx overlay = self.__displayCtx.getSelectedOverlay() if overlay is None: return # TODO support for other overlay types if type(overlay) != fslimage.Image: raise RuntimeError('Currently, only {} instances can be ' 'copied'.format(fslimage.Image.__name__)) display = self.__displayCtx.getDisplay(overlay) # We ask the user questions three: # - Copy data, or create an empty (a.k.a. mask) image? # - Copy display settings? # - For 4D, copy 4D, or just the current 3D volume? # # Here we build a list of # questions and initial states. options = [] states = [] createMaskSetting = 'fsleyes.actions.copyoverlay.createMask' copyDisplaySetting = 'fsleyes.actions.copyoverlay.copyDisplay' copy4DSetting = 'fsleyes.actions.copyoverlay.copy4D' createMask = fslsettings.read(createMaskSetting, False) copyDisplay = fslsettings.read(copyDisplaySetting, False) copy4D = fslsettings.read(copy4DSetting, False) is4D = len(overlay.shape) > 3 and overlay.shape[3] > 1 options.append(strings.messages['actions.copyoverlay.createMask']) states.append(createMask) options.append(strings.messages['actions.copyoverlay.copyDisplay']) states.append(copyDisplay) if is4D: options.append(strings.messages['actions.copyoverlay.copy4D']) states.append(copy4D) # Ask the user what they want to do dlg = fsldlg.CheckBoxMessageDialog(self.__frame, title=strings.actions[self], message='Copy {}'.format( display.name), cbMessages=options, cbStates=states, yesText='OK', cancelText='Cancel', focus='yes') if dlg.ShowModal() != wx.ID_YES: return createMask = dlg.CheckBoxState(0) copyDisplay = dlg.CheckBoxState(1) if is4D: copy4D = dlg.CheckBoxState(2) fslsettings.write(createMaskSetting, createMask) fslsettings.write(copyDisplaySetting, copyDisplay) if is4D: fslsettings.write(copy4DSetting, copy4D) copyImage(self.__overlayList, self.__displayCtx, overlay, createMask=createMask, copy4D=copy4D, copyDisplay=copyDisplay)
def readDisplayNames(mapType): if mapType == 'cmap': key = 'fsleyes.colourmaps' elif mapType == 'lut': key = 'fsleyes.luts' return fslsettings.read(key, OrderedDict())
def __onLoadButton(self, ev): """Called when the *Load labels* button is pushed. Prompts the user to select a label file to load, then does the following: 1. If the selected label file refers to the currently selected melodic_IC overlay, the labels are applied to the overlay. 2. If the selected label file refers to a different melodic_IC overlay, the user is asked whether they want to load the different melodic_IC file (the default), or whether they want the labels applied to the existing overlay. 3. If the selected label file does not refer to any overlay (it only contains the bad component list), the user is asked whether they want the labels applied to the current melodic_IC overlay. If the number of labels in the file is less than the number of melodic_IC components, the remaining components are labelled as unknown. If the number of labels in the file is greater than the number of melodic_IC components, an error is shown, and nothing is done. """ # The aim of the code beneath the # applyLabels function is to load # a set of component labels, and # to figure out which overlay # they should be added to. # When it has done this, it calls # applyLabels, which applies the # loaded labels to the overlay. def applyLabels(labelFile, overlay, allLabels, newOverlay): # labelFile: Path to the loaded label file # overlay: Overlay to apply them to # allLabels: Loaded labels (list of (component, [label]) tuples) # newOverlay: True if the selected overlay has changed, False # otherwise lut = self.__lut volLabels = self.overlayList.getData(overlay, 'VolumeLabels') ncomps = volLabels.numComponents() nlabels = len(allLabels) # Error: number of labels in the # file is greater than the number # of components in the overlay. if ncomps < nlabels: msg = strings.messages[self, 'wrongNComps'].format( labelFile, overlay.dataSource) title = strings.titles[ self, 'loadError'] wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) return # Number of labels in the file is # less than number of components # in the overlay - we pad the # labels with 'Unknown' elif ncomps > nlabels: for i in range(nlabels, ncomps): allLabels.append(['Unknown']) # Disable notification while applying # labels so the component/label grids # don't confuse themselves. with volLabels.skip(self.__componentGrid.name), \ volLabels.skip(self.__labelGrid .name): volLabels.clear() for comp, lbls in enumerate(allLabels): for lbl in lbls: volLabels.addLabel(comp, lbl) # Make sure a colour in the melodic # lookup table exists for all labels for label in volLabels.getAllLabels(): label = volLabels.getDisplayLabel(label) lutLabel = lut.getByName(label) if lutLabel is None: log.debug('New melodic classification ' 'label: {}'.format(label)) lut.new(label, colour=fslcm.randomBrightColour()) # New overlay was loaded if newOverlay: # Make sure the new image is selected. with props.skip(self.displayCtx, 'selectedOverlay', self.name): self.displayCtx.selectOverlay(overlay) self.__componentGrid.setOverlay(overlay) self.__labelGrid .setOverlay(overlay) # Labels were applied to # already selected overlay. else: self.__componentGrid.refreshTags() self.__labelGrid .refreshTags() # If the current overlay is a compatible # Image, the open file dialog starting # point will be its directory. overlay = self.displayCtx.getSelectedOverlay() selectedIsCompat = isinstance(overlay, fslimage.Image) if selectedIsCompat and overlay.dataSource is not None: loadDir = op.dirname(overlay.dataSource) # Otherwise it will be the most # recent overlay load directory. else: loadDir = fslsettings.read('loadSaveOverlayDir', os.getcwd()) # Ask the user to select a label file dlg = wx.FileDialog( self, message=strings.titles[self, 'loadDialog'], defaultDir=loadDir, style=wx.FD_OPEN) # User cancelled the dialog if dlg.ShowModal() != wx.ID_OK: return # Load the specified label file filename = dlg.GetPath() emsg = strings.messages[self, 'loadError'].format(filename) etitle = strings.titles[ self, 'loadError'] try: with status.reportIfError(msg=emsg, title=etitle): melDir, allLabels = fixlabels.loadLabelFile(filename) except: return # Ok we've got the labels, now # we need to figure out which # overlay to add them to. # If the label file does not refer # to a Melodic directory, and the # current overlay is a compatible # image, apply the labels to the # image. if selectedIsCompat and (melDir is None): applyLabels(filename, overlay, allLabels, False) return # If the label file refers to a # Melodic directory, and the # current overlay is a compatible # image. if selectedIsCompat and (melDir is not None): if isinstance(overlay, fslmelimage.MelodicImage): overlayDir = overlay.getMelodicDir() elif overlay.dataSource is not None: overlayDir = op.dirname(overlay.dataSource) else: overlayDir = 'none' # And both the current overlay and # the label file refer to the same # directory, then we apply the # labels to the curent overlay. if op.abspath(melDir) == op.abspath(overlayDir): applyLabels(filename, overlay, allLabels, False) return # Otherwise, if the overlay and the # label file refer to different # directories... # Ask the user whether they want to load # the image specified in the label file, # or apply the labels to the currently # selected image. dlg = wx.MessageDialog( self, strings.messages[self, 'diffMelDir'].format( melDir, overlayDir), style=wx.ICON_QUESTION | wx.YES_NO | wx.CANCEL) dlg.SetYesNoLabels( strings.messages[self, 'diffMelDir.labels'], strings.messages[self, 'diffMelDir.overlay']) response = dlg.ShowModal() # User cancelled the dialog if response == wx.ID_CANCEL: return # User chose to load the melodic # image specified in the label # file. We'll carry on with this # processing below. elif response == wx.ID_YES: pass # Apply the labels to the current # overlay, even though they are # from different analyses. else: applyLabels(filename, overlay, allLabels, False) return # If we've reached this far, we are # going to attempt to identify the # image associated with the label # file, load that image, and then # apply the labels. # The label file does not # specify a melodic directory if melDir is None: msg = strings.messages[self, 'noMelDir'].format(filename) title = strings.titles[ self, 'loadError'] wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) return # Try loading the melodic_IC image # specified in the label file. try: overlay = fslmelimage.MelodicImage(melDir) log.debug('Adding {} to overlay list'.format(overlay)) with props.skip(self.overlayList, 'overlays', self.name),\ props.skip(self.displayCtx, 'selectedOverlay', self.name): self.overlayList.append(overlay) if self.displayCtx.autoDisplay: autodisplay.autoDisplay(overlay, self.overlayList, self.displayCtx) fslsettings.write('loadSaveOverlayDir', op.abspath(melDir)) except Exception as e: e = str(e) msg = strings.messages[self, 'loadError'].format(filename, e) title = strings.titles[ self, 'loadError'] log.debug('Error loading classification file ' '({}), ({})'.format(filename, e), exc_info=True) wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) # Apply the loaded labels # to the loaded overlay. applyLabels(filename, overlay, allLabels, True)
def __doScreenshot(self): """Capture a screenshot. Prompts the user to select a file to save the screenshot to, and then calls the :func:`screenshot` function. """ lastDirSetting = 'fsleyes.actions.screenshot.lastDir' # Ask the user where they want # the screenshot to be saved fromDir = fslsettings.read(lastDirSetting, os.getcwd()) # We can get a list of supported output # types via a matplotlib figure object fig = plt.figure() fmts = fig.canvas.get_supported_filetypes() # Default to png if # it is available if 'png' in fmts: fmts = [('png', fmts['png'])] + \ [(k, v) for k, v in fmts.items() if k is not 'png'] else: fmts = list(fmts.items()) wildcard = [ '[*.{}] {}|*.{}'.format(fmt, desc, fmt) for fmt, desc in fmts ] wildcard = '|'.join(wildcard) filename = 'screenshot' dlg = wx.FileDialog(self.__panel, message=strings.messages[self, 'screenshot'], wildcard=wildcard, defaultDir=fromDir, defaultFile=filename, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if dlg.ShowModal() != wx.ID_OK: return filename = dlg.GetPath() # Make the dialog go away before # the screenshot gets taken dlg.Close() dlg.Destroy() wx.GetApp().Yield() # Show an error if the screenshot # function raises an error doScreenshot = status.reportErrorDecorator( strings.titles[self, 'error'], strings.messages[self, 'error'])(screenshot) # We do the screenshot asynchronously, # to make sure it is performed on # the main thread, during idle time idle.idle(doScreenshot, self.__panel, filename) status.update(strings.messages[self, 'pleaseWait'].format(filename)) fslsettings.write(lastDirSetting, op.dirname(filename))
def loadDicom(dcmdir=None, parent=None, callback=None): """Does the following: 1. Prompts the user to select a DICOM directory (unless ``dcmdir is not None``) 2. Loads metadata about all of the data series in the DICOM directory 3. Uses a :class:`.BrowseDicomDialog` to allow the user to choose which data series they wish to load 4. Loads the selected series, and passes them to the ``callback`` function if it is provided. :arg dcmdir: Directory to load DICOMs from. If not provided, the user is prompted to select a directory. :arg parent: ``wx`` parent object. :arg callback: Function which is passed the loaded DICOM series (:class:`.Image` objects). """ if parent is None: parent = wx.GetTopLevelWindows()[0] # 1. prompt user to select dicom directory if dcmdir is None: fromDir = fslsettings.read('loadSaveOverlayDir', os.getcwd()) dlg = wx.DirDialog(parent, message=strings.messages['loadDicom.selectDir'], defaultPath=fromDir, style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST) if dlg.ShowModal() != wx.ID_OK: return dcmdir = dlg.GetPath() # 2. load metadata about all data series in # the DICOM directory. This is performed # on a separate thread via the # progress.runWithBounce function. series = [] images = [] def scan(): try: series.extend(fsldcm.scanDir(dcmdir)) if len(series) == 0: raise Exception('Could not find any DICOM ' 'data series in {}'.format(dcmdir)) except Exception as e: series.append(e) # 3. ask user which data series # they want to load. This is called # after the scan function has # finished - see runWithBounce. def postScan(completed): # did the user cancel the progress dialog? if not completed: return # did an error occur in the scan step above? if isinstance(series[0], Exception): errTitle = strings.titles['loadDicom.scanError'] errMsg = strings.messages['loadDicom.scanError'] status.reportError(errTitle, errMsg, series[0]) return dlg = BrowseDicomDialog(parent, series) dlg.CentreOnParent() if dlg.ShowModal() != wx.ID_OK: return # load the selected series - this is # done asynchronously via another # call to runWithBounce. for i in reversed(list(range(len(series)))): if not dlg.IsSelected(i): series.pop(i) title = strings.titles['loadDicom.loading'] msg = strings.messages['loadDicom.loading'] progress.runWithBounce(load, title, msg, callback=postLoad) # 4. Load the selected series. This is run # on a separate thread via runWithBounce def load(): try: for s in series: images.extend(fsldcm.loadSeries(s)) if len(images) == 0: raise Exception('No images could be loaded ' 'from {}'.format(dcmdir)) except Exception as e: images.insert(0, e) # Pass the loaded images to the calback # function. This is called after the # load function has finished. def postLoad(completed): # Did the user cancel the progress dialog? if not completed: return # Did an error occur in the load step above? if isinstance(images[0], Exception): errTitle = strings.titles['loadDicom.loadError'] errMsg = strings.messages['loadDicom.loadError'] status.reportError(errTitle, errMsg, images[0]) return fslsettings.write('loadSaveOverlayDir', op.dirname(dcmdir.rstrip(op.sep))) if callback is not None: callback(images) # Kick off the process title = strings.titles['loadDicom.scanning'] msg = strings.messages['loadDicom.scanning'] progress.runWithBounce(scan, title, msg, callback=postScan)
def __doImport(self): import wx frame = wx.GetApp().GetTopWindow() # Ask the user where to get the data msg = strings.messages[self, 'selectFile'] fromDir = fslsettings.read('loadSaveOverlayDir', os.getcwd()) dlg = wx.FileDialog(frame, message=msg, defaultDir=fromDir, style=wx.FD_OPEN) if dlg.ShowModal() != wx.ID_OK: return filePath = dlg.GetPath() fileName = op.basename(filePath) # Load the file, show an # error if it fails try: # Assuming that the data series # to plot are stored as columns data = np.loadtxt(filePath, dtype=np.float).T # Make sure the data is 2D, to # make code below easier and # happier. if len(data.shape) == 1: data = data.reshape((1, -1)) except Exception as e: title = strings.titles[self, 'error'] msg = strings.messages[self, 'error'].format( filePath, '{}: {}'.format(type(e).__name__, str(e))) wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) return fslsettings.write('loadSaveOverlayDir', filePath) # Ask the user the x axis scaling factor. # If the currently selected overlay is # Nifti and 4D, default to its pixdim[3] overlay = self.displayCtx.getSelectedOverlay() if overlay is not None and \ isinstance(overlay, fslimage.Nifti) and \ len(overlay.shape) == 4 and \ self.__plotPanel.usePixdim: xscale = overlay.pixdim[3] else: xscale = 1 title = strings.titles[self, 'selectXScale'] msg = strings.messages[self, 'selectXScale'] # If the user pushes 'Ok', the entered value # is used as a fixed X axis interval. Otherwise, # it is assumed that the first column in the # file is the x axis data. dlg = numdlg.NumberDialog(frame, title=title, message=msg, initial=xscale, minValue=1e-5, cancelText=strings.labels[self, 'firstColumnIsX']) firstColumnIsX = dlg.ShowModal() != wx.ID_OK xscale = dlg.GetValue() # Add the data series series = [] if firstColumnIsX: xdata = data[0, :] ydata = data[1:, :] else: xdata = np.arange(0, data.shape[1] * xscale, xscale, dtype=np.float) ydata = data for i, ydata in enumerate(ydata): x = np.array(xdata) y = np.array(ydata) fin = np.isfinite(x) & np.isfinite(y) x = x[fin] y = y[fin] ds = plotting.DataSeries(None, self.overlayList, self.displayCtx, self.__plotPanel) ds.setData(x, y) # If we recognise the file name, # we can give it a useful label. label = strings.plotLabels.get('{}.{}'.format(fileName, i), '{} [{}]'.format(fileName, i)) ds.label = label ds.lineWidth = 1 ds.colour = fslcm.randomDarkColour() series.append(ds) self.__plotPanel.dataSeries.extend(series)
def saveOverlay(overlay, display=None): """Saves the currently selected overlay (only if it is a :class:`.Image`), by a call to :meth:`.Image.save`. If a ``display`` is provided, the :attr:`.Display.name` may be updated to match the new overlay file name. :arg overlay: The :class:`.Image` overlay to save :arg display: The :class:`.Display` instance associated with the overlay. """ import wx # TODO support for other overlay types if not isinstance(overlay, fslimage.Image): raise RuntimeError('Non-volumetric types not supported yet') # If this image has been loaded from a file, # ask the user whether they want to overwrite # that file, or save the image to a new file. # if overlay.dataSource is not None: # If the data source is not nifti (e.g. # mgz), we are not going to overwrite it, # so we don't ask. if fslimage.looksLikeImage(overlay.dataSource): msg = strings.messages['SaveOverlayAction.overwrite'].format( overlay.dataSource) title = strings.titles['SaveOverlayAction.overwrite'].format( overlay.dataSource) dlg = wx.MessageDialog(wx.GetTopLevelWindows()[0], message=msg, caption=title, style=(wx.ICON_WARNING | wx.YES_NO | wx.CANCEL | wx.NO_DEFAULT)) dlg.SetYesNoCancelLabels( strings.labels['SaveOverlayAction.overwrite'], strings.labels['SaveOverlayAction.saveNew'], strings.labels['SaveOverlayAction.cancel']) response = dlg.ShowModal() # Cancel == cancel the save # Yes == overwrite the existing file # No == save to a new file (prompt the user for the file name) if response == wx.ID_CANCEL: return if response == wx.ID_YES: doSave(overlay) return fromDir = op.dirname(overlay.dataSource) filename = fslimage.removeExt(op.basename(overlay.dataSource)) filename = '{}_copy'.format(filename) else: fromDir = fslsettings.read('loadSaveOverlayDir', os.getcwd()) if display is not None: filename = display.name else: filename = overlay.name filename = filename.replace('/', '_') filename = filename.replace('\\', '_') # Ask the user where they # want to save the image msg = strings.titles['SaveOverlayAction.saveFile'] dlg = wx.FileDialog(wx.GetApp().GetTopWindow(), message=msg, defaultDir=fromDir, defaultFile=filename, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if dlg.ShowModal() != wx.ID_OK: return # Make sure that the user chose a supported # extension. If not, use the default extension. savePath = dlg.GetPath() prefix, suffix = fslimage.splitExt(savePath) if suffix == '': savePath = '{}{}'.format(prefix, fslimage.defaultExt()) oldPath = overlay.dataSource saveDir = op.dirname(savePath) if doSave(overlay, savePath): # Cache the save directory for next time. fslsettings.write('loadSaveOverlayDir', saveDir) # If image was in memory, or its old # name equalled the old datasource # base name, update its name. if oldPath is None or \ fslimage.removeExt(op.basename(oldPath)) == overlay.name: overlay.name = fslimage.removeExt(op.basename(savePath)) if display is not None: display.name = overlay.name
def __copyOverlay(self): """Creates a copy of the currently selected overlay, and inserts it into the :class:`.OverlayList`. """ import wx overlay = self.displayCtx.getSelectedOverlay() if overlay is None: return # TODO support for other overlay types if type(overlay) != fslimage.Image: raise RuntimeError('Currently, only {} instances can be ' 'copied'.format(fslimage.Image.__name__)) display = self.displayCtx.getDisplay(overlay) # We ask the user questions four: # - Copy data, or create an empty (a.k.a. mask) image? # - Copy display settings? # - For 4D, copy 4D, or just the current 3D volume? # - For complex/RGB(A), copy as single channel, or multi-channel? # # Here we build a list of # questions and initial states. options = [] states = [] createMaskSetting = 'fsleyes.actions.copyoverlay.createMask' copyDisplaySetting = 'fsleyes.actions.copyoverlay.copyDisplay' copy4DSetting = 'fsleyes.actions.copyoverlay.copy4D' copyMultiSetting = 'fsleyes.actions.copyoverlay.copyMulti' createMask = fslsettings.read(createMaskSetting, False) copyDisplay = fslsettings.read(copyDisplaySetting, False) copy4D = fslsettings.read(copy4DSetting, False) copyMulti = fslsettings.read(copy4DSetting, True) is4D = len(overlay.shape) > 3 and overlay.shape[3] > 1 isMulti = overlay.iscomplex or overlay.nvals > 1 options.append(strings.messages['actions.copyoverlay.createMask']) states.append(createMask) options.append(strings.messages['actions.copyoverlay.copyDisplay']) states.append(copyDisplay) if is4D: options.append(strings.messages['actions.copyoverlay.copy4D']) states.append(copy4D) if isMulti: options.append(strings.messages['actions.copyoverlay.copyMulti']) states.append(copyMulti) # Ask the user what they want to do dlg = fsldlg.CheckBoxMessageDialog(self.__frame, title=strings.actions[self], message='Copy {}'.format( display.name), cbMessages=options, cbStates=states, yesText='OK', cancelText='Cancel', focus='yes') if dlg.ShowModal() != wx.ID_YES: return createMask = dlg.CheckBoxState(0) copyDisplay = dlg.CheckBoxState(1) if is4D: copy4D = dlg.CheckBoxState(2) if isMulti: copyMulti = dlg.CheckBoxState(3 if is4D else 2) fslsettings.write(createMaskSetting, createMask) fslsettings.write(copyDisplaySetting, copyDisplay) if is4D: fslsettings.write(copy4DSetting, copy4D) if isMulti: fslsettings.write(copyMultiSetting, copyMulti) # If the user de-selected copy all channels, # ask them which channel they want to copy channel = None if isMulti and (not copyMulti): if overlay.iscomplex: choices = ['real', 'imag'] else: choices = ['R', 'G', 'B', 'A'][:overlay.nvals] labels = [strings.choices[self, 'component'][c] for c in choices] title = strings.titles['actions.copyoverlay.component'] msg = strings.messages['actions.copyoverlay.component'] dlg = wx.SingleChoiceDialog(self.__frame, msg, title, choices=labels) if dlg.ShowModal() != wx.ID_OK: return channel = choices[dlg.GetSelection()] copyImage(self.overlayList, self.displayCtx, overlay, createMask=createMask, copy4D=copy4D, channel=channel, copyDisplay=copyDisplay)
def loadImage(dtype, path, inmem=False): """Called by the :func:`loadOverlays` function. Loads an overlay which is represented by an ``Image`` instance, or a sub-class of ``Image``. Depending upon the image size, the data may be loaded into memory or kept on disk, and the initial image data range may be calculated from the whole image, or from a sample. :arg dtype: Overlay type (``Image``, or a sub-class of ``Image``). :arg path: Path to the overlay file. :arg inmem: If ``True``, ``Image`` overlays are loaded into memory. """ rangethres = fslsettings.read('fsleyes.overlay.rangethres', 419430400) idxthres = fslsettings.read('fsleyes.overlay.idxthres', 1073741824) # We're going to load the file # twice - first to get its # dimensions, and then for real. # # TODO It is annoying that you have to create a 'dtype' # instance twice, as e.g. the MelodicImage does a # bunch of extra stuff (e.g. loading component # time courses) that don't need to be done. Maybe # the path passed to this function could be resolved # (e.g. ./filtered_func.ica/ turned into # ./filtered_func.ica/melodic_IC) so that you can # just create a fsl.data.Image, or a nib.Nifti1Image. image = dtype(path, loadData=False, calcRange=False, indexed=False, threaded=False) nbytes = np.prod(image.shape) * image.dtype.itemsize image = None # If the file is compressed (gzipped), # index the file if its compressed size # is greater than the index threshold. indexed = nbytes > idxthres image = dtype(path, loadData=inmem, calcRange=False, indexed=indexed, threaded=indexed) # If the image is bigger than the # index threshold, keep it on disk. if inmem or (not indexed): log.debug('Loading {} into memory'.format(path)) image.loadData() else: log.debug('Keeping {} on disk'.format(path)) # If the image size is less than the range # threshold, calculate the full data range # now. Otherwise calculate the data range # from a sample. This is handled by the # Image.calcRange method. image.calcRange(rangethres) return image