def __init__( self, navigationController, brushingController ): QObject.__init__( self ) self._navCtrl = navigationController self._navIntr = NavigationInterpreter( navigationController ) self._brushingCtrl = brushingController self._current_state = self.FINAL self._temp_erasing = False # indicates, if user pressed shift # for temporary erasing (in # contrast to selecting the eraser brush) self._lineItems = [] # list of line items that have been # added to the qgraphicsscene for drawing indication self._lastEvent = None self._doubleClickTimer = None # clear the temporary line items once they # have been pushed to the sink self._brushingCtrl.wroteToSink.connect(self.clearLines)
def __init__( self, navigationController, layerStack, posModel ): QObject.__init__( self ) self._navCtrl = navigationController self._navIntr = NavigationInterpreter( navigationController ) self._layerStack = layerStack self._active_layer = None self._active_channel_idx = -1 self._current_state = self.FINAL self._current_position = QPoint(0,0) # Setting default values, scaled on actual data later on self._steps_mean = 10 self._steps_delta = self._steps_mean*2 self._steps_scaling = 0.07 self._range_max = 4096.0 # hardcoded, in the case drange is not set in the data file or in the dataSelectionDialogue self._range_min = -4096.0 self._range = np.abs(self._range_max-self._range_min) self._channel_range = dict() self._posModel = posModel
class BrushingInterpreter( QObject ): # states FINAL = 0 DEFAULT_MODE = 1 # FIXME: This state isn't really needed, now that we use a QTimer to manage the double-click case. # (The state machine should be rewritten.) MAYBE_DRAW_MODE = 2 #received a single left-click; however, the next event #might be a double-click event; therefore the state has #not been decided yet DRAW_MODE = 3 @property def state( self ): return self._current_state def __init__( self, navigationController, brushingController ): QObject.__init__( self ) self._navCtrl = navigationController self._navIntr = NavigationInterpreter( navigationController ) self._brushingCtrl = brushingController self._current_state = self.FINAL self._temp_erasing = False # indicates, if user pressed shift # for temporary erasing (in # contrast to selecting the eraser brush) self._lineItems = [] # list of line items that have been # added to the qgraphicsscene for drawing indication self._lastEvent = None self._doubleClickTimer = None # clear the temporary line items once they # have been pushed to the sink self._brushingCtrl.wroteToSink.connect(self.clearLines) def start( self ): if self._current_state == self.FINAL: self._navIntr.start() self._current_state = self.DEFAULT_MODE else: pass # ignore def stop( self ): if self._brushingCtrl._isDrawing: for imageview in self._navCtrl._views: self._brushingCtrl.endDrawing(imageview.mousePos) self._current_state = self.FINAL self._navIntr.stop() def eventFilter( self, watched, event ): etype = event.type() # Before we steal this event from the scene, check that it is allowing brush strokes allow_brushing = True for view in self._navCtrl._views: allow_brushing &= view.scene().allow_brushing if not allow_brushing: return self._navIntr.eventFilter( watched, event ) if etype == QEvent.MouseButtonDblClick and self._doubleClickTimer is not None: # On doubleclick, cancel release handler that normally draws the stroke. self._doubleClickTimer.stop() self._doubleClickTimer = None self._current_state = self.DEFAULT_MODE self.onEntry_default( watched, event ) if self._current_state == self.DEFAULT_MODE: if etype == QEvent.MouseButtonPress \ and event.button() == Qt.LeftButton \ and event.modifiers() == Qt.NoModifier \ and self._navIntr.mousePositionValid(watched, event): ### default mode -> maybe draw mode self._current_state = self.MAYBE_DRAW_MODE # event will not be valid to use after this function exits, # so we must make a copy of it instead of just saving the pointer self._lastEvent = QMouseEvent( event.type(), event.pos(), event.globalPos(), event.button(), event.buttons(), event.modifiers() ) elif self._current_state == self.MAYBE_DRAW_MODE: if etype == QEvent.MouseMove: # navigation interpreter also has to be in # default mode to avoid inconsistencies if self._navIntr.state == self._navIntr.DEFAULT_MODE: ### maybe draw mode -> maybe draw mode self._current_state = self.DRAW_MODE self.onEntry_draw( watched, self._lastEvent ) self.onMouseMove_draw( watched, event ) return True else: self._navIntr.eventFilter( watched, self._lastEvent ) return self._navIntr.eventFilter( watched, event ) elif etype == QEvent.MouseButtonDblClick: ### maybe draw mode -> default mode self._current_state = self.DEFAULT_MODE return self._navIntr.eventFilter( watched, event ) elif etype == QEvent.MouseButtonRelease: def handleRelease(releaseEvent): self._current_state = self.DRAW_MODE self.onEntry_draw( watched, self._lastEvent ) self.onExit_draw( watched, releaseEvent) self._current_state = self.DEFAULT_MODE self.onEntry_default( watched, releaseEvent ) # If this event is part of a double-click, we don't really want to handle it. # Typical event sequence is press, release, double-click (not two presses). # Instead of handling this right away, set a timer to do the work. # We'll cancel the timer if we see a double-click event (see above). self._doubleClickTimer = QTimer(self) self._doubleClickTimer.setInterval(200) self._doubleClickTimer.setSingleShot(True) # event will not be valid to use after this function exits, # so we must make a copy of it instead of just saving the pointer eventCopy = QMouseEvent( event.type(), event.pos(), event.button(), event.buttons(), event.modifiers() ) self._doubleClickTimer.timeout.connect( partial(handleRelease, eventCopy ) ) self._doubleClickTimer.start() return True elif self._current_state == self.DRAW_MODE: if etype == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton: self.onExit_draw( watched, event ) ### draw mode -> default mode self._current_state = self.DEFAULT_MODE self.onEntry_default( watched, event ) return True elif etype == QEvent.MouseMove and event.buttons() & Qt.LeftButton: if self._navIntr.mousePositionValid(watched, event): self.onMouseMove_draw( watched, event ) return True else: self.onExit_draw( watched, event ) ### draw mode -> default mode self._current_state = self.DEFAULT_MODE self.onEntry_default( watched, event ) # let the navigation interpreter handle common events return self._navIntr.eventFilter( watched, event ) ### ### Default Mode ### def onEntry_default( self, imageview, event ): pass ### ### Draw Mode ### def onEntry_draw( self, imageview, event ): if QApplication.keyboardModifiers() == Qt.ShiftModifier: self._brushingCtrl._brushingModel.setErasing() self._temp_erasing = True imageview.mousePos = imageview.mapScene2Data(imageview.mapToScene(event.pos())) self._brushingCtrl.beginDrawing(imageview, imageview.mousePos) def onExit_draw( self, imageview, event ): self._brushingCtrl.endDrawing(imageview.mousePos) if self._temp_erasing: self._brushingCtrl._brushingModel.disableErasing() self._temp_erasing = False def onMouseMove_draw( self, imageview, event ): self._navIntr.onMouseMove_default( imageview, event ) o = imageview.scene().data2scene.map(QPointF(imageview.oldX,imageview.oldY)) n = imageview.scene().data2scene.map(QPointF(imageview.x,imageview.y)) # Draw temporary line for the brush stroke so the user gets feedback before the data is really updated. pen = QPen( QBrush(self._brushingCtrl._brushingModel.drawColor), self._brushingCtrl._brushingModel.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) line = QGraphicsLineItem(o.x(), o.y(), n.x(), n.y()) line.setPen(pen) imageview.scene().addItem(line) line.setParentItem(imageview.scene().dataRectItem) self._lineItems.append(line) self._brushingCtrl._brushingModel.moveTo(imageview.mousePos) def clearLines(self): # This is called after the brush stroke is stored to the data. # Our temporary line object is no longer needed because the data provides the true pixel labels that were stored. lines = self._lineItems self._lineItems = [] for l in lines: l.hide() def updateCursorPosition(self, *args, **kwargs): self._navIntr.updateCursorPosition(*args, **kwargs)
class ThresholdingInterpreter( QObject ): # states FINAL = 0 DEFAULT_MODE = 1 # normal navigation functionality THRESHOLDING_MODE = 2 # while pressing left mouse button allow thresholding NO_VALID_LAYER = 3 # not a grayscale layer @property def state( self ): return self._current_state def __init__( self, navigationController, layerStack, posModel ): QObject.__init__( self ) self._navCtrl = navigationController self._navIntr = NavigationInterpreter( navigationController ) self._layerStack = layerStack self._active_layer = None self._active_channel_idx = -1 self._current_state = self.FINAL self._current_position = QPoint(0,0) # Setting default values, scaled on actual data later on self._steps_mean = 10 self._steps_delta = self._steps_mean*2 self._steps_scaling = 0.07 self._range_max = 4096.0 # hardcoded, in the case drange is not set in the data file or in the dataSelectionDialogue self._range_min = -4096.0 self._range = np.abs(self._range_max-self._range_min) self._channel_range = dict() self._posModel = posModel def start( self ): if self._current_state == self.FINAL: self._navIntr.start() self._current_state = self.DEFAULT_MODE self._init_layer() else: pass def stop( self ): self._current_state = self.FINAL if self.valid_layer(): self._active_layer.channelChanged.disconnect(self.channel_changed) self._navIntr.stop() def eventFilter( self, watched, event ): etype = event.type() if self._current_state == self.DEFAULT_MODE: if etype == QEvent.MouseButtonPress \ and event.button() == Qt.LeftButton \ and event.modifiers() == Qt.NoModifier \ and self._navIntr.mousePositionValid(watched, event): # TODO maybe remove, if we can find out which view is active self.set_active_layer() if self.valid_layer(): self._current_state = self.THRESHOLDING_MODE self._current_position = watched.mapToGlobal( event.pos() ) return True else: self._current_state = self.NO_VALID_LAYER return self._navIntr.eventFilter( watched, event ) elif etype == QEvent.MouseButtonPress \ and event.button() == Qt.RightButton \ and event.modifiers() == Qt.NoModifier \ and self._navIntr.mousePositionValid(watched, event): self.set_active_layer() if self.valid_layer(): self.onRightClick_resetThreshold(watched, event) else: pass # do nothing return True else: return self._navIntr.eventFilter( watched, event ) elif self._current_state == self.NO_VALID_LAYER: self.set_active_layer() if self.valid_layer(): self._current_state = self.DEFAULT_MODE return self._navIntr.eventFilter( watched, event ) elif self._current_state == self.THRESHOLDING_MODE: if self._active_layer == None: # No active layer set, should not go here return self._navIntr.eventFilter( watched, event ) if etype == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton: self._current_state = self.DEFAULT_MODE self._active_layer = None self.onExit_threshold( watched, event ) return True elif etype == QEvent.MouseMove and event.buttons() == Qt.LeftButton: self.onMouseMove_thresholding(watched, event) return True else: return self._navIntr.eventFilter( watched, event ) else: # let the navigation interpreter handle common events return self._navIntr.eventFilter( watched, event ) def onRightClick_resetThreshold(self, imageview, event): range = self.get_min_max_of_current_view(imageview) self._active_layer.set_normalize(0, (range[0],range[1])) self._channel_range[self._active_channel_idx] = (range[0],range[1]) def set_active_layer(self): """ determines the layer postion in the stack and the currently displayed channel. Needs to be called constantly, because the user can change the position of the input layer within the stack """ for idx, layer in enumerate(self._layerStack): if isinstance(layer, GrayscaleLayer): if layer.window_leveling: self._active_layer = layer self._active_channel_idx= layer._channel return self._active_layer = None def _init_layer(self): self.set_active_layer() if self.valid_layer(): self._active_layer.channelChanged.connect(self.channel_changed) if self.get_drange() != None: self._range_min, self._range_max = self.get_drange() def onExit_threshold( self, watched, event ): pass def get_drange(self): """ returns tuple of drange (min, max) as set in hdf5 file or None if nothing is specified """ return self._active_layer._datasources[0]._rawSource._op5.Output.meta.drange def valid_layer(self): if isinstance(self._active_layer, GrayscaleLayer): return self._active_layer.window_leveling else: return False def channel_changed(self): self.set_active_layer() if self._active_channel_idx in self._channel_range: self._active_layer.set_normalize(0, self._channel_range[self._active_channel_idx]) else: self._active_layer.set_normalize(0,(self._range_min, self._range_max)) def get_min_max_of_current_view(self, imageview): """ Function returns min and max value of the current view based on the raw data. Ugly hack, but all we got for now """ shape2D = posView2D( list(self._posModel.shape5D[1:4]), axis=self._posModel.activeView ) data_x, data_y = 0, 0 data_x2, data_y2 = shape2D[0], shape2D[1] if self._posModel.activeView == 0: x_pos = self._posModel.slicingPos5D[1] slicing = [slice(0, 1), slice(x_pos, x_pos+1), slice(data_x, data_x2), slice(data_y, data_y2), slice(self._active_channel_idx, self._active_channel_idx+1)] if self._posModel.activeView == 1: y_pos = self._posModel.slicingPos5D[2] slicing = [slice(0, 1), slice(data_x, data_x2), slice(y_pos, y_pos+1), slice(data_y, data_y2), slice(self._active_channel_idx, self._active_channel_idx+1)] if self._posModel.activeView == 2: z_pos = self._posModel.slicingPos5D[3] slicing = [slice(0, 1), slice(data_x, data_x2), slice(data_y, data_y2), slice(z_pos, z_pos+1), slice(self._active_channel_idx, self._active_channel_idx+1)] request = self._active_layer._datasources[0].request(slicing) result = request.wait() return result.min(), result.max() def onMouseMove_thresholding(self, imageview, event): if self._active_channel_idx not in self._channel_range: range = self.get_min_max_of_current_view(imageview) range_lower = range[0] range_upper = range[1] else: range = self._channel_range[self._active_channel_idx] range_lower = range[0] range_upper = range[1] # don't know what version is more efficient # range_delta = np.sqrt((range_upper - range_lower)**2) range_delta = np.abs(range_upper - range_lower) range_mean = range_lower + range_delta/2.0 self._steps_mean = range_delta * self._steps_scaling self._steps_delta = self._steps_mean * 2 pos = imageview.mapToGlobal( event.pos() ) dx = pos.x() - self._current_position.x() dy = self._current_position.y() - pos.y() if dx > 0.0: # move mean to right range_mean += self._steps_mean elif dx < 0.0: # move mean to left range_mean -= self._steps_mean if dy > 0.0: # increase delta range_delta += self._steps_delta elif dy < 0.0: # decrease delta range_delta -= self._steps_delta # check the bounds, ugly use min max values actually present if range_mean < self._range_min: range_mean = self._range_min elif range_mean > self._range_max: range_mean = self._range_max if range_delta < 1: range_delta = 1 elif range_delta > self._range: range_delta = self._range a = range_mean - range_delta/2.0 b = range_mean + range_delta/2.0 if a < self._range_min: a = self._range_min elif a > self._range_max: a = self._range_max if b < self._range_min: b = self._range_min elif b > self._range_max: b = self._range_max assert a <= b # TODO test if in allowed range (i.e. max and min of data) self._active_layer.set_normalize(0, (a,b)) self._channel_range[self._active_channel_idx] = (a,b) self._current_position = pos
def __init__(self, layerStackModel, parent, labelsink=None, crosshair=True, syncAlongAxes=(0, 1)): super(VolumeEditor, self).__init__(parent=parent) self._sync_along = tuple(syncAlongAxes) ## ## properties ## self._showDebugPatches = False self._showTileProgress = True ## ## base components ## self.layerStack = layerStackModel self.posModel = PositionModel(self) self.brushingModel = BrushingModel() self.cropModel = CropExtentsModel(self) self.imageScenes = [ ImageScene2D(self.posModel, (0, 1, 4), swapped_default=True), ImageScene2D(self.posModel, (0, 2, 4)), ImageScene2D(self.posModel, (0, 3, 4)) ] self.imageViews = [ ImageView2D(parent, self.cropModel, self.imageScenes[i]) for i in [0, 1, 2] ] self.imageViews[0].focusChanged.connect( lambda arg=0: self.lastImageViewFocus(arg)) self.imageViews[1].focusChanged.connect( lambda arg=1: self.lastImageViewFocus(arg)) self.imageViews[2].focusChanged.connect( lambda arg=2: self.lastImageViewFocus(arg)) self._lastImageViewFocus = 0 if not crosshair: for view in self.imageViews: view._crossHairCursor.enabled = False self.imagepumps = self._initImagePumps() self.view3d = self._initView3d() if useVTK else QWidget() names = ['x', 'y', 'z'] for scene, name, pump in zip(self.imageScenes, names, self.imagepumps): scene.setObjectName(name) scene.stackedImageSources = pump.stackedImageSources self.cacheSize = 50 ## ## interaction ## # event switch self.eventSwitch = EventSwitch(self.imageViews) # navigation control v3d = self.view3d if useVTK else None self.navCtrl = NavigationController(self.imageViews, self.imagepumps, self.posModel, view3d=v3d) self.navInterpret = NavigationInterpreter(self.navCtrl) # brushing control if crosshair: self.crosshairController = CrosshairController( self.brushingModel, self.imageViews) self.brushingController = BrushingController(self.brushingModel, self.posModel, labelsink) self.brushingInterpreter = BrushingInterpreter(self.navCtrl, self.brushingController) for v in self.imageViews: self.brushingController._brushingModel.brushSizeChanged.connect( v._sliceIntersectionMarker._set_diameter) # thresholding control self.thresInterpreter = ThresholdingInterpreter( self.navCtrl, self.layerStack, self.posModel) # initial interaction mode self.eventSwitch.interpreter = self.navInterpret # By default, don't show cropping controls self.showCropLines(False) ## ## connect ## self.posModel.timeChanged.connect(self.navCtrl.changeTime) self.posModel.slicingPositionChanged.connect( self.navCtrl.moveSlicingPosition) if crosshair: self.posModel.cursorPositionChanged.connect( self.navCtrl.moveCrosshair) self.posModel.slicingPositionSettled.connect( self.navCtrl.settleSlicingPosition) self.layerStack.layerAdded.connect(self._onLayerAdded) self.parent = parent