def _initEditor(self, crosshair): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack, crosshair=crosshair) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel) self.editor.setNavigationInterpreter(self.clickReporter) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick) clickReporter2 = ClickReportingInterpreter( self.editor.brushingInterpreter, self.editor.posModel) clickReporter2.rightClickReceived.connect(self._handleEditorRightClick) self.editor.brushingInterpreter = clickReporter2 self.editor.setInteractionMode('navigation') self.volumeEditorWidget.init(self.editor) self.editor._lastImageViewFocus = 0 # Zoom at a 1-1 scale to avoid loading big datasets entirely... for view in self.editor.imageViews: view.doScaleTo(1)
def initEditor(self): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel ) self.editor.setNavigationInterpreter( self.clickReporter ) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick ) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick ) self.editor.newImageView2DFocus.connect(self.setIconToViewMenu) self.editor.setInteractionMode( 'navigation' ) self.volumeEditorWidget.init(self.editor) # The editor's layerstack is in charge of which layer movement buttons are enabled model = self.editor.layerStack if self.__viewerControlWidget is not None: model.canMoveSelectedUp.connect(self.__viewerControlWidget.UpButton.setEnabled) model.canMoveSelectedDown.connect(self.__viewerControlWidget.DownButton.setEnabled) model.canDeleteSelected.connect(self.__viewerControlWidget.DeleteButton.setEnabled) # Connect our layer movement buttons to the appropriate layerstack actions self.__viewerControlWidget.layerWidget.init(model) self.__viewerControlWidget.UpButton.clicked.connect(model.moveSelectedUp) self.__viewerControlWidget.DownButton.clicked.connect(model.moveSelectedDown) self.__viewerControlWidget.DeleteButton.clicked.connect(model.deleteSelected) self.editor._lastImageViewFocus = 0
def initEditor(self): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel) self.editor.setNavigationInterpreter(self.clickReporter) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick) self.editor.newImageView2DFocus.connect(self.setIconToViewMenu) self.editor.setInteractionMode('navigation') self.volumeEditorWidget.init(self.editor) # The editor's layerstack is in charge of which layer movement buttons are enabled model = self.editor.layerStack if self.__viewerControlWidget is not None: model.canMoveSelectedUp.connect( self.__viewerControlWidget.UpButton.setEnabled) model.canMoveSelectedDown.connect( self.__viewerControlWidget.DownButton.setEnabled) model.canDeleteSelected.connect( self.__viewerControlWidget.DeleteButton.setEnabled) # Connect our layer movement buttons to the appropriate layerstack actions self.__viewerControlWidget.layerWidget.init(model) self.__viewerControlWidget.UpButton.clicked.connect( model.moveSelectedUp) self.__viewerControlWidget.DownButton.clicked.connect( model.moveSelectedDown) self.__viewerControlWidget.DeleteButton.clicked.connect( model.deleteSelected) self.editor._lastImageViewFocus = 0
def initEditor(self): shape=self.inputProvider.outputs["Output"].shape self.editor = VolumeEditor(self.layerstack, labelsink=self.labelsrc) self.editor.dataShape = shape self.editor.newImageView2DFocus.connect(self.setIconToViewMenu) #drawing will be enabled when the first label is added self.editor.setInteractionMode( 'navigation' ) self.volumeEditorWidget.init(self.editor) model = self.editor.layerStack self.layerWidget.init(model) self.UpButton.clicked.connect(model.moveSelectedUp) model.canMoveSelectedUp.connect(self.UpButton.setEnabled) self.DownButton.clicked.connect(model.moveSelectedDown) model.canMoveSelectedDown.connect(self.DownButton.setEnabled) self.DeleteButton.clicked.connect(model.deleteSelected) model.canDeleteSelected.connect(self.DeleteButton.setEnabled) self.opLabels.inputs["eraser"].setValue(self.editor.brushingModel.erasingNumber)
def _initEditor(self, crosshair): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack, crosshair=crosshair) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel ) self.editor.setNavigationInterpreter( self.clickReporter ) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick ) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick ) self.editor.newImageView2DFocus.connect(self._setIconToViewMenu) self.editor.setInteractionMode( 'navigation' ) self.volumeEditorWidget.init(self.editor) self.editor._lastImageViewFocus = 0 # Zoom at a 1-1 scale to avoid loading big datasets entirely... for view in self.editor.imageViews: view.doScaleTo(1)
class Main(QMainWindow): haveData = pyqtSignal() dataReadyToView = pyqtSignal() def __init__(self, argv): QMainWindow.__init__(self) #Normalize the data if true self._normalize_data=True if 'notnormalize' in sys.argv: print sys.argv self._normalize_data=False sys.argv.remove('notnormalize') self.opPredict = None self.opTrain = None self.opThreshold = None self._colorTable16 = self._createDefault16ColorColorTable() #self.g = Graph(7, 2048*1024**2*5) self.g = Graph() self.fixableOperators = [] self.featureDlg=None #The old ilastik provided the following scale names: #['Tiny', 'Small', 'Medium', 'Large', 'Huge', 'Megahuge', 'Gigahuge'] #The corresponding scales are: self.featScalesList=[0.3, 0.7, 1, 1.6, 3.5, 5.0, 10.0] self.initUic() #if the filename was specified on command line, load it if len(sys.argv) >= 2: def loadFile(): self._openFile(sys.argv[1:]) QTimer.singleShot(0, loadFile) def setIconToViewMenu(self): self.actionOnly_for_current_view.setIcon(QIcon(self.editor.imageViews[self.editor._lastImageViewFocus]._hud.axisLabel.pixmap())) def initUic(self): p = os.path.split(__file__)[0]+'/' if p == "/": p = "."+p uic.loadUi(p+"MainWindow_cc.ui", self) #connect the window and graph creation to the opening of the file self.actionOpen.triggered.connect(self.openFile) self.actionQuit.triggered.connect(qApp.quit) def toggleDebugPatches(show): self.editor.showDebugPatches = show def fitToScreen(): shape = self.editor.posModel.shape for i, v in enumerate(self.editor.imageViews): s = list(copy.copy(shape)) del s[i] v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0,0,*s))) def fitImage(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].fitImage() def restoreImageToOriginalSize(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].doScaleTo() def rubberBandZoom(): if hasattr(self.editor, '_lastImageViewFocus'): if not self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom: self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = True self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup = self.editor.imageViews[self.editor._lastImageViewFocus].cursor() self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(Qt.CrossCursor) else: self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = False self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup) def hideHud(): if self.editor.imageViews[0]._hud.isVisible(): hide = False else: hide = True for i, v in enumerate(self.editor.imageViews): v.hideHud(hide) def toggleSelectedHud(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].toggleHud() def centerAllImages(): for i, v in enumerate(self.editor.imageViews): v.centerImage() def centerImage(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].centerImage() self.actionOnly_for_current_view.setEnabled(True) self.actionCenterAllImages.triggered.connect(centerAllImages) self.actionCenterImage.triggered.connect(centerImage) self.actionToggleAllHuds.triggered.connect(hideHud) self.actionToggleSelectedHud.triggered.connect(toggleSelectedHud) self.actionShowDebugPatches.toggled.connect(toggleDebugPatches) self.actionFitToScreen.triggered.connect(fitToScreen) self.actionFitImage.triggered.connect(fitImage) self.actionReset_zoom.triggered.connect(restoreImageToOriginalSize) self.actionRubberBandZoom.triggered.connect(rubberBandZoom) self.haveData.connect(self.initGraph) self.dataReadyToView.connect(self.initEditor) self.layerstack = LayerStackModel() model = LabelListModel() self.labelListView.setModel(model) self.labelListModel=model self.labelListModel.rowsAboutToBeRemoved.connect(self.onLabelAboutToBeRemoved) self.labelListModel.labelSelected.connect(self.switchLabel) def onDataChanged(topLeft, bottomRight): firstRow = topLeft.row() lastRow = bottomRight.row() firstCol = topLeft.column() lastCol = bottomRight.column() if lastCol == firstCol == 0: assert(firstRow == lastRow) #only one data item changes at a time #in this case, the actual data (for example color) has changed self.switchColor(firstRow+1, self.labelListModel[firstRow].color) self.editor.scheduleSlicesRedraw() else: #this column is used for the 'delete' buttons, we don't care #about data changed here pass self.labelListModel.dataChanged.connect(onDataChanged) self.AddLabelButton.clicked.connect(self.addLabel) self.SelectFeaturesButton.clicked.connect(self.onFeatureButtonClicked) self.StartClassificationButton.clicked.connect(self.startClassification) self.StartClassificationButton.setEnabled(False) self.checkInteractive.setEnabled(False) self.checkInteractive.toggled.connect(self.toggleInteractive) self.interactionComboBox.currentIndexChanged.connect(self.changeInteractionMode) self.interactionComboBox.setEnabled(False) self.AddThresholdButton.clicked.connect(self.addThresholdOperator) self.AddThresholdButton.setEnabled(False) self.CCButton.clicked.connect(self.addCCOperator) self.CCButton.setEnabled(False) self._initFeatureDlg() def toggleInteractive(self, checked): print "toggling interactive mode to '%r'" % checked #Check if the number of labels in the layer stack is equals to the number of Painted labels if checked==True: labels =numpy.unique(numpy.asarray(self.opLabels.outputs["nonzeroValues"][:].allocate().wait()[0])) nPaintedLabels=labels.shape[0] nLabelsLayers = self.labelListModel.rowCount() selectedFeatures = numpy.asarray(self.featureDlg.featureTableWidget.createSelectedFeaturesBoolMatrix()) if nPaintedLabels!=nLabelsLayers: self.checkInteractive.setCheckState(0) mexBox=QMessageBox() mexBox.setText("Did you forget to paint some labels?") mexBox.setInformativeText("Painted Labels %d \nNumber Active Labels Layers %d"%(nPaintedLabels,self.labelListModel.rowCount())) mexBox.exec_() return if (selectedFeatures==0).all(): self.checkInteractive.setCheckState(0) mexBox=QMessageBox() mexBox.setText("The are no features selected ") mexBox.exec_() return else: self.g.stopGraph() self.g.resumeGraph() self.AddLabelButton.setEnabled(not checked) self.SelectFeaturesButton.setEnabled(not checked) for o in self.fixableOperators: o.inputs["fixAtCurrent"].setValue(not checked) self.labelListModel.allowRemove(not checked) self.editor.scheduleSlicesRedraw() def changeInteractionMode( self, index ): modes = {0: "navigation", 1: "brushing"} self.editor.setInteractionMode( modes[index] ) self.interactionComboBox.setCurrentIndex(index) print "interaction mode switched to", modes[index] def switchLabel(self, row): print "switching to label=%r" % (self.labelListModel[row]) #+1 because first is transparent #FIXME: shouldn't be just row+1 here self.editor.brushingModel.setDrawnNumber(row+1) self.editor.brushingModel.setBrushColor(self.labelListModel[row].color) def switchColor(self, row, color): print "label=%d changes color to %r" % (row, color) self.labellayer.colorTable[row]=color.rgba() self.editor.brushingModel.setBrushColor(color) self.editor.scheduleSlicesRedraw() def addLabel(self): color = QColor(numpy.random.randint(0,255), numpy.random.randint(0,255), numpy.random.randint(0,255)) numLabels = len(self.labelListModel) if numLabels < len(self._colorTable16): color = self._colorTable16[numLabels] self.labellayer.colorTable.append(color.rgba()) self.labelListModel.insertRow(self.labelListModel.rowCount(), Label("Label %d" % (self.labelListModel.rowCount() + 1), color)) nlabels = self.labelListModel.rowCount() if self.opPredict is not None: print "Label added, changing predictions" #re-train the forest now that we have more labels self.opPredict.inputs['LabelsCount'].setValue(nlabels) self.addPredictionLayer(nlabels-1, self.labelListModel._labels[nlabels-1]) #make the new label selected index = self.labelListModel.index(nlabels-1, 1) self.labelListModel._selectionModel.select(index, QItemSelectionModel.ClearAndSelect) #FIXME: this should watch for model changes #drawing will be enabled when the first label is added self.changeInteractionMode( 1 ) self.interactionComboBox.setEnabled(True) def onLabelAboutToBeRemoved(self, parent, start, end): #the user deleted a label, reshape prediction and remove the layer #the interface only allows to remove one label at a time? nout = start-end+1 ncurrent = self.labelListModel.rowCount() print "removing", nout, "out of ", ncurrent if self.opPredict is not None: self.opPredict.inputs['LabelsCount'].setValue(ncurrent-nout) for il in range(start, end+1): labelvalue = self.labelListModel._labels[il] self.removePredictionLayer(labelvalue) self.opLabels.inputs["deleteLabel"].setValue(il+1) self.editor.scheduleSlicesRedraw() def startClassification(self): if self.opTrain is None: #initialize all classification operators print "initializing classification..." opMultiL = Op5ToMulti(self.g) opMultiL.inputs["Input0"].connect(self.opLabels.outputs["Output"]) opMultiLblocks = Op5ToMulti(self.g) opMultiLblocks.inputs["Input0"].connect(self.opLabels.outputs["nonzeroBlocks"]) self.opTrain = OpTrainRandomForestBlocked(self.g) self.opTrain.inputs['Labels'].connect(opMultiL.outputs["Outputs"]) self.opTrain.inputs['Images'].connect(self.opFeatureCache.outputs["Output"]) self.opTrain.inputs["nonzeroLabelBlocks"].connect(opMultiLblocks.outputs["Outputs"]) self.opTrain.inputs['fixClassifier'].setValue(False) opClassifierCache = OpArrayCache(self.g) opClassifierCache.inputs["Input"].connect(self.opTrain.outputs['Classifier']) ################## Prediction self.opPredict=OpPredictRandomForest(self.g) nclasses = self.labelListModel.rowCount() self.opPredict.inputs['LabelsCount'].setValue(nclasses) self.opPredict.inputs['Classifier'].connect(opClassifierCache.outputs['Output']) self.opPredict.inputs['Image'].connect(self.opPF.outputs["Output"]) pCache = OpSlicedBlockedArrayCache(self.g) pCache.inputs["fixAtCurrent"].setValue(False) pCache.inputs["innerBlockShape"].setValue(((1,256,256,1,2),(1,256,1,256,2),(1,1,256,256,2))) pCache.inputs["outerBlockShape"].setValue(((1,256,256,4,2),(1,256,4,256,2),(1,4,256,256,2))) pCache.inputs["Input"].connect(self.opPredict.outputs["PMaps"]) self.pCache = pCache #add prediction results for all classes as separate channels for icl in range(nclasses): self.addPredictionLayer(icl, self.labelListModel._labels[icl]) self.StartClassificationButton.setEnabled(False) self.checkInteractive.setEnabled(True) self.AddThresholdButton.setEnabled(True) def addPredictionLayer(self, icl, ref_label): selector=OpSingleChannelSelector(self.g) selector.inputs["Input"].connect(self.pCache.outputs['Output']) selector.inputs["Index"].setValue(icl) if self.checkInteractive.isChecked(): self.pCache.inputs["fixAtCurrent"].setValue(False) else: self.pCache.inputs["fixAtCurrent"].setValue(True) predictsrc = LazyflowSource(selector.outputs["Output"][0]) def srcName(newName): predictsrc.setObjectName("Prediction for %s" % ref_label.name) srcName("") predictLayer = AlphaModulatedLayer(predictsrc, tintColor=ref_label.color) predictLayer.nameChanged.connect(srcName) def setLayerColor(c): print "as the color of label '%s' has changed, setting layer's '%s' tint color to %r" % (ref_label.name, predictLayer.name, c) predictLayer.tintColor = c ref_label.colorChanged.connect(setLayerColor) def setLayerName(n): newName = "Prediction for %s" % ref_label.name print "as the name of label '%s' has changed, setting layer's '%s' name to '%s'" % (ref_label.name, predictLayer.name, newName) predictLayer.name = newName setLayerName(ref_label.name) ref_label.nameChanged.connect(setLayerName) predictLayer.ref_object = ref_label #make sure that labels (index = 0) stay on top! self.layerstack.insert(1, predictLayer ) self.fixableOperators.append(self.pCache) def removePredictionLayer(self, ref_label): for il, layer in enumerate(self.layerstack): if layer.ref_object==ref_label: print "found the prediction", layer.ref_object, ref_label self.layerstack.removeRows(il, 1) break def addThresholdOperator(self): if self.opThreshold is None: self.opThreshold = OpThreshold(self.g) self.opThreshold.inputs["Input"].connect(self.pCache.outputs["Output"]) #channel, value = ThresholdDlg(self.labelListModel._labels) channel = 0 value = 0.5 ref_label = self.labelListModel._labels[channel] self.opThreshold.inputs["Channel"].setValue(channel) self.opThreshold.inputs["Threshold"].setValue(value) threshsrc = LazyflowSource(self.opThreshold.outputs["Output"][0]) threshsrc.setObjectName("Threshold for %s" % ref_label.name) transparent = QColor(0,0,0,0) white = QColor(255,255,255) colorTable = [transparent.rgba(), white.rgba()] threshLayer = ColortableLayer(threshsrc, colorTable = colorTable ) threshLayer.name = "Threshold for %s" % ref_label.name self.layerstack.insert(1, threshLayer) self.CCButton.setEnabled(True) def addCCOperator(self): self.opCC = OpConnectedComponents(self.g) self.opCC.inputs["Input"].connect(self.opThreshold.outputs["Output"]) #we shouldn't have to define these. But just in case... self.opCC.inputs["Neighborhood"].setValue(6) self.opCC.inputs["Background"].setValue(0) ccsrc = LazyflowSource(self.opCC.outputs["Output"][0]) ccsrc.setObjectName("Connected Components") ctb = colortables.create_default_16bit() ctb.insert(0, QColor(0, 0, 0, 0).rgba()) # make background transparent ccLayer = ColortableLayer(ccsrc, ctb) ccLayer.name = "Connected Components" self.layerstack.insert(1, ccLayer) def openFile(self): fileNames = QFileDialog.getOpenFileNames(self, "Open Image", os.path.abspath(__file__), "Numpy and h5 files (*.npy *.h5)") if fileNames.count() == 0: return self._openFile(fileNames) def _openFile(self, fileNames): self.inputProvider = None fName, fExt = os.path.splitext(str(fileNames[0])) print "Opening Files %r" % fileNames if fExt=='.npy': fileName = fileNames[0] if len(fileNames)>1: print "WARNING: only the first file will be read, multiple file prediction not supported yet" fName, fExt = os.path.splitext(str(fileName)) self.raw = numpy.load(str(fileName)) self.min, self.max = numpy.min(self.raw), numpy.max(self.raw) self.inputProvider = OpArrayPiper(self.g) self.raw = self.raw.view(vigra.VigraArray) self.raw.axistags = vigra.AxisTags( vigra.AxisInfo('t',vigra.AxisType.Time), vigra.AxisInfo('x',vigra.AxisType.Space), vigra.AxisInfo('y',vigra.AxisType.Space), vigra.AxisInfo('z',vigra.AxisType.Space), vigra.AxisInfo('c',vigra.AxisType.Channels)) self.inputProvider.inputs["Input"].setValue(self.raw) elif fExt=='.h5': readerNew=OpH5ReaderBigDataset(self.g) readerNew.inputs["Filenames"].setValue(fileNames) readerNew.inputs["hdf5Path"].setValue("volume/data") readerCache = OpSlicedBlockedArrayCache(self.g) readerCache.inputs["fixAtCurrent"].setValue(False) readerCache.inputs["innerBlockShape"].setValue(((1,256,256,1,2),(1,256,1,256,2),(1,1,256,256,2))) readerCache.inputs["outerBlockShape"].setValue(((1,256,256,4,2),(1,256,4,256,2),(1,4,256,256,2))) readerCache.inputs["Input"].connect(readerNew.outputs["Output"]) self.inputProvider = OpArrayPiper(self.g) self.inputProvider.inputs["Input"].connect(readerCache.outputs["Output"]) else: raise RuntimeError("opening filenames=%r not supported yet" % fileNames) self.haveData.emit() def initGraph(self): shape = self.inputProvider.outputs["Output"].shape srcs = [] minMax = [] normalize = [] print "* Data has shape=%r" % (shape,) #create a layer for each channel of the input: slicer=OpMultiArraySlicer2(self.g) slicer.inputs["Input"].connect(self.inputProvider.outputs["Output"]) slicer.inputs["AxisFlag"].setValue('c') nchannels = shape[-1] for ich in xrange(nchannels): data=slicer.outputs['Slices'][ich][:].allocate().wait() #find the minimum and maximum value for normalization mm = (numpy.min(data), numpy.max(data)) print " - channel %d: min=%r, max=%r" % (ich, mm[0], mm[1]) minMax.append(mm) if self._normalize_data: normalize.append(mm) else: normalize.append((0,255)) layersrc = LazyflowSource(slicer.outputs['Slices'][ich], priority = 100) layersrc.setObjectName("raw data channel=%d" % ich) srcs.append(layersrc) #FIXME: we shouldn't merge channels automatically, but for now it's prettier layer1 = None if nchannels == 1: layer1 = GrayscaleLayer(srcs[0], range=minMax[0], normalize=normalize[0]) print " - showing raw data as grayscale" elif nchannels==2: layer1 = RGBALayer(red = srcs[0], normalizeR=normalize[0], green = srcs[1], normalizeG=normalize[1], range=minMax[0:2]+[(0,255), (0,255)]) print " - showing channel 1 as red, channel 2 as green" elif nchannels==3: layer1 = RGBALayer(red = srcs[0], normalizeR=normalize[0], green = srcs[1], normalizeG=normalize[1], blue = srcs[2], normalizeB=normalize[2], range = minMax[0:3]) print " - showing channel 1 as red, channel 2 as green, channel 3 as blue" else: print "only 1,2 or 3 channels supported so far" return print layer1.name = "Input data" layer1.ref_object = None self.layerstack.append(layer1) opImageList = Op5ToMulti(self.g) opImageList.inputs["Input0"].connect(self.inputProvider.outputs["Output"]) #init the features operator opPF = OpPixelFeaturesPresmoothed(self.g) opPF.inputs["Input"].connect(opImageList.outputs["Outputs"]) opPF.inputs["Scales"].setValue(self.featScalesList) self.opPF=opPF #Caches the features opFeatureCache = OpBlockedArrayCache(self.g) opFeatureCache.inputs["innerBlockShape"].setValue((1,32,32,32,16)) opFeatureCache.inputs["outerBlockShape"].setValue((1,128,128,128,64)) opFeatureCache.inputs["Input"].connect(opPF.outputs["Output"]) opFeatureCache.inputs["fixAtCurrent"].setValue(False) self.opFeatureCache=opFeatureCache self.initLabels() self.dataReadyToView.emit() def initLabels(self): #Add the layer to draw the labels, but don't add any labels shape=self.inputProvider.outputs["Output"].shape self.opLabels = OpBlockedSparseLabelArray(self.g) self.opLabels.inputs["shape"].setValue(shape[:-1] + (1,)) self.opLabels.inputs["blockShape"].setValue((1, 32, 32, 32, 1)) self.opLabels.inputs["eraser"].setValue(100) self.labelsrc = LazyflowSinkSource(self.opLabels, self.opLabels.outputs["Output"], self.opLabels.inputs["Input"]) self.labelsrc.setObjectName("labels") transparent = QColor(0,0,0,0) self.labellayer = ColortableLayer(self.labelsrc, colorTable = [transparent.rgba()] ) self.labellayer.name = "Labels" self.labellayer.ref_object = None self.layerstack.append(self.labellayer) def initEditor(self): shape=self.inputProvider.outputs["Output"].shape self.editor = VolumeEditor(self.layerstack, labelsink=self.labelsrc) self.editor.dataShape = shape self.editor.newImageView2DFocus.connect(self.setIconToViewMenu) #drawing will be enabled when the first label is added self.editor.setInteractionMode( 'navigation' ) self.volumeEditorWidget.init(self.editor) model = self.editor.layerStack self.layerWidget.init(model) self.UpButton.clicked.connect(model.moveSelectedUp) model.canMoveSelectedUp.connect(self.UpButton.setEnabled) self.DownButton.clicked.connect(model.moveSelectedDown) model.canMoveSelectedDown.connect(self.DownButton.setEnabled) self.DeleteButton.clicked.connect(model.deleteSelected) model.canDeleteSelected.connect(self.DeleteButton.setEnabled) self.opLabels.inputs["eraser"].setValue(self.editor.brushingModel.erasingNumber) def _createDefault16ColorColorTable(self): c = [] c.append(QColor(0, 0, 255)) c.append(QColor(255, 255, 0)) c.append(QColor(255, 0, 0)) c.append(QColor(0, 255, 0)) c.append(QColor(0, 255, 255)) c.append(QColor(255, 0, 255)) c.append(QColor(255, 105, 180)) #hot pink c.append(QColor(102, 205, 170)) #dark aquamarine c.append(QColor(165, 42, 42)) #brown c.append(QColor(0, 0, 128)) #navy c.append(QColor(255, 165, 0)) #orange c.append(QColor(173, 255, 47)) #green-yellow c.append(QColor(128,0, 128)) #purple c.append(QColor(192, 192, 192)) #silver c.append(QColor(240, 230, 140)) #khaki c.append(QColor(69, 69, 69)) # dark grey return c def onFeatureButtonClicked(self): self.featureDlg.show() def onDlgAccepted(): self.StartClassificationButton.setEnabled(True) self.featureDlg.accepted.connect(onDlgAccepted) def _onFeaturesChosen(self): selectedFeatures = self.featureDlg.featureTableWidget.createSelectedFeaturesBoolMatrix() print "new feature set:", selectedFeatures self.opPF.inputs['Matrix'].setValue(numpy.asarray(selectedFeatures)) def _initFeatureDlg(self): dlg = self.featureDlg = FeatureDlg() dlg.setWindowTitle("Features") dlg.createFeatureTable({"Features": [FeatureEntry("Gaussian smoothing"), \ FeatureEntry("Laplacian of Gaussian"), \ FeatureEntry("Structure Tensor Eigenvalues"), \ FeatureEntry("Hessian of Gaussian EV"), \ FeatureEntry("Gaussian Gradient Magnitude"), \ FeatureEntry("Difference Of Gaussian")]}, \ self.featScalesList) dlg.setImageToPreView(None) m = [[1,0,0,0,0,0,0],[1,0,0,0,0,0,0],[0,0,0,0,0,0,0],[1,0,0,0,0,0,0],[1,0,0,0,0,0,0],[1,0,0,0,0,0,0]] dlg.featureTableWidget.setSelectedFeatureBoolMatrix(m) dlg.accepted.connect(self._onFeaturesChosen)
class LayerViewerGui(QWidget): """ Implements an applet GUI whose central widget is a VolumeEditor and whose layer controls simply contains a layer list widget. Intended to be used as a subclass for applet GUI objects. Provides: Central widget (viewer), View Menu, and Layer controls Provides an EMPTY applet drawer widget. Subclasses should replace it with their own applet drawer. """ __metaclass__ = LayerViewerGuiMetaclass ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget( self ): return self def appletDrawer(self): return self._drawer def menus( self ): return [self.menuView] # From the .ui file def viewerControlWidget(self): return self.__viewerControlWidget def stopAndCleanUp(self): self._stopped = True # Remove all layers self.layerstack.clear() # Stop rendering for scene in self.editor.imageScenes: if scene._tileProvider: scene._tileProvider.notifyThreadsToStop() scene.joinRendering() for op in self._orphanOperators: op.cleanUp() ########################################### ########################################### @traceLogged(traceLogger) def __init__(self, topLevelOperatorView, additionalMonitoredSlots=[], centralWidgetOnly=False, crosshair=True): """ Constructor. **All** slots of the provided *topLevelOperatorView* will be monitored for changes. Changes include slot resize events, and slot ready/unready status changes. When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget. :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to. :param additionalMonitoredSlots: Optional. Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored). :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls. """ super(LayerViewerGui, self).__init__() self._stopped = False self._initialized = False self.threadRouter = ThreadRouter(self) # For using @threadRouted self.topLevelOperatorView = topLevelOperatorView observedSlots = [] for slot in topLevelOperatorView.inputs.values() + topLevelOperatorView.outputs.values(): if slot.level == 0 or slot.level == 1: observedSlots.append(slot) observedSlots += additionalMonitoredSlots self._orphanOperators = [] # Operators that are owned by this GUI directly (not owned by the top-level operator) self.observedSlots = [] for slot in observedSlots: if slot.level == 0: if not isinstance(slot.stype, ArrayLike): # We don't support visualization of non-Array slots. continue # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0. # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes. opPromoteInput = Op1ToMulti(graph=slot.operator.graph) opPromoteInput.Input.connect(slot) slot = opPromoteInput.Outputs self._orphanOperators.append( opPromoteInput ) # Each slot should now be indexed as slot[layer_index] assert slot.level == 1 self.observedSlots.append( slot ) slot.notifyInserted( bind(self._handleLayerInsertion) ) slot.notifyRemoved( bind(self._handleLayerRemoval) ) for i in range(len(slot)): self._handleLayerInsertion(slot, i) self.layerstack = LayerStackModel() self._initCentralUic() self._initEditor(crosshair=crosshair) self.__viewerControlWidget = None if not centralWidgetOnly: self.initViewerControlUi() # Might be overridden in a subclass. Default implementation loads a standard layer widget. #self._drawer = QWidget( self ) self.initAppletDrawerUi() # Default implementation loads a blank drawer from drawer.ui. def _after_init(self): self._initialized = True self.updateAllLayers() def setupLayers( self ): """ Create a list of layers to be displayed in the central widget. Subclasses should override this method to create the list of layers that can be displayed. For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots. """ layers = [] for multiLayerSlot in self.observedSlots: for j, slot in enumerate(multiLayerSlot): if slot.ready() and slot.meta.axistags is not None: layer = self.createStandardLayerFromSlot(slot) # Name the layer after the slot name. if isinstance( multiLayerSlot.getRealOperator(), Op1ToMulti ): # We attached an 'upleveling' operator, so look upstream for the real slot. layer.name = multiLayerSlot.getRealOperator().Input.partner.name else: layer.name = multiLayerSlot.name + " " + str(j) #layers.append(layer) return layers @traceLogged(traceLogger) def _handleLayerInsertion(self, slot, slotIndex): """ The multislot providing our layers has a new item. Make room for it in the layer GUI and subscribe to updates. """ # When the slot is ready, we'll replace the blank layer with real data slot[slotIndex].notifyReady( bind(self.updateAllLayers) ) slot[slotIndex].notifyUnready( bind(self.updateAllLayers) ) @traceLogged(traceLogger) def _handleLayerRemoval(self, slot, slotIndex): """ An item is about to be removed from the multislot that is providing our layers. Remove the layer from the GUI. """ self.updateAllLayers() def generateAlphaModulatedLayersFromChannels(self, slot): # TODO assert False @classmethod def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False): """ Convenience function. Generates a volumina layer using the given slot. Chooses between grayscale or RGB depending on the number of channels in the slot. * If *slot* has 1 channel, a GrayscaleLayer is created. * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels. * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels. * If *slot* has 4 channels, an RGBA layer is created :param slot: The slot to generate a layer from :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel. If slot has 4 channels, this parameter has no effect. """ def getRange(meta): if 'drange' in meta: return meta.drange if meta.dtype == numpy.uint8: return (0, 255) else: # If we don't know the range of the data, create a layer that is auto-normalized. # See volumina.pixelpipeline.datasources for details. # # Even in the case of integer data, which has more than 255 possible values, # (like uint16), it seems reasonable to use this setting as default return 'autoPercentiles' # Examine channel dimension to determine Grayscale vs. RGB shape = slot.meta.shape normalize = getRange(slot.meta) try: channelAxisIndex = slot.meta.axistags.index('c') #assert channelAxisIndex < len(slot.meta.axistags), \ # "slot %s has shape = %r, axistags = %r, but no channel dimension" \ # % (slot.name, slot.meta.shape, slot.meta.axistags) numChannels = shape[channelAxisIndex] except: numChannels = 1 if numChannels == 4: lastChannelIsAlpha = True if lastChannelIsAlpha: assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha). Your image has {} channels.".format(numChannels) else: assert numChannels <= 3, "Can't display a standard layer with more than three channels (with no alpha). Your image has {} channels.".format(numChannels) if numChannels == 1: assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel" source = LazyflowSource(slot) normSource = NormalizingSource( source, bounds=normalize ) return GrayscaleLayer(normSource) assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \ "Unhandled combination of channels. numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags ) redProvider = OpSingleChannelSelector(graph=slot.graph) redProvider.Input.connect(slot) redProvider.Index.setValue( 0 ) redSource = LazyflowSource( redProvider.Output ) redNormSource = NormalizingSource( redSource, bounds=normalize ) greenProvider = OpSingleChannelSelector(graph=slot.graph) greenProvider.Input.connect(slot) greenProvider.Index.setValue( 1 ) greenSource = LazyflowSource( greenProvider.Output ) greenNormSource = NormalizingSource( greenSource, bounds=normalize ) blueNormSource = None if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha): blueProvider = OpSingleChannelSelector(graph=slot.graph) blueProvider.Input.connect(slot) blueProvider.Index.setValue( 2 ) blueSource = LazyflowSource( blueProvider.Output ) blueNormSource = NormalizingSource( blueSource, bounds=normalize ) alphaNormSource = None if lastChannelIsAlpha: alphaProvider = OpSingleChannelSelector(graph=slot.graph) alphaProvider.Input.connect(slot) alphaProvider.Index.setValue( numChannels-1 ) alphaSource = LazyflowSource( alphaProvider.Output ) alphaNormSource = NormalizingSource( alphaSource, bounds=normalize ) layer = RGBALayer( red=redNormSource, green=greenNormSource, blue=blueNormSource, alpha=alphaNormSource ) return layer @traceLogged(traceLogger) @threadRouted def updateAllLayers(self): if self._stopped or not self._initialized: return # Ask for the updated layer list (usually provided by the subclass) newGuiLayers = self.setupLayers() newNames = set(l.name for l in newGuiLayers) if len(newNames) != len(newGuiLayers): msg = "All layers must have unique names.\n" msg += "You're attempting to use these layer names:\n" msg += str( [l.name for l in newGuiLayers] ) raise RuntimeError(msg) # If the datashape changed, tell the editor # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case... newDataShape = self.determineDatashape() if newDataShape is not None and self.editor.dataShape != newDataShape: self.editor.dataShape = newDataShape #if the image is 2D, do not show the HUD action (issue #190) is2D = numpy.sum(numpy.asarray(newDataShape[1:4]) == 1) == 1 self.menuGui.actionToggleSelectedHud.setVisible(not is2D) self.menuGui.actionToggleAllHuds.setVisible(not is2D) # Find the xyz midpoint midpos5d = [x/2 for x in newDataShape] midpos3d = midpos5d[1:4] # Start in the center of the volume self.editor.posModel.slicingPos = midpos3d self.editor.navCtrl.panSlicingViews( midpos3d, [0,1,2] ) # Old layers are deleted if # (1) They are not in the new set or # (2) Their data has changed for index, oldLayer in reversed(list(enumerate(self.layerstack))): if oldLayer.name not in newNames: needDelete = True else: newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0] needDelete = (newLayer.datasources != oldLayer.datasources) if needDelete: layer = self.layerstack[index] if hasattr(layer, 'shortcutRegistration'): obsoleteShortcut = layer.shortcutRegistration[2] obsoleteShortcut.setEnabled(False) ShortcutManager().unregister( obsoleteShortcut ) self.layerstack.selectRow(index) self.layerstack.deleteSelected() # Insert all layers that aren't already in the layerstack # (Identified by the name attribute) existingNames = set(l.name for l in self.layerstack) for index, layer in enumerate(newGuiLayers): if layer.name not in existingNames: # Insert new self.layerstack.insert( index, layer ) # If this layer has an associated shortcut, register it with the shortcut manager if hasattr(layer, 'shortcutRegistration'): ShortcutManager().register( *layer.shortcutRegistration ) else: # Clean up the layer instance that the client just gave us. # We don't want to use it. if hasattr(layer, 'shortcutRegistration'): shortcut = layer.shortcutRegistration[2] shortcut.setEnabled(False) # Move existing layer to the correct position stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name) self.layerstack.selectRow(stackIndex) while stackIndex > index: self.layerstack.moveSelectedUp() stackIndex -= 1 while stackIndex < index: self.layerstack.moveSelectedDown() stackIndex += 1 @traceLogged(traceLogger) def determineDatashape(self): newDataShape = None for provider in self.observedSlots: for i, slot in enumerate(provider): if newDataShape is None and slot.ready() and slot.meta.axistags is not None: # Use an Op5ifyer adapter to transpose the shape for us. op5 = Op5ifyer( graph=slot.graph ) op5.input.connect( slot ) newDataShape = op5.output.meta.shape # We just needed the operator to determine the transposed shape. # Disconnect it so it can be garbage collected. op5.input.disconnect() if newDataShape is not None: # For now, this base class combines multi-channel images into a single layer, # So, we want the volume editor to behave as though there is only one channel newDataShape = newDataShape[:-1] + (1,) return newDataShape @traceLogged(traceLogger) def initViewerControlUi(self): """ Load the viewer controls GUI, which appears below the applet bar. In our case, the viewer control GUI consists mainly of a layer list. Subclasses should override this if they provide their own viewer control widget. """ localDir = os.path.split(__file__)[0] self.__viewerControlWidget = ViewerControls() # The editor's layerstack is in charge of which layer movement buttons are enabled model = self.editor.layerStack if self.__viewerControlWidget is not None: self.__viewerControlWidget.setupConnections(model) @traceLogged(traceLogger) def initAppletDrawerUi(self): """ By default, this base class provides a blank applet drawer. Override this in a subclass to get a real applet drawer. """ # Load the ui file (find it in our own directory) localDir = os.path.split(__file__)[0] self._drawer = uic.loadUi(localDir+"/drawer.ui") def getAppletDrawerUi(self): return self._drawer @traceLogged(traceLogger) def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] uic.loadUi(localDir+"/centralWidget.ui", self) # Menu is specified in a separate ui file with a dummy window self.menuGui = uic.loadUi(localDir+"/menu.ui") # Save as member so it doesn't get picked up by GC self.menuBar = self.menuGui.menuBar self.menuView = self.menuGui.menuView def toggleDebugPatches(show): self.editor.showDebugPatches = show def setCacheSize( cache_size ): dlg = QDialog(self) layout = QHBoxLayout() layout.addWidget( QLabel("Cached Slices Per View:") ) cache_size = [self.editor.cacheSize] def parseCacheSize( strSize ): try: cache_size[0] = int(strSize) except: pass edit = QLineEdit( str(cache_size[0]), parent=dlg ) edit.textChanged.connect( parseCacheSize ) layout.addWidget( edit ) okButton = QPushButton( "OK", parent=dlg ) okButton.clicked.connect( dlg.accept ) layout.addWidget( okButton ) dlg.setLayout( layout ) dlg.setModal(True) dlg.exec_() self.editor.cacheSize = cache_size[0] def enablePrefetching( enable ): for scene in self.editor.imageScenes: scene.setPrefetchingEnabled( enable ) def fitToScreen(): shape = self.editor.posModel.shape for i, v in enumerate(self.editor.imageViews): s = list(shape) del s[i] v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0,0,*s))) def fitImage(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].fitImage() def restoreImageToOriginalSize(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].doScaleTo() ''' def rubberBandZoom(): if hasattr(self.editor, '_lastImageViewFocus'): if not self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom: self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = True self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup = self.editor.imageViews[self.editor._lastImageViewFocus].cursor() self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(Qt.CrossCursor) else: self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = False self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup) ''' def hideHud(): hide = not self.editor.imageViews[0]._hud.isVisible() for i, v in enumerate(self.editor.imageViews): v.setHudVisible(hide) def toggleSelectedHud(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].toggleHud() def centerAllImages(): for i, v in enumerate(self.editor.imageViews): v.centerImage() def centerImage(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].centerImage() self.menuGui.actionOnly_for_current_view.setEnabled(True) def resetAxes(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageScenes[self.editor._lastImageViewFocus].resetAxes() self.menuGui.actionOnly_for_current_view.setEnabled(True) def resetAllAxes(): for i, s in enumerate(self.editor.imageScenes): s.resetAxes() self.menuGui.actionCenterAllImages.triggered.connect(centerAllImages) self.menuGui.actionCenterImage.triggered.connect(centerImage) self.menuGui.actionToggleAllHuds.triggered.connect(hideHud) self.menuGui.actionResetAllAxes.triggered.connect(resetAllAxes) self.menuGui.actionToggleSelectedHud.triggered.connect(toggleSelectedHud) self.menuGui.actionResetAxes.triggered.connect(resetAxes) self.menuGui.actionShowDebugPatches.toggled.connect(toggleDebugPatches) self.menuGui.actionFitToScreen.triggered.connect(fitToScreen) self.menuGui.actionFitImage.triggered.connect(fitImage) self.menuGui.actionReset_zoom.triggered.connect(restoreImageToOriginalSize) #FIXME: this needs bug fixing #self.menuGui.actionRubberBandZoom.triggered.connect(rubberBandZoom) self.menuGui.actionSetCacheSize.triggered.connect(setCacheSize) self.menuGui.actionUsePrefetching.toggled.connect(enablePrefetching) @traceLogged(traceLogger) def _initEditor(self, crosshair): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack, crosshair=crosshair) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel ) self.editor.setNavigationInterpreter( self.clickReporter ) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick ) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick ) self.editor.newImageView2DFocus.connect(self._setIconToViewMenu) self.editor.setInteractionMode( 'navigation' ) self.volumeEditorWidget.init(self.editor) self.editor._lastImageViewFocus = 0 # Zoom at a 1-1 scale to avoid loading big datasets entirely... for view in self.editor.imageViews: view.doScaleTo(1) @traceLogged(traceLogger) def _setIconToViewMenu(self): """ In the "Only for Current View" menu item of the View menu, show the user which axis is the current one by changing the menu item icon. """ self.menuGui.actionOnly_for_current_view.setIcon(QIcon(self.editor.imageViews[self.editor._lastImageViewFocus]._hud.axisLabel.pixmap())) @traceLogged(traceLogger) def _convertPositionToDataSpace(self, voluminaPosition): taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)} # Find the first lazyflow layer in the stack # We assume that all lazyflow layers have the same axistags dataTags = None for layer in self.layerstack: for datasource in layer.datasources: if isinstance( datasource, NormalizingSource ): datasource = datasource._rawSource if isinstance(datasource, LazyflowSource): dataTags = datasource.dataSlot.meta.axistags if dataTags is not None: break assert dataTags is not None, "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer." position = () for tag in dataTags: position += (taggedPosition[tag.key],) return position def _handleEditorRightClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorRightClick(dataPosition, globalWindowCoordinate) def _handleEditorLeftClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorLeftClick(dataPosition, globalWindowCoordinate) def handleEditorRightClick(self, position5d, globalWindowCoordinate): # Override me pass def handleEditorLeftClick(self, position5d, globalWindowCoordiante): # Override me pass
class LayerViewerGui(QWidget): """ Implements an applet GUI whose central widget is a VolumeEditor and whose layer controls simply contains a layer list widget. Intended to be used as a subclass for applet GUI objects. Provides: Central widget (viewer), View Menu, and Layer controls Provides an EMPTY applet drawer widget. Subclasses should replace it with their own applet drawer. """ __metaclass__ = LayerViewerGuiMetaclass ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget( self ): return self def appletDrawer(self): return self._drawer def menus( self ): debug_mode = ilastik_config.getboolean("ilastik", "debug") return [ self.volumeEditorWidget.getViewMenu(debug_mode) ] def viewerControlWidget(self): return self.__viewerControlWidget def stopAndCleanUp(self): self._stopped = True # Remove all layers self.layerstack.clear() # Stop rendering for scene in self.editor.imageScenes: if scene._tileProvider: scene._tileProvider.notifyThreadsToStop() scene.joinRendering() for op in self._orphanOperators: op.cleanUp() ########################################### ########################################### @traceLogged(traceLogger) def __init__(self, topLevelOperatorView, additionalMonitoredSlots=[], centralWidgetOnly=False, crosshair=True): """ Constructor. **All** slots of the provided *topLevelOperatorView* will be monitored for changes. Changes include slot resize events, and slot ready/unready status changes. When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget. :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to. :param additionalMonitoredSlots: Optional. Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored). :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls. """ super(LayerViewerGui, self).__init__() self._stopped = False self._initialized = False self.threadRouter = ThreadRouter(self) # For using @threadRouted self.topLevelOperatorView = topLevelOperatorView observedSlots = [] for slot in topLevelOperatorView.inputs.values() + topLevelOperatorView.outputs.values(): if slot.level == 0 or slot.level == 1: observedSlots.append(slot) observedSlots += additionalMonitoredSlots self._orphanOperators = [] # Operators that are owned by this GUI directly (not owned by the top-level operator) self.observedSlots = [] for slot in observedSlots: if slot.level == 0: if not isinstance(slot.stype, ArrayLike): # We don't support visualization of non-Array slots. continue # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0. # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes. opPromoteInput = OpWrapSlot(parent=slot.getRealOperator().parent) opPromoteInput.Input.connect(slot) slot = opPromoteInput.Output self._orphanOperators.append( opPromoteInput ) # Each slot should now be indexed as slot[layer_index] assert slot.level == 1 self.observedSlots.append( slot ) slot.notifyInserted( bind(self._handleLayerInsertion) ) slot.notifyRemoved( bind(self._handleLayerRemoval) ) for i in range(len(slot)): self._handleLayerInsertion(slot, i) self.layerstack = LayerStackModel() self._initCentralUic() self._initEditor(crosshair=crosshair) self.__viewerControlWidget = None if not centralWidgetOnly: self.initViewerControlUi() # Might be overridden in a subclass. Default implementation loads a standard layer widget. #self._drawer = QWidget( self ) self.initAppletDrawerUi() # Default implementation loads a blank drawer from drawer.ui. def _after_init(self): self._initialized = True self.updateAllLayers() def setupLayers( self ): """ Create a list of layers to be displayed in the central widget. Subclasses should override this method to create the list of layers that can be displayed. For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots. """ layers = [] for multiLayerSlot in self.observedSlots: for j, slot in enumerate(multiLayerSlot): if slot.ready() and slot.meta.axistags is not None: layer = self.createStandardLayerFromSlot(slot) # Name the layer after the slot name. if isinstance( multiLayerSlot.getRealOperator(), OpWrapSlot ): # We attached an 'upleveling' operator, so look upstream for the real slot. layer.name = multiLayerSlot.getRealOperator().Input.partner.name else: layer.name = multiLayerSlot.name + " " + str(j) layers.append(layer) return layers @traceLogged(traceLogger) def _handleLayerInsertion(self, slot, slotIndex): """ The multislot providing our layers has a new item. Make room for it in the layer GUI and subscribe to updates. """ # When the slot is ready, we'll replace the blank layer with real data slot[slotIndex].notifyReady( bind(self.updateAllLayers) ) slot[slotIndex].notifyUnready( bind(self.updateAllLayers) ) @traceLogged(traceLogger) def _handleLayerRemoval(self, slot, slotIndex): """ An item is about to be removed from the multislot that is providing our layers. Remove the layer from the GUI. """ self.updateAllLayers(slot) def generateAlphaModulatedLayersFromChannels(self, slot): # TODO assert False @classmethod def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False): """ Convenience function. Generates a volumina layer using the given slot. Chooses between grayscale or RGB depending on the number of channels in the slot. * If *slot* has 1 channel or more than 4 channels, a GrayscaleLayer is created. * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels. * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels. * If *slot* has 4 channels, an RGBA layer is created :param slot: The slot to generate a layer from :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel. If slot has 4 channels, this parameter has no effect. """ def getRange(meta): if meta.drange is not None and meta.normalizeDisplay is False: # do not normalize if the user provided a range and set normalization to False return meta.drange else: # If we don't know the range of the data and normalization is allowed # by the user, create a layer that is auto-normalized. # See volumina.pixelpipeline.datasources for details. # # Even in the case of integer data, which has more than 255 possible values, # (like uint16), it seems reasonable to use this setting as default return None # means autoNormalize shape = slot.meta.shape try: channelAxisIndex = slot.meta.axistags.index('c') #assert channelAxisIndex < len(slot.meta.axistags), \ # "slot %s has shape = %r, axistags = %r, but no channel dimension" \ # % (slot.name, slot.meta.shape, slot.meta.axistags) numChannels = shape[channelAxisIndex] axisinfo = slot.meta.axistags["c"].description except: numChannels = 1 axisinfo = "" # == no info on channels given rindex = None bindex = None gindex = None aindex = None if axisinfo == "" or axisinfo == "default": # Examine channel dimension to determine Grayscale vs. RGB if numChannels == 4: lastChannelIsAlpha = True if lastChannelIsAlpha: assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha). Your image has {} channels.".format(numChannels) if numChannels == 1 or (numChannels > 4): assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel" source = LazyflowSource(slot) layer = GrayscaleLayer(source) layer.numberOfChannels = numChannels normalize = getRange(slot.meta) layer.set_normalize(0,normalize) return layer assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \ "Unhandled combination of channels. numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags ) rindex = 0 gindex = 1 if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha): bindex = 2 if lastChannelIsAlpha: aindex = numChannels-1 elif axisinfo == "grayscale": source = LazyflowSource(slot) layer = GrayscaleLayer(source) layer.numberOfChannels = numChannels normalize = getRange(slot.meta) layer.set_normalize(0,normalize) return layer elif axisinfo == "rgba": rindex = 0 if numChannels>=2: gindex = 1 if numChannels>=3: bindex = 2 if numChannels>=4: aindex = numChannels-1 else: raise RuntimeError("unknown channel display mode") redSource = None if rindex is not None: redProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent) redProvider.Input.connect(slot) redProvider.Index.setValue( rindex ) redSource = LazyflowSource( redProvider.Output ) greenSource = None if gindex is not None: greenProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent) greenProvider.Input.connect(slot) greenProvider.Index.setValue( gindex ) greenSource = LazyflowSource( greenProvider.Output ) blueSource = None if bindex is not None: blueProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent) blueProvider.Input.connect(slot) blueProvider.Index.setValue( bindex ) blueSource = LazyflowSource( blueProvider.Output ) alphaSource = None if aindex is not None: alphaProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent) alphaProvider.Input.connect(slot) alphaProvider.Index.setValue( aindex ) alphaSource = LazyflowSource( alphaProvider.Output ) layer = RGBALayer( red=redSource, green=greenSource, blue=blueSource, alpha=alphaSource) normalize = getRange(slot.meta) print "createLayer normalize", normalize for i in xrange(4): if [redSource,greenSource,blueSource,alphaSource][i]: layer.set_normalize(i,normalize) return layer @traceLogged(traceLogger) @threadRouted def updateAllLayers(self, slot=None): if self._stopped or not self._initialized: return if slot is not None and slot.ready() and slot.meta.axistags is None: # Don't update in response to value slots. return # Ask for the updated layer list (usually provided by the subclass) newGuiLayers = self.setupLayers() newNames = set(l.name for l in newGuiLayers) if len(newNames) != len(newGuiLayers): msg = "All layers must have unique names.\n" msg += "You're attempting to use these layer names:\n" msg += str( [l.name for l in newGuiLayers] ) raise RuntimeError(msg) # If the datashape changed, tell the editor # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case... newDataShape = self.determineDatashape() if newDataShape is not None and self.editor.dataShape != newDataShape: self.editor.dataShape = newDataShape # Find the xyz midpoint midpos5d = [x/2 for x in newDataShape] midpos3d = midpos5d[1:4] # Start in the center of the volume self.editor.posModel.slicingPos = midpos3d self.editor.navCtrl.panSlicingViews( midpos3d, [0,1,2] ) # Old layers are deleted if # (1) They are not in the new set or # (2) Their data has changed for index, oldLayer in reversed(list(enumerate(self.layerstack))): if oldLayer.name not in newNames: needDelete = True else: newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0] needDelete = (newLayer.datasources != oldLayer.datasources) if needDelete: layer = self.layerstack[index] if hasattr(layer, 'shortcutRegistration'): obsoleteShortcut = layer.shortcutRegistration[2] obsoleteShortcut.setEnabled(False) ShortcutManager().unregister( obsoleteShortcut ) self.layerstack.selectRow(index) self.layerstack.deleteSelected() # Insert all layers that aren't already in the layerstack # (Identified by the name attribute) existingNames = set(l.name for l in self.layerstack) for index, layer in enumerate(newGuiLayers): if layer.name not in existingNames: # Insert new self.layerstack.insert( index, layer ) # If this layer has an associated shortcut, register it with the shortcut manager if hasattr(layer, 'shortcutRegistration'): ShortcutManager().register( *layer.shortcutRegistration ) else: # Clean up the layer instance that the client just gave us. # We don't want to use it. if hasattr(layer, 'shortcutRegistration'): shortcut = layer.shortcutRegistration[2] shortcut.setEnabled(False) # Move existing layer to the correct position stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name) self.layerstack.selectRow(stackIndex) while stackIndex > index: self.layerstack.moveSelectedUp() stackIndex -= 1 while stackIndex < index: self.layerstack.moveSelectedDown() stackIndex += 1 @traceLogged(traceLogger) def determineDatashape(self): newDataShape = None for provider in self.observedSlots: for i, slot in enumerate(provider): if newDataShape is None and slot.ready() and slot.meta.axistags is not None: # Use an OpReorderAxes adapter to transpose the shape for us. op5 = OpReorderAxes( parent=slot.getRealOperator().parent ) op5.Input.connect( slot ) newDataShape = op5.Output.meta.shape # We just needed the operator to determine the transposed shape. # Disconnect it so it can be garbage collected. op5.Input.disconnect() return newDataShape @traceLogged(traceLogger) def initViewerControlUi(self): """ Load the viewer controls GUI, which appears below the applet bar. In our case, the viewer control GUI consists mainly of a layer list. Subclasses should override this if they provide their own viewer control widget. """ localDir = os.path.split(__file__)[0] self.__viewerControlWidget = ViewerControls() # The editor's layerstack is in charge of which layer movement buttons are enabled model = self.editor.layerStack if self.__viewerControlWidget is not None: self.__viewerControlWidget.setupConnections(model) @traceLogged(traceLogger) def initAppletDrawerUi(self): """ By default, this base class provides a blank applet drawer. Override this in a subclass to get a real applet drawer. """ # Load the ui file (find it in our own directory) localDir = os.path.split(__file__)[0] self._drawer = uic.loadUi(localDir+"/drawer.ui") def getAppletDrawerUi(self): return self._drawer @traceLogged(traceLogger) def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] uic.loadUi(localDir+"/centralWidget.ui", self) @traceLogged(traceLogger) def _initEditor(self, crosshair): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack, crosshair=crosshair) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel ) self.editor.setNavigationInterpreter( self.clickReporter ) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick ) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick ) clickReporter2 = ClickReportingInterpreter( self.editor.brushingInterpreter, self.editor.posModel ) clickReporter2.rightClickReceived.connect( self._handleEditorRightClick ) self.editor.brushingInterpreter = clickReporter2 self.editor.setInteractionMode( 'navigation' ) self.volumeEditorWidget.init(self.editor) self.editor._lastImageViewFocus = 0 # Zoom at a 1-1 scale to avoid loading big datasets entirely... for view in self.editor.imageViews: view.doScaleTo(1) @traceLogged(traceLogger) def _convertPositionToDataSpace(self, voluminaPosition): taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)} # Find the first lazyflow layer in the stack # We assume that all lazyflow layers have the same axistags dataTags = None for layer in self.layerstack: for datasource in layer.datasources: try: # not all datasources have the dataSlot property, find out by trying dataTags = datasource.dataSlot.meta.axistags if dataTags is not None: break except AttributeError: pass if(dataTags is None): raise RuntimeError("Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer.") position = () for tag in dataTags: position += (taggedPosition[tag.key],) return position def _handleEditorRightClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorRightClick(dataPosition, globalWindowCoordinate) def _handleEditorLeftClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorLeftClick(dataPosition, globalWindowCoordinate) def handleEditorRightClick(self, position5d, globalWindowCoordinate): # Override me pass def handleEditorLeftClick(self, position5d, globalWindowCoordiante): # Override me pass
class LayerViewerGui(QMainWindow): """ Implements an applet GUI whose central widget is a VolumeEditor and whose layer controls simply contains a layer list widget. Intended to be used as a subclass for applet GUI objects. Provides: - Central widget (viewer) - View Menu - Layer controls Does NOT provide an applet drawer widget. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget(self): return self def appletDrawers(self): return [('Viewer', QWidget())] def menus(self): return [self.menuView] # From the .ui file def viewerControlWidget(self): return self.__viewerControlWidget def setImageIndex(self, index): self._setImageIndex(index) def reset(self): # Remove all layers self.layerstack.clear() ########################################### ########################################### def operatorForCurrentImage(self): try: return self.topLevelOperator[self.imageIndex] except IndexError: return None @traceLogged(traceLogger) def __init__(self, topLevelOperator): """ Args: observedSlots - A list of slots that we'll listen for changes on. Each must be a multislot with level=1 or level=2. The first index in the multislot is the image index. """ super(LayerViewerGui, self).__init__() self.threadRouter = ThreadRouter(self) # For using @threadRouted self.topLevelOperator = topLevelOperator observedSlots = [] for slot in topLevelOperator.inputs.values( ) + topLevelOperator.outputs.values(): if slot.level == 1 or slot.level == 2: observedSlots.append(slot) self.observedSlots = [] for slot in observedSlots: if slot.level == 1: # The user gave us a slot that is indexed as slot[image] # Wrap the operator so it has the right level. Indexed as: slot[image][0] opPromoteInput = OperatorWrapper(Op1ToMulti, graph=slot.operator.graph) opPromoteInput.Input.connect(slot) slot = opPromoteInput.Outputs # Each slot should now be indexed as slot[image][sub_index] assert slot.level == 2 self.observedSlots.append(slot) self.layerstack = LayerStackModel() self.initAppletDrawerUi( ) # Default implementation loads a blank drawer. self.initCentralUic() self.__viewerControlWidget = None self.initViewerControlUi() self.initEditor() self.imageIndex = -1 self.lastUpdateImageIndex = -1 def handleDatasetInsertion(slot, imageIndex): if self.imageIndex == -1 and self.areProvidersInSync(): self.setImageIndex(imageIndex) for provider in self.observedSlots: provider.notifyInserted(bind(handleDatasetInsertion)) def handleDatasetRemoval(slot, index, finalsize): if finalsize == 0: # Clear everything self.setImageIndex(-1) elif index == self.imageIndex: # Our currently displayed image is being removed. # Switch to the first image (unless that's the one being removed!) newIndex = 0 if index == newIndex: newIndex = 1 self.setImageIndex(newIndex) for provider in self.observedSlots: provider.notifyRemove(bind(handleDatasetRemoval)) def setupLayers(self, currentImageIndex): layers = [] for multiImageSlot in self.observedSlots: if 0 <= currentImageIndex < len(multiImageSlot): multiLayerSlot = multiImageSlot[currentImageIndex] for j, slot in enumerate(multiLayerSlot): if slot.ready(): layer = self.createStandardLayerFromSlot(slot) layer.name = multiImageSlot.name + " " + str(j) layers.append(layer) return layers @traceLogged(traceLogger) def _setImageIndex(self, imageIndex): if self.imageIndex != -1: for provider in self.observedSlots: # We're switching datasets. Unsubscribe from the old one's notifications. provider[self.imageIndex].unregisterInserted( bind(self.handleLayerInsertion)) provider[self.imageIndex].unregisterRemove( bind(self.handleLayerRemoval)) self.imageIndex = imageIndex # Don't repopulate the GUI if there isn't a current dataset. Stop now. if imageIndex is -1: self.layerstack.clear() return # Update the GUI for all layers in the current dataset self.updateAllLayers() # For layers that already exist, subscribe to ready notifications for provider in self.observedSlots: for slotIndex, slot in enumerate(provider): slot.notifyReady(bind(self.updateAllLayers)) slot.notifyUnready(bind(self.updateAllLayers)) # Make sure we're notified if a layer is inserted in the future so we can subscribe to its ready notifications for provider in self.observedSlots: if self.imageIndex < len(provider): provider[self.imageIndex].notifyInserted( bind(self.handleLayerInsertion)) provider[self.imageIndex].notifyRemoved( bind(self.handleLayerRemoval)) def handleLayerInsertion(self, slot, slotIndex): """ The multislot providing our layers has a new item. Make room for it in the layer GUI and subscribe to updates. """ with Tracer(traceLogger): # When the slot is ready, we'll replace the blank layer with real data slot[slotIndex].notifyReady(bind(self.updateAllLayers)) slot[slotIndex].notifyUnready(bind(self.updateAllLayers)) def handleLayerRemoval(self, slot, slotIndex): """ An item is about to be removed from the multislot that is providing our layers. Remove the layer from the GUI. """ with Tracer(traceLogger): self.updateAllLayers() def generateAlphaModulatedLayersFromChannels(self, slot): # TODO assert False @traceLogged(traceLogger) def createStandardLayerFromSlot(self, slot, lastChannelIsAlpha=False): """ Generate a volumina layer using the given slot. Choose between grayscale or RGB depending on the number of channels. """ def getRange(meta): if 'drange' in meta: return meta.drange if numpy.issubdtype(meta.dtype, numpy.integer): # We assume that ints range up to their max possible value, return (0, numpy.iinfo(meta.dtype).max) else: # If we don't know the range of the data, create a layer that is auto-normalized. # See volumina.pixelpipeline.datasources for details. return 'autoPercentiles' # Examine channel dimension to determine Grayscale vs. RGB shape = slot.meta.shape normalize = getRange(slot.meta) try: channelAxisIndex = slot.meta.axistags.index('c') #assert channelAxisIndex < len(slot.meta.axistags), \ # "slot %s has shape = %r, axistags = %r, but no channel dimension" \ # % (slot.name, slot.meta.shape, slot.meta.axistags) numChannels = shape[channelAxisIndex] except: numChannels = 1 if lastChannelIsAlpha: assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha). Your image has {} channels.".format( numChannels) else: assert numChannels <= 3, "Can't display a standard layer with more than three channels (with no alpha). Your image has {} channels.".format( numChannels) if numChannels == 1: assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel" source = LazyflowSource(slot) normSource = NormalizingSource(source, bounds=normalize) return GrayscaleLayer(normSource) assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha) redProvider = OpSingleChannelSelector(graph=slot.graph) redProvider.Input.connect(slot) redProvider.Index.setValue(0) redSource = LazyflowSource(redProvider.Output) redNormSource = NormalizingSource(redSource, bounds=normalize) greenProvider = OpSingleChannelSelector(graph=slot.graph) greenProvider.Input.connect(slot) greenProvider.Index.setValue(1) greenSource = LazyflowSource(greenProvider.Output) greenNormSource = NormalizingSource(greenSource, bounds=normalize) blueNormSource = None if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha): blueProvider = OpSingleChannelSelector(graph=slot.graph) blueProvider.Input.connect(slot) blueProvider.Index.setValue(2) blueSource = LazyflowSource(blueProvider.Output) blueNormSource = NormalizingSource(blueSource, bounds=normalize) alphaNormSource = None if lastChannelIsAlpha: alphaProvider = OpSingleChannelSelector(graph=slot.graph) alphaProvider.Input.connect(slot) alphaProvider.Index.setValue(numChannels - 1) alphaSource = LazyflowSource(alphaProvider.Output) alphaNormSource = NormalizingSource(alphaSource, bounds=normalize) layer = RGBALayer(red=redNormSource, green=greenNormSource, blue=blueNormSource, alpha=alphaNormSource) return layer @traceLogged(traceLogger) def areProvidersInSync(self): """ When an image is appended to the workflow, not all slots are resized simultaneously. We should avoid calling setupLayers() until all the slots have been resized with the new image. """ try: numImages = len(self.observedSlots[0]) except IndexError: # observedSlots is empty pass inSync = True for slot in self.observedSlots: # Check each slot for out-of-sync status except: # - slots that are optional and unconnected # - slots that are not images (e.g. a classifier or other object) if not (slot._optional and slot.partner is None): if len(slot) == 0: inSync = False break elif len(slot[0]) > 0 and slot[0][0].meta.axistags is not None: inSync &= (len(slot) == numImages) return inSync @traceLogged(traceLogger) @threadRouted def updateAllLayers(self): # Check to make sure all layers are in sync # (During image insertions, outputs are resized one at a time.) if not self.areProvidersInSync(): return if self.imageIndex >= 0: # Ask the subclass for the updated layer list newGuiLayers = self.setupLayers(self.imageIndex) else: newGuiLayers = [] newNames = set(l.name for l in newGuiLayers) if len(newNames) != len(newGuiLayers): msg = "All layers must have unique names.\n" msg += "You're attempting to use these layer names:\n" msg += [l.name for l in newGuiLayers] raise RuntimeError() # Copy the old visibilities and opacities if self.imageIndex != self.lastUpdateImageIndex: existing = {l.name: l for l in self.layerstack} for layer in newGuiLayers: if layer.name in existing.keys(): layer.visible = existing[layer.name].visible layer.opacity = existing[layer.name].opacity # Clear all existing layers. self.layerstack.clear() self.lastUpdateImageIndex = self.imageIndex # Zoom at a 1-1 scale to avoid loading big datasets entirely... for view in self.editor.imageViews: view.doScaleTo(1) # If the datashape changed, tell the editor newDataShape = self.determineDatashape() if newDataShape is not None and self.editor.dataShape != newDataShape: self.editor.dataShape = newDataShape # Find the xyz midpoint midpos5d = [x / 2 for x in newDataShape] midpos3d = midpos5d[1:4] # Start in the center of the volume self.editor.posModel.slicingPos = midpos3d self.editor.navCtrl.panSlicingViews(midpos3d, [0, 1, 2]) # If one of the xyz dimensions is 1, the data is 2d. singletonDims = filter(lambda (i, dim): dim == 1, enumerate(newDataShape[1:4])) if len(singletonDims) == 1: # Maximize the slicing view for this axis axis = singletonDims[0][0] self.volumeEditorWidget.quadview.ensureMaximized(axis) # Old layers are deleted if # (1) They are not in the new set or # (2) Their data has changed for index, oldLayer in reversed(list(enumerate(self.layerstack))): if oldLayer.name not in newNames: needDelete = True else: newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0] needDelete = (newLayer.datasources != oldLayer.datasources) if needDelete: layer = self.layerstack[index] if hasattr(layer, 'shortcutRegistration'): obsoleteShortcut = layer.shortcutRegistration[2] obsoleteShortcut.setEnabled(False) ShortcutManager().unregister(obsoleteShortcut) self.layerstack.selectRow(index) self.layerstack.deleteSelected() # Insert all layers that aren't already in the layerstack # (Identified by the name attribute) existingNames = set(l.name for l in self.layerstack) for index, layer in enumerate(newGuiLayers): if layer.name not in existingNames: # Insert new self.layerstack.insert(index, layer) # If this layer has an associated shortcut, register it with the shortcut manager if hasattr(layer, 'shortcutRegistration'): ShortcutManager().register(*layer.shortcutRegistration) else: # Clean up the layer instance that the client just gave us. # We don't want to use it. if hasattr(layer, 'shortcutRegistration'): shortcut = layer.shortcutRegistration[2] shortcut.setEnabled(False) # Move existing layer to the correct positon stackIndex = self.layerstack.findMatchingIndex( lambda l: l.name == layer.name) self.layerstack.selectRow(stackIndex) while stackIndex > index: self.layerstack.moveSelectedUp() stackIndex -= 1 while stackIndex < index: self.layerstack.moveSelectedDown() stackIndex += 1 @traceLogged(traceLogger) def determineDatashape(self): if self.imageIndex < 0: return None newDataShape = None for provider in self.observedSlots: if self.imageIndex < len(provider): for i, slot in enumerate(provider[self.imageIndex]): if newDataShape is None and slot.ready( ) and slot.meta.axistags is not None: # Use an Op5ifyer adapter to transpose the shape for us. op5 = Op5ifyer(graph=slot.graph) op5.input.connect(slot) newDataShape = op5.output.meta.shape # We just needed the operator to determine the transposed shape. # Disconnect it so it can be garbage collected. op5.input.disconnect() if newDataShape is not None: # For now, this base class combines multi-channel images into a single layer, # So, we want the volume editor to behave as though there is only one channel newDataShape = newDataShape[:-1] + (1, ) return newDataShape @traceLogged(traceLogger) def initViewerControlUi(self): """ Load the viewer controls GUI, which appears below the applet bar. In our case, the viewer control GUI consists mainly of a layer list. """ localDir = os.path.split(__file__)[0] self.__viewerControlWidget = uic.loadUi(localDir + "/viewerControls.ui") @traceLogged(traceLogger) def initAppletDrawerUi(self): """ By default, this base class provides a blank applet drawer. Override this in a subclass to get a real applet drawer. """ # Load the ui file (find it in our own directory) localDir = os.path.split(__file__)[0] self._drawer = uic.loadUi(localDir + "/drawer.ui") def getAppletDrawerUi(self): return self._drawer @traceLogged(traceLogger) def initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] uic.loadUi(localDir + "/centralWidget.ui", self) # Menu is specified in a separate ui file with a dummy window self.menuGui = uic.loadUi( localDir + "/menu.ui") # Save as member so it doesn't get picked up by GC self.menuBar = self.menuGui.menuBar self.menuView = self.menuGui.menuView def toggleDebugPatches(show): self.editor.showDebugPatches = show def setCacheSize(cache_size): dlg = QDialog(self) layout = QHBoxLayout() layout.addWidget(QLabel("Cached Slices Per View:")) cache_size = [self.editor.cacheSize] def parseCacheSize(strSize): try: cache_size[0] = int(strSize) except: pass edit = QLineEdit(str(cache_size[0]), parent=dlg) edit.textChanged.connect(parseCacheSize) layout.addWidget(edit) okButton = QPushButton("OK", parent=dlg) okButton.clicked.connect(dlg.accept) layout.addWidget(okButton) dlg.setLayout(layout) dlg.setModal(True) dlg.exec_() self.editor.cacheSize = cache_size[0] def fitToScreen(): shape = self.editor.posModel.shape for i, v in enumerate(self.editor.imageViews): s = list(shape) del s[i] v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0, 0, *s))) def fitImage(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[ self.editor._lastImageViewFocus].fitImage() def restoreImageToOriginalSize(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[ self.editor._lastImageViewFocus].doScaleTo() def rubberBandZoom(): if hasattr(self.editor, '_lastImageViewFocus'): if not self.editor.imageViews[ self.editor._lastImageViewFocus]._isRubberBandZoom: self.editor.imageViews[ self.editor. _lastImageViewFocus]._isRubberBandZoom = True self.editor.imageViews[ self.editor. _lastImageViewFocus]._cursorBackup = self.editor.imageViews[ self.editor._lastImageViewFocus].cursor() self.editor.imageViews[ self.editor._lastImageViewFocus].setCursor( Qt.CrossCursor) else: self.editor.imageViews[ self.editor. _lastImageViewFocus]._isRubberBandZoom = False self.editor.imageViews[ self.editor._lastImageViewFocus].setCursor( self.editor.imageViews[ self.editor._lastImageViewFocus]._cursorBackup) def hideHud(): hide = not self.editor.imageViews[0]._hud.isVisible() for i, v in enumerate(self.editor.imageViews): v.setHudVisible(hide) def toggleSelectedHud(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[ self.editor._lastImageViewFocus].toggleHud() def centerAllImages(): for i, v in enumerate(self.editor.imageViews): v.centerImage() def centerImage(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[ self.editor._lastImageViewFocus].centerImage() self.actionOnly_for_current_view.setEnabled(True) self.menuGui.actionCenterAllImages.triggered.connect(centerAllImages) self.menuGui.actionCenterImage.triggered.connect(centerImage) self.menuGui.actionToggleAllHuds.triggered.connect(hideHud) self.menuGui.actionToggleSelectedHud.triggered.connect( toggleSelectedHud) self.menuGui.actionShowDebugPatches.toggled.connect(toggleDebugPatches) self.menuGui.actionFitToScreen.triggered.connect(fitToScreen) self.menuGui.actionFitImage.triggered.connect(fitImage) self.menuGui.actionReset_zoom.triggered.connect( restoreImageToOriginalSize) self.menuGui.actionRubberBandZoom.triggered.connect(rubberBandZoom) self.menuGui.actionSetCacheSize.triggered.connect(setCacheSize) @traceLogged(traceLogger) def initEditor(self): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel) self.editor.setNavigationInterpreter(self.clickReporter) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick) self.editor.newImageView2DFocus.connect(self.setIconToViewMenu) self.editor.setInteractionMode('navigation') self.volumeEditorWidget.init(self.editor) # The editor's layerstack is in charge of which layer movement buttons are enabled model = self.editor.layerStack if self.__viewerControlWidget is not None: model.canMoveSelectedUp.connect( self.__viewerControlWidget.UpButton.setEnabled) model.canMoveSelectedDown.connect( self.__viewerControlWidget.DownButton.setEnabled) model.canDeleteSelected.connect( self.__viewerControlWidget.DeleteButton.setEnabled) # Connect our layer movement buttons to the appropriate layerstack actions self.__viewerControlWidget.layerWidget.init(model) self.__viewerControlWidget.UpButton.clicked.connect( model.moveSelectedUp) self.__viewerControlWidget.DownButton.clicked.connect( model.moveSelectedDown) self.__viewerControlWidget.DeleteButton.clicked.connect( model.deleteSelected) self.editor._lastImageViewFocus = 0 @traceLogged(traceLogger) def setIconToViewMenu(self): """ In the "Only for Current View" menu item of the View menu, show the user which axis is the current one by changing the menu item icon. """ self.actionOnly_for_current_view.setIcon( QIcon(self.editor.imageViews[ self.editor._lastImageViewFocus]._hud.axisLabel.pixmap())) @traceLogged(traceLogger) def _convertPositionToDataSpace(self, voluminaPosition): taggedPosition = {k: p for k, p in zip('txyzc', voluminaPosition)} # Find the first lazyflow layer in the stack # We assume that all lazyflow layers have the same axistags dataTags = None for layer in self.layerstack: for datasource in layer.datasources: if isinstance(datasource, NormalizingSource): datasource = datasource._rawSource if isinstance(datasource, LazyflowSource): dataTags = datasource.dataSlot.meta.axistags if dataTags is not None: break assert dataTags is not None, "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer." position = () for tag in dataTags: position += (taggedPosition[tag.key], ) return position def _handleEditorRightClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorRightClick(self.imageIndex, dataPosition, globalWindowCoordinate) def _handleEditorLeftClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorLeftClick(self.imageIndex, dataPosition, globalWindowCoordinate) def handleEditorRightClick(self, currentImageIndex, position5d, globalWindowCoordinate): # Override me pass def handleEditorLeftClick(self, currentImageIndex, position5d, globalWindowCoordiante): # Override me pass
class LayerViewerGui(QMainWindow): """ Implements an applet GUI whose central widget is a VolumeEditor and whose layer controls simply contains a layer list widget. Intended to be used as a subclass for applet GUI objects. Provides: - Central widget (viewer) - View Menu - Layer controls Does NOT provide an applet drawer widget. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget( self ): return self def appletDrawers(self): return [('Viewer', QWidget())] def menus( self ): return [self.menuView] # From the .ui file def viewerControlWidget(self): return self.__viewerControlWidget def setImageIndex(self, index): self._setImageIndex(index) def reset(self): # Remove all layers self.layerstack.clear() ########################################### ########################################### def operatorForCurrentImage(self): try: return self.topLevelOperator[self.imageIndex] except IndexError: return None @traceLogged(traceLogger) def __init__(self, topLevelOperator): """ Args: observedSlots - A list of slots that we'll listen for changes on. Each must be a multislot with level=1 or level=2. The first index in the multislot is the image index. """ super(LayerViewerGui, self).__init__() self.threadRouter = ThreadRouter(self) # For using @threadRouted self.topLevelOperator = topLevelOperator observedSlots = [] for slot in topLevelOperator.inputs.values() + topLevelOperator.outputs.values(): if slot.level == 1 or slot.level == 2: observedSlots.append(slot) self.observedSlots = [] for slot in observedSlots: if slot.level == 1: # The user gave us a slot that is indexed as slot[image] # Wrap the operator so it has the right level. Indexed as: slot[image][0] opPromoteInput = OperatorWrapper( Op1ToMulti, graph=slot.operator.graph ) opPromoteInput.Input.connect(slot) slot = opPromoteInput.Outputs # Each slot should now be indexed as slot[image][sub_index] assert slot.level == 2 self.observedSlots.append( slot ) self.layerstack = LayerStackModel() self.initAppletDrawerUi() # Default implementation loads a blank drawer. self.initCentralUic() self.__viewerControlWidget = None self.initViewerControlUi() self.initEditor() self.imageIndex = -1 self.lastUpdateImageIndex = -1 def handleDatasetInsertion(slot, imageIndex): if self.imageIndex == -1 and self.areProvidersInSync(): self.setImageIndex( imageIndex ) for provider in self.observedSlots: provider.notifyInserted( bind( handleDatasetInsertion ) ) def handleDatasetRemoval(slot, index, finalsize): if finalsize == 0: # Clear everything self.setImageIndex(-1) elif index == self.imageIndex: # Our currently displayed image is being removed. # Switch to the first image (unless that's the one being removed!) newIndex = 0 if index == newIndex: newIndex = 1 self.setImageIndex(newIndex) for provider in self.observedSlots: provider.notifyRemove( bind( handleDatasetRemoval ) ) def setupLayers( self, currentImageIndex ): layers = [] for multiImageSlot in self.observedSlots: if 0 <= currentImageIndex < len(multiImageSlot): multiLayerSlot = multiImageSlot[currentImageIndex] for j, slot in enumerate(multiLayerSlot): if slot.ready(): layer = self.createStandardLayerFromSlot(slot) layer.name = multiImageSlot.name + " " + str(j) layers.append(layer) return layers @traceLogged(traceLogger) def _setImageIndex(self, imageIndex): if self.imageIndex != -1: for provider in self.observedSlots: # We're switching datasets. Unsubscribe from the old one's notifications. provider[self.imageIndex].unregisterInserted( bind(self.handleLayerInsertion) ) provider[self.imageIndex].unregisterRemove( bind(self.handleLayerRemoval) ) self.imageIndex = imageIndex # Don't repopulate the GUI if there isn't a current dataset. Stop now. if imageIndex is -1: self.layerstack.clear() return # Update the GUI for all layers in the current dataset self.updateAllLayers() # For layers that already exist, subscribe to ready notifications for provider in self.observedSlots: for slotIndex, slot in enumerate(provider): slot.notifyReady( bind(self.updateAllLayers) ) slot.notifyUnready( bind(self.updateAllLayers) ) # Make sure we're notified if a layer is inserted in the future so we can subscribe to its ready notifications for provider in self.observedSlots: if self.imageIndex < len(provider): provider[self.imageIndex].notifyInserted( bind(self.handleLayerInsertion) ) provider[self.imageIndex].notifyRemoved( bind(self.handleLayerRemoval) ) def handleLayerInsertion(self, slot, slotIndex): """ The multislot providing our layers has a new item. Make room for it in the layer GUI and subscribe to updates. """ with Tracer(traceLogger): # When the slot is ready, we'll replace the blank layer with real data slot[slotIndex].notifyReady( bind(self.updateAllLayers) ) slot[slotIndex].notifyUnready( bind(self.updateAllLayers) ) def handleLayerRemoval(self, slot, slotIndex): """ An item is about to be removed from the multislot that is providing our layers. Remove the layer from the GUI. """ with Tracer(traceLogger): self.updateAllLayers() def generateAlphaModulatedLayersFromChannels(self, slot): # TODO assert False @traceLogged(traceLogger) def createStandardLayerFromSlot(self, slot, lastChannelIsAlpha=False): """ Generate a volumina layer using the given slot. Choose between grayscale or RGB depending on the number of channels. """ def getRange(meta): if 'drange' in meta: return meta.drange if numpy.issubdtype(meta.dtype, numpy.integer): # We assume that ints range up to their max possible value, return (0, numpy.iinfo( meta.dtype ).max) else: # If we don't know the range of the data, create a layer that is auto-normalized. # See volumina.pixelpipeline.datasources for details. return 'autoPercentiles' # Examine channel dimension to determine Grayscale vs. RGB shape = slot.meta.shape normalize = getRange(slot.meta) try: channelAxisIndex = slot.meta.axistags.index('c') #assert channelAxisIndex < len(slot.meta.axistags), \ # "slot %s has shape = %r, axistags = %r, but no channel dimension" \ # % (slot.name, slot.meta.shape, slot.meta.axistags) numChannels = shape[channelAxisIndex] except: numChannels = 1 if lastChannelIsAlpha: assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha). Your image has {} channels.".format(numChannels) else: assert numChannels <= 3, "Can't display a standard layer with more than three channels (with no alpha). Your image has {} channels.".format(numChannels) if numChannels == 1: assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel" source = LazyflowSource(slot) normSource = NormalizingSource( source, bounds=normalize ) return GrayscaleLayer(normSource) assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha) redProvider = OpSingleChannelSelector(graph=slot.graph) redProvider.Input.connect(slot) redProvider.Index.setValue( 0 ) redSource = LazyflowSource( redProvider.Output ) redNormSource = NormalizingSource( redSource, bounds=normalize ) greenProvider = OpSingleChannelSelector(graph=slot.graph) greenProvider.Input.connect(slot) greenProvider.Index.setValue( 1 ) greenSource = LazyflowSource( greenProvider.Output ) greenNormSource = NormalizingSource( greenSource, bounds=normalize ) blueNormSource = None if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha): blueProvider = OpSingleChannelSelector(graph=slot.graph) blueProvider.Input.connect(slot) blueProvider.Index.setValue( 2 ) blueSource = LazyflowSource( blueProvider.Output ) blueNormSource = NormalizingSource( blueSource, bounds=normalize ) alphaNormSource = None if lastChannelIsAlpha: alphaProvider = OpSingleChannelSelector(graph=slot.graph) alphaProvider.Input.connect(slot) alphaProvider.Index.setValue( numChannels-1 ) alphaSource = LazyflowSource( alphaProvider.Output ) alphaNormSource = NormalizingSource( alphaSource, bounds=normalize ) layer = RGBALayer( red=redNormSource, green=greenNormSource, blue=blueNormSource, alpha=alphaNormSource ) return layer @traceLogged(traceLogger) def areProvidersInSync(self): """ When an image is appended to the workflow, not all slots are resized simultaneously. We should avoid calling setupLayers() until all the slots have been resized with the new image. """ try: numImages = len(self.observedSlots[0]) except IndexError: # observedSlots is empty pass inSync = True for slot in self.observedSlots: # Check each slot for out-of-sync status except: # - slots that are optional and unconnected # - slots that are not images (e.g. a classifier or other object) if not (slot._optional and slot.partner is None): if len(slot) == 0: inSync = False break elif len(slot[0]) > 0 and slot[0][0].meta.axistags is not None: inSync &= (len(slot) == numImages) return inSync @traceLogged(traceLogger) @threadRouted def updateAllLayers(self): # Check to make sure all layers are in sync # (During image insertions, outputs are resized one at a time.) if not self.areProvidersInSync(): return if self.imageIndex >= 0: # Ask the subclass for the updated layer list newGuiLayers = self.setupLayers(self.imageIndex) else: newGuiLayers = [] newNames = set(l.name for l in newGuiLayers) if len(newNames) != len(newGuiLayers): msg = "All layers must have unique names.\n" msg += "You're attempting to use these layer names:\n" msg += [l.name for l in newGuiLayers] raise RuntimeError() # Copy the old visibilities and opacities if self.imageIndex != self.lastUpdateImageIndex: existing = {l.name : l for l in self.layerstack} for layer in newGuiLayers: if layer.name in existing.keys(): layer.visible = existing[layer.name].visible layer.opacity = existing[layer.name].opacity # Clear all existing layers. self.layerstack.clear() self.lastUpdateImageIndex = self.imageIndex # Zoom at a 1-1 scale to avoid loading big datasets entirely... for view in self.editor.imageViews: view.doScaleTo(1) # If the datashape changed, tell the editor newDataShape = self.determineDatashape() if newDataShape is not None and self.editor.dataShape != newDataShape: self.editor.dataShape = newDataShape # Find the xyz midpoint midpos5d = [x/2 for x in newDataShape] midpos3d = midpos5d[1:4] # Start in the center of the volume self.editor.posModel.slicingPos = midpos3d self.editor.navCtrl.panSlicingViews( midpos3d, [0,1,2] ) # If one of the xyz dimensions is 1, the data is 2d. singletonDims = filter( lambda (i,dim): dim == 1, enumerate(newDataShape[1:4]) ) if len(singletonDims) == 1: # Maximize the slicing view for this axis axis = singletonDims[0][0] self.volumeEditorWidget.quadview.ensureMaximized(axis) # Old layers are deleted if # (1) They are not in the new set or # (2) Their data has changed for index, oldLayer in reversed(list(enumerate(self.layerstack))): if oldLayer.name not in newNames: needDelete = True else: newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0] needDelete = (newLayer.datasources != oldLayer.datasources) if needDelete: layer = self.layerstack[index] if hasattr(layer, 'shortcutRegistration'): obsoleteShortcut = layer.shortcutRegistration[2] obsoleteShortcut.setEnabled(False) ShortcutManager().unregister( obsoleteShortcut ) self.layerstack.selectRow(index) self.layerstack.deleteSelected() # Insert all layers that aren't already in the layerstack # (Identified by the name attribute) existingNames = set(l.name for l in self.layerstack) for index, layer in enumerate(newGuiLayers): if layer.name not in existingNames: # Insert new self.layerstack.insert( index, layer ) # If this layer has an associated shortcut, register it with the shortcut manager if hasattr(layer, 'shortcutRegistration'): ShortcutManager().register( *layer.shortcutRegistration ) else: # Clean up the layer instance that the client just gave us. # We don't want to use it. if hasattr(layer, 'shortcutRegistration'): shortcut = layer.shortcutRegistration[2] shortcut.setEnabled(False) # Move existing layer to the correct positon stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name) self.layerstack.selectRow(stackIndex) while stackIndex > index: self.layerstack.moveSelectedUp() stackIndex -= 1 while stackIndex < index: self.layerstack.moveSelectedDown() stackIndex += 1 @traceLogged(traceLogger) def determineDatashape(self): if self.imageIndex < 0: return None newDataShape = None for provider in self.observedSlots: if self.imageIndex < len(provider): for i, slot in enumerate(provider[self.imageIndex]): if newDataShape is None and slot.ready() and slot.meta.axistags is not None: # Use an Op5ifyer adapter to transpose the shape for us. op5 = Op5ifyer( graph=slot.graph ) op5.input.connect( slot ) newDataShape = op5.output.meta.shape # We just needed the operator to determine the transposed shape. # Disconnect it so it can be garbage collected. op5.input.disconnect() if newDataShape is not None: # For now, this base class combines multi-channel images into a single layer, # So, we want the volume editor to behave as though there is only one channel newDataShape = newDataShape[:-1] + (1,) return newDataShape @traceLogged(traceLogger) def initViewerControlUi(self): """ Load the viewer controls GUI, which appears below the applet bar. In our case, the viewer control GUI consists mainly of a layer list. """ localDir = os.path.split(__file__)[0] self.__viewerControlWidget = uic.loadUi(localDir + "/viewerControls.ui") @traceLogged(traceLogger) def initAppletDrawerUi(self): """ By default, this base class provides a blank applet drawer. Override this in a subclass to get a real applet drawer. """ # Load the ui file (find it in our own directory) localDir = os.path.split(__file__)[0] self._drawer = uic.loadUi(localDir+"/drawer.ui") def getAppletDrawerUi(self): return self._drawer @traceLogged(traceLogger) def initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] uic.loadUi(localDir+"/centralWidget.ui", self) # Menu is specified in a separate ui file with a dummy window self.menuGui = uic.loadUi(localDir+"/menu.ui") # Save as member so it doesn't get picked up by GC self.menuBar = self.menuGui.menuBar self.menuView = self.menuGui.menuView def toggleDebugPatches(show): self.editor.showDebugPatches = show def setCacheSize( cache_size ): dlg = QDialog(self) layout = QHBoxLayout() layout.addWidget( QLabel("Cached Slices Per View:") ) cache_size = [self.editor.cacheSize] def parseCacheSize( strSize ): try: cache_size[0] = int(strSize) except: pass edit = QLineEdit( str(cache_size[0]), parent=dlg ) edit.textChanged.connect( parseCacheSize ) layout.addWidget( edit ) okButton = QPushButton( "OK", parent=dlg ) okButton.clicked.connect( dlg.accept ) layout.addWidget( okButton ) dlg.setLayout( layout ) dlg.setModal(True) dlg.exec_() self.editor.cacheSize = cache_size[0] def fitToScreen(): shape = self.editor.posModel.shape for i, v in enumerate(self.editor.imageViews): s = list(shape) del s[i] v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0,0,*s))) def fitImage(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].fitImage() def restoreImageToOriginalSize(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].doScaleTo() def rubberBandZoom(): if hasattr(self.editor, '_lastImageViewFocus'): if not self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom: self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = True self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup = self.editor.imageViews[self.editor._lastImageViewFocus].cursor() self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(Qt.CrossCursor) else: self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = False self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup) def hideHud(): hide = not self.editor.imageViews[0]._hud.isVisible() for i, v in enumerate(self.editor.imageViews): v.setHudVisible(hide) def toggleSelectedHud(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].toggleHud() def centerAllImages(): for i, v in enumerate(self.editor.imageViews): v.centerImage() def centerImage(): if hasattr(self.editor, '_lastImageViewFocus'): self.editor.imageViews[self.editor._lastImageViewFocus].centerImage() self.actionOnly_for_current_view.setEnabled(True) self.menuGui.actionCenterAllImages.triggered.connect(centerAllImages) self.menuGui.actionCenterImage.triggered.connect(centerImage) self.menuGui.actionToggleAllHuds.triggered.connect(hideHud) self.menuGui.actionToggleSelectedHud.triggered.connect(toggleSelectedHud) self.menuGui.actionShowDebugPatches.toggled.connect(toggleDebugPatches) self.menuGui.actionFitToScreen.triggered.connect(fitToScreen) self.menuGui.actionFitImage.triggered.connect(fitImage) self.menuGui.actionReset_zoom.triggered.connect(restoreImageToOriginalSize) self.menuGui.actionRubberBandZoom.triggered.connect(rubberBandZoom) self.menuGui.actionSetCacheSize.triggered.connect(setCacheSize) @traceLogged(traceLogger) def initEditor(self): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel ) self.editor.setNavigationInterpreter( self.clickReporter ) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick ) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick ) self.editor.newImageView2DFocus.connect(self.setIconToViewMenu) self.editor.setInteractionMode( 'navigation' ) self.volumeEditorWidget.init(self.editor) # The editor's layerstack is in charge of which layer movement buttons are enabled model = self.editor.layerStack if self.__viewerControlWidget is not None: model.canMoveSelectedUp.connect(self.__viewerControlWidget.UpButton.setEnabled) model.canMoveSelectedDown.connect(self.__viewerControlWidget.DownButton.setEnabled) model.canDeleteSelected.connect(self.__viewerControlWidget.DeleteButton.setEnabled) # Connect our layer movement buttons to the appropriate layerstack actions self.__viewerControlWidget.layerWidget.init(model) self.__viewerControlWidget.UpButton.clicked.connect(model.moveSelectedUp) self.__viewerControlWidget.DownButton.clicked.connect(model.moveSelectedDown) self.__viewerControlWidget.DeleteButton.clicked.connect(model.deleteSelected) self.editor._lastImageViewFocus = 0 @traceLogged(traceLogger) def setIconToViewMenu(self): """ In the "Only for Current View" menu item of the View menu, show the user which axis is the current one by changing the menu item icon. """ self.actionOnly_for_current_view.setIcon(QIcon(self.editor.imageViews[self.editor._lastImageViewFocus]._hud.axisLabel.pixmap())) @traceLogged(traceLogger) def _convertPositionToDataSpace(self, voluminaPosition): taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)} # Find the first lazyflow layer in the stack # We assume that all lazyflow layers have the same axistags dataTags = None for layer in self.layerstack: for datasource in layer.datasources: if isinstance( datasource, NormalizingSource ): datasource = datasource._rawSource if isinstance(datasource, LazyflowSource): dataTags = datasource.dataSlot.meta.axistags if dataTags is not None: break assert dataTags is not None, "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer." position = () for tag in dataTags: position += (taggedPosition[tag.key],) return position def _handleEditorRightClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorRightClick(self.imageIndex, dataPosition, globalWindowCoordinate) def _handleEditorLeftClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorLeftClick(self.imageIndex, dataPosition, globalWindowCoordinate) def handleEditorRightClick(self, currentImageIndex, position5d, globalWindowCoordinate): # Override me pass def handleEditorLeftClick(self, currentImageIndex, position5d, globalWindowCoordiante): # Override me pass
class LayerViewerGui(QWidget): """ Implements an applet GUI whose central widget is a VolumeEditor and whose layer controls simply contains a layer list widget. Intended to be used as a subclass for applet GUI objects. Provides: Central widget (viewer), View Menu, and Layer controls Provides an EMPTY applet drawer widget. Subclasses should replace it with their own applet drawer. """ __metaclass__ = LayerViewerGuiMetaclass ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget(self): return self def appletDrawer(self): return self._drawer def menus(self): debug_mode = ilastik_config.getboolean("ilastik", "debug") return [self.volumeEditorWidget.getViewMenu(debug_mode)] def viewerControlWidget(self): return self.__viewerControlWidget def stopAndCleanUp(self): self._stopped = True # Remove all layers self.layerstack.clear() # Unsubscribe to all signals for fn in self.__cleanup_fns: fn() # Stop rendering for scene in self.editor.imageScenes: if scene._tileProvider: scene._tileProvider.notifyThreadsToStop() scene.joinRendering() for op in self._orphanOperators: op.cleanUp() ########################################### ########################################### def __init__(self, topLevelOperatorView, additionalMonitoredSlots=[], centralWidgetOnly=False, crosshair=True): """ Constructor. **All** slots of the provided *topLevelOperatorView* will be monitored for changes. Changes include slot resize events, and slot ready/unready status changes. When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget. :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to. :param additionalMonitoredSlots: Optional. Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored). :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls. """ super(LayerViewerGui, self).__init__() self._stopped = False self._initialized = False self.__cleanup_fns = [] self.threadRouter = ThreadRouter(self) # For using @threadRouted self.topLevelOperatorView = topLevelOperatorView observedSlots = [] for slot in topLevelOperatorView.inputs.values( ) + topLevelOperatorView.outputs.values(): if slot.level == 0 or slot.level == 1: observedSlots.append(slot) observedSlots += additionalMonitoredSlots self._orphanOperators = [ ] # Operators that are owned by this GUI directly (not owned by the top-level operator) self.observedSlots = [] for slot in observedSlots: if slot.level == 0: if not isinstance(slot.stype, ArrayLike): # We don't support visualization of non-Array slots. continue # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0. # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes. opPromoteInput = OpWrapSlot( parent=slot.getRealOperator().parent) opPromoteInput.Input.connect(slot) slot = opPromoteInput.Output self._orphanOperators.append(opPromoteInput) # Each slot should now be indexed as slot[layer_index] assert slot.level == 1 self.observedSlots.append(slot) slot.notifyInserted(bind(self._handleLayerInsertion)) self.__cleanup_fns.append( partial(slot.unregisterInserted, bind(self._handleLayerInsertion))) slot.notifyRemoved(bind(self._handleLayerRemoval)) self.__cleanup_fns.append( partial(slot.unregisterRemoved, bind(self._handleLayerRemoval))) for i in range(len(slot)): self._handleLayerInsertion(slot, i) self.layerstack = LayerStackModel() self._initCentralUic() self._initEditor(crosshair=crosshair) self.__viewerControlWidget = None if not centralWidgetOnly: self.initViewerControlUi( ) # Might be overridden in a subclass. Default implementation loads a standard layer widget. #self._drawer = QWidget( self ) self.initAppletDrawerUi( ) # Default implementation loads a blank drawer from drawer.ui. def _after_init(self): self._initialized = True self.updateAllLayers() def setupLayers(self): """ Create a list of layers to be displayed in the central widget. Subclasses should override this method to create the list of layers that can be displayed. For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots. """ layers = [] for multiLayerSlot in self.observedSlots: for j, slot in enumerate(multiLayerSlot): if slot.ready() and slot.meta.axistags is not None: layer = self.createStandardLayerFromSlot(slot) # Name the layer after the slot name. if isinstance(multiLayerSlot.getRealOperator(), OpWrapSlot): # We attached an 'upleveling' operator, so look upstream for the real slot. layer.name = multiLayerSlot.getRealOperator( ).Input.partner.name else: layer.name = multiLayerSlot.name + " " + str(j) layers.append(layer) return layers def _handleLayerInsertion(self, slot, slotIndex): """ The multislot providing our layers has a new item. Make room for it in the layer GUI and subscribe to updates. """ # When the slot is ready, we'll replace the blank layer with real data slot[slotIndex].notifyReady(bind(self.updateAllLayers)) slot[slotIndex].notifyUnready(bind(self.updateAllLayers)) self.__cleanup_fns.append( partial(slot[slotIndex].unregisterReady, bind(self.updateAllLayers))) self.__cleanup_fns.append( partial(slot[slotIndex].unregisterUnready, bind(self.updateAllLayers))) def _handleLayerRemoval(self, slot, slotIndex): """ An item is about to be removed from the multislot that is providing our layers. Remove the layer from the GUI. """ self.updateAllLayers(slot) def generateAlphaModulatedLayersFromChannels(self, slot): # TODO assert False @classmethod def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False): """ Convenience function. Generates a volumina layer using the given slot. Chooses between grayscale or RGB depending on the number of channels in the slot. * If *slot* has 1 channel or more than 4 channels, a GrayscaleLayer is created. * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels. * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels. * If *slot* has 4 channels, an RGBA layer is created :param slot: The slot to generate a layer from :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel. If slot has 4 channels, this parameter has no effect. """ def getRange(meta): return meta.drange def getNormalize(meta): if meta.drange is not None and meta.normalizeDisplay is False: # do not normalize if the user provided a range and set normalization to False return False else: # If we don't know the range of the data and normalization is allowed # by the user, create a layer that is auto-normalized. # See volumina.pixelpipeline.datasources for details. # # Even in the case of integer data, which has more than 255 possible values, # (like uint16), it seems reasonable to use this setting as default return None # means autoNormalize shape = slot.meta.shape try: channelAxisIndex = slot.meta.axistags.index('c') #assert channelAxisIndex < len(slot.meta.axistags), \ # "slot %s has shape = %r, axistags = %r, but no channel dimension" \ # % (slot.name, slot.meta.shape, slot.meta.axistags) numChannels = shape[channelAxisIndex] axisinfo = slot.meta.axistags["c"].description except: numChannels = 1 axisinfo = "" # == no info on channels given rindex = None bindex = None gindex = None aindex = None if axisinfo == "" or axisinfo == "default": # Examine channel dimension to determine Grayscale vs. RGB if numChannels == 4: lastChannelIsAlpha = True if lastChannelIsAlpha: assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha). Your image has {} channels.".format( numChannels) if numChannels == 1 or (numChannels > 4): assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel" source = LazyflowSource(slot) layer = GrayscaleLayer(source) layer.numberOfChannels = numChannels normalize = getNormalize(slot.meta) range = getRange(slot.meta) layer.set_range(0, range) layer.set_normalize(0, normalize) return layer assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \ "Unhandled combination of channels. numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags ) rindex = 0 gindex = 1 if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha): bindex = 2 if lastChannelIsAlpha: aindex = numChannels - 1 elif axisinfo == "grayscale": source = LazyflowSource(slot) layer = GrayscaleLayer(source) layer.numberOfChannels = numChannels normalize = getNormalize(slot.meta) range = getRange(slot.meta) layer.set_range(0, range) layer.set_normalize(0, normalize) return layer elif axisinfo == "rgba": rindex = 0 if numChannels >= 2: gindex = 1 if numChannels >= 3: bindex = 2 if numChannels >= 4: aindex = numChannels - 1 else: raise RuntimeError("unknown channel display mode") redSource = None if rindex is not None: redProvider = OpSingleChannelSelector( parent=slot.getRealOperator().parent) redProvider.Input.connect(slot) redProvider.Index.setValue(rindex) redSource = LazyflowSource(redProvider.Output) redSource.additional_owned_ops.append(redProvider) greenSource = None if gindex is not None: greenProvider = OpSingleChannelSelector( parent=slot.getRealOperator().parent) greenProvider.Input.connect(slot) greenProvider.Index.setValue(gindex) greenSource = LazyflowSource(greenProvider.Output) greenSource.additional_owned_ops.append(greenProvider) blueSource = None if bindex is not None: blueProvider = OpSingleChannelSelector( parent=slot.getRealOperator().parent) blueProvider.Input.connect(slot) blueProvider.Index.setValue(bindex) blueSource = LazyflowSource(blueProvider.Output) blueSource.additional_owned_ops.append(blueProvider) alphaSource = None if aindex is not None: alphaProvider = OpSingleChannelSelector( parent=slot.getRealOperator().parent) alphaProvider.Input.connect(slot) alphaProvider.Index.setValue(aindex) alphaSource = LazyflowSource(alphaProvider.Output) alphaSource.additional_owned_ops.append(alphaProvider) layer = RGBALayer(red=redSource, green=greenSource, blue=blueSource, alpha=alphaSource) normalize = getNormalize(slot.meta) range = getRange(slot.meta) for i in xrange(4): if [redSource, greenSource, blueSource, alphaSource][i]: layer.set_range(i, range) layer.set_normalize(i, normalize) return layer @threadRouted def updateAllLayers(self, slot=None): if self._stopped or not self._initialized: return if slot is not None and slot.ready() and slot.meta.axistags is None: # Don't update in response to value slots. return # Ask for the updated layer list (usually provided by the subclass) newGuiLayers = self.setupLayers() for layer in newGuiLayers: assert not filter( lambda l: l is layer, self.layerstack ), \ "You are attempting to re-use a layer ({}). " \ "Your setupOutputs() function may not re-use layer objects. " \ "The layerstack retains ownership of the layers you provide and " \ "may choose to clean and delete them without your knowledge.".format( layer.name ) newNames = set(l.name for l in newGuiLayers) if len(newNames) != len(newGuiLayers): msg = "All layers must have unique names.\n" msg += "You're attempting to use these layer names:\n" msg += str([l.name for l in newGuiLayers]) raise RuntimeError(msg) # If the datashape changed, tell the editor # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case... newDataShape = self.determineDatashape() if newDataShape is not None and self.editor.dataShape != newDataShape: self.editor.dataShape = newDataShape # Find the xyz midpoint midpos5d = [x / 2 for x in newDataShape] midpos3d = midpos5d[1:4] # Start in the center of the volume self.editor.posModel.slicingPos = midpos3d self.editor.navCtrl.panSlicingViews(midpos3d, [0, 1, 2]) # Old layers are deleted if # (1) They are not in the new set or # (2) Their data has changed for index, oldLayer in reversed(list(enumerate(self.layerstack))): if oldLayer.name not in newNames: needDelete = True else: newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0] needDelete = newLayer.isDifferentEnough(oldLayer) if needDelete: layer = self.layerstack[index] if hasattr(layer, 'shortcutRegistration'): obsoleteShortcut = layer.shortcutRegistration[2] obsoleteShortcut.setEnabled(False) ShortcutManager().unregister(obsoleteShortcut) self.layerstack.selectRow(index) self.layerstack.deleteSelected() # Insert all layers that aren't already in the layerstack # (Identified by the name attribute) existingNames = set(l.name for l in self.layerstack) for index, layer in enumerate(newGuiLayers): if layer.name not in existingNames: # Insert new self.layerstack.insert(index, layer) # If this layer has an associated shortcut, register it with the shortcut manager if hasattr(layer, 'shortcutRegistration'): ShortcutManager().register(*layer.shortcutRegistration) else: # Clean up the layer instance that the client just gave us. # We don't want to use it. if hasattr(layer, 'shortcutRegistration'): shortcut = layer.shortcutRegistration[2] shortcut.setEnabled(False) layer.clean_up() # Move existing layer to the correct position stackIndex = self.layerstack.findMatchingIndex( lambda l: l.name == layer.name) self.layerstack.selectRow(stackIndex) while stackIndex > index: self.layerstack.moveSelectedUp() stackIndex -= 1 while stackIndex < index: self.layerstack.moveSelectedDown() stackIndex += 1 def determineDatashape(self): newDataShape = None for provider in self.observedSlots: for i, slot in enumerate(provider): if newDataShape is None: newDataShape = self.getVoluminaShapeForSlot(slot) return newDataShape @classmethod def getVoluminaShapeForSlot(self, slot): shape = None if slot.ready() and slot.meta.axistags is not None: # Use an OpReorderAxes adapter to transpose the shape for us. op5 = OpReorderAxes(parent=slot.getRealOperator().parent) op5.Input.connect(slot) shape = op5.Output.meta.shape # We just needed the operator to determine the transposed shape. # Disconnect it so it can be garbage collected. op5.Input.disconnect() op5.cleanUp() return shape def initViewerControlUi(self): """ Load the viewer controls GUI, which appears below the applet bar. In our case, the viewer control GUI consists mainly of a layer list. Subclasses should override this if they provide their own viewer control widget. """ localDir = os.path.split(__file__)[0] self.__viewerControlWidget = ViewerControls() # The editor's layerstack is in charge of which layer movement buttons are enabled model = self.editor.layerStack if self.__viewerControlWidget is not None: self.__viewerControlWidget.setupConnections(model) def initAppletDrawerUi(self): """ By default, this base class provides a blank applet drawer. Override this in a subclass to get a real applet drawer. """ # Load the ui file (find it in our own directory) localDir = os.path.split(__file__)[0] self._drawer = uic.loadUi(localDir + "/drawer.ui") def getAppletDrawerUi(self): return self._drawer def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] uic.loadUi(localDir + "/centralWidget.ui", self) def _initEditor(self, crosshair): """ Initialize the Volume Editor GUI. """ self.editor = VolumeEditor(self.layerstack, crosshair=crosshair) # Replace the editor's navigation interpreter with one that has extra functionality self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel) self.editor.setNavigationInterpreter(self.clickReporter) self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick) self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick) clickReporter2 = ClickReportingInterpreter( self.editor.brushingInterpreter, self.editor.posModel) clickReporter2.rightClickReceived.connect(self._handleEditorRightClick) self.editor.brushingInterpreter = clickReporter2 self.editor.setInteractionMode('navigation') self.volumeEditorWidget.init(self.editor) self.editor._lastImageViewFocus = 0 # Zoom at a 1-1 scale to avoid loading big datasets entirely... for view in self.editor.imageViews: view.doScaleTo(1) def _convertPositionToDataSpace(self, voluminaPosition): taggedPosition = {k: p for k, p in zip('txyzc', voluminaPosition)} # Find the first lazyflow layer in the stack # We assume that all lazyflow layers have the same axistags dataTags = None for layer in self.layerstack: for datasource in layer.datasources: try: # not all datasources have the dataSlot property, find out by trying dataTags = datasource.dataSlot.meta.axistags if dataTags is not None: break except AttributeError: pass if (dataTags is None): raise RuntimeError( "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer." ) position = () for tag in dataTags: position += (taggedPosition[tag.key], ) return position def _handleEditorRightClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorRightClick(dataPosition, globalWindowCoordinate) def _handleEditorLeftClick(self, position5d, globalWindowCoordinate): dataPosition = self._convertPositionToDataSpace(position5d) self.handleEditorLeftClick(dataPosition, globalWindowCoordinate) def handleEditorRightClick(self, position5d, globalWindowCoordinate): # Override me pass def handleEditorLeftClick(self, position5d, globalWindowCoordiante): # Override me pass