class GMPage: @property def document(self): return self._doc @property def index(self): return self._index @property def page_image(self): return self._page_image @property def drawing_image(self): return self._drawing_image @property def command_count(self): return self._undo_stack.count() @property def can_undo(self): return self._undo_stack.canUndo() @property def can_redo(self): return self._undo_stack.canRedo() def __init__(self, document, mu_page, index): assert isinstance(document, GMDoc) assert isinstance(mu_page, fitz.Page) assert type(index) is int self._doc = document self._index = index self._mu_page = mu_page # zoom in to get higher resolution pixmap # todo: add this param to preferences dialog zoom = 2 matrix = fitz.Matrix(zoom, zoom) page_data = self._mu_page.getPixmap(matrix=matrix, alpha=False).getImageData() self._undo_stack = QUndoStack() self._page_image = QPixmap() self._page_image.loadFromData(page_data) self._drawing_image = generateDrawingPixmap(self._page_image) self._last_drawing_hash = None self.captureState() def getData(self, alpha=False): return self._mu_page.getPixmap(alpha=alpha).getImageData() def getSize(self): return self._page_image.size() def setDrawingPixmap(self, pixmap): self._drawing_image = pixmap def getMergedPixmap(self): result = QPixmap(self.getSize()) result.fill(QColor(255, 255, 255)) painter = QPainter(result) painter.setRenderHint(QPainter.HighQualityAntialiasing, True) painter.drawPixmap(QPoint(0, 0), self._page_image) painter.drawPixmap(QPoint(0, 0), self._drawing_image) painter.end() return result def getUndoAction(self, parent): return self._undo_stack.createUndoAction(parent) def getRedoAction(self, parent): return self._undo_stack.createRedoAction(parent) def pushCommand(self, command): assert isinstance(command, QUndoCommand) self._undo_stack.push(command) def captureState(self): self._last_drawing_hash = self._drawing_image.cacheKey() self._undo_stack.setClean() def changed(self): if self._undo_stack.isClean(): return False return self._undo_stack.count() > 0 def undo(self): self._undo_stack.undo() def redo(self): self._undo_stack.redo()
class mylabel(QLabel): print("into mylabel") def __init__(self, parent): super(mylabel, self).__init__(parent) self.image = QImage() self.drawing = True self.lastPoint = QPoint() self.modified = False self.scribbling = False self.eraserSize = 5 #橡皮擦初始值 self.fontSize = 12 #字形初始值 self.setAcceptDrops(True) self.savetextedit = [] self.text = [] #紀錄文字 self.eraserPos = [] self.temp_img = 1 #紀錄圖片編號 self.numstack = [] self.i = 0 # 紀錄textedit self.eraserClicked = False self.textClicked = False self.clearClicked = False self.undoStack = QUndoStack() self.m_undoaction = self.undoStack.createUndoAction(self, self.tr("&Undo")) # self.m_undoaction.setShortcut('Ctrl+Z') self.addAction(self.m_undoaction) self.m_redoaction = self.undoStack.createRedoAction(self, self.tr("&Redo")) # self.m_redoaction.setShortcut('Ctrl+Y') self.addAction(self.m_redoaction) #開啟圖片 def openimage(self, filename): self.image = filename self.scaredPixmap = self.image.scaled(self.width(), self.height(), aspectRatioMode=Qt.KeepAspectRatio) self.image = self.scaredPixmap self.setAlignment(Qt.AlignLeft) self.update() #獲得橡皮擦大小 def getErasersize(self, esize): self.eraserSize = int(esize) print(self.eraserSize) #獲得字體大小 def getFontsize(self, fsize): self.fontSize = int(fsize) print(self.fontSize) def mousePressEvent(self, event): print("left press") super().mousePressEvent(event) if event.button() == Qt.LeftButton and self.eraserClicked: print("start erase action") self.drawing = True self.eraserPos.clear() self.lastPoint = event.pos() self.eraserPos.append(self.lastPoint) elif event.button() == Qt.LeftButton and self.textClicked: print("add textedit") self.item = self.childAt(event.x(), event.y()) if not self.childAt(event.x(), event.y()): # 如果不是點在文字框上的話 # 文字框 self.textedit = textedit1(self) self.textedit.setObjectName("textedit{}".format(self.i)) self.textedit.setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid black;\n"); # 設定邊框 self.textedit.move(event.pos().x(), event.pos().y()) self.savetextedit.append(self.textedit) print("record textedit", self.savetextedit) # 設定文字框格式 self.textedit.setFont(QFont("Roman times", self.fontSize)) self.textedit.setLineWrapMode(QTextEdit.FixedColumnWidth) # 自動斷行 self.textedit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # 隱藏滾動軸 self.textedit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # 隱藏滾動軸 self.textedit.show() print(self.textedit) self.item = self.childAt(event.x(), event.y()) print("choose item", self.item) command = storeCommand(self.textedit) self.undoStack.push(command) print("undostack count", self.undoStack.count()) print("into undostack", self.textedit) if self.textedit: # 將文字框設為不能寫 x = 0 for self.savetextedit[x] in self.savetextedit: self.savetextedit[x].setReadOnly(True) self.savetextedit[x].setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid black;\n"); # 設定邊框 x = x + 1 else: self.item.setReadOnly(False) self.doc = self.item.document() self.doc.contentsChanged.connect(self.textAreaChanged) # 隨著輸入的文字改變文字框的大小 elif event.button() == Qt.RightButton: self.item = self.childAt(event.x(), event.y()) print("choose textedit", self.item) # 有存到stack裡,可是沒辦法redo,沒辦法傳進去移動過後的位置 try: if self.item: self.item.setReadOnly(True) print("moveCmd", self.item, self.item.pos()) # self.undoStack.push(moveCommand(self.item, self.item.pos())) self.undoStack.push(moveCommand(self.item, self.item.pos())) print("moveundostack count", self.undoStack.count()) except Exception as e: print(e) def keyPressEvent(self, event): if event.key() == Qt.Key_Delete: print("delete textedit", self.item) self.item.deleteLater() self.savetextedit.remove(self.item) # 隨著輸入的文字改變文字框大小 def textAreaChanged(self): self.doc.adjustSize() newWidth = self.doc.size().width() + 20 newHeight = self.doc.size().height() + 20 if newWidth != self.item.width(): self.item.setFixedWidth(newWidth) if newHeight != self.item.height(): self.item.setFixedHeight(newHeight) def dragEnterEvent(self, e): print("drag") e.accept() def dropEvent(self, e): print("drop item") mime = e.mimeData().text() x, y = map(int, mime.split(',')) print(x,y) position = e.pos() print(position) e.source().setReadOnly(True) self.undoStack.push(moveCommand(e.source(), e.source().pos())) e.source().move(position - QPoint(x,y)) print("move item to", self.item.pos()) e.setDropAction(Qt.MoveAction) e.accept() def mouseMoveEvent(self, event): if (event.buttons() and Qt.LeftButton) and self.drawing and self.eraserClicked: print("start painting") self.path = QPainterPath() self.path.moveTo(self.lastPoint) self.path.lineTo(event.pos()) painter = QPainter(self.image) x = self.width() - self.image.width() y = self.height() - self.image.height() painter.translate(x / 2 - x, y / 2 - y) #因為將圖片置中,所以要重設置畫筆原點 painter.setPen(QPen(Qt.white, self.eraserSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.drawPath(self.path) print(self.path) self.lastPoint = event.pos() self.eraserPos.append(self.lastPoint) self.update() def mouseReleaseEvent(self, event): print("release mouse") if event.button() == Qt.LeftButton: self.drawing = False #儲存圖片佔存,供undo redo使用 if self.eraserClicked == True: self.imagename = "img{}.PNG".format(self.temp_img) print(self.imagename) self.image.save("./saveload/{}".format(self.imagename)) self.temp_img = self.temp_img + 1 self.numstack.append(self.imagename) def paintEvent(self, event): canvasPainter = QPainter(self) self.x = (self.width() - self.image.width())/2 self.y = (self.height() - self.image.height())/2 canvasPainter.translate(self.x, self.y) canvasPainter.drawImage(event.rect(), self.image, event.rect()) if not self.textClicked: x = 0 self.undoStack.clear() for self.savetextedit[x] in self.savetextedit: self.savetextedit[x].setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid transparent;\n"); # 設定邊框 if self.savetextedit[x].toPlainText() == "": self.savetextedit[x].deleteLater() self.savetextedit.remove(self.savetextedit[x]) x = x + 1 self.update()
class VolumeEditor(QObject): newImageView2DFocus = pyqtSignal() shapeChanged = pyqtSignal() @property def showDebugPatches(self): return self._showDebugPatches @showDebugPatches.setter def showDebugPatches(self, show): for s in self.imageScenes: s.showTileOutlines = show self._showDebugPatches = show @property def showTileProgress(self): return self._showTileProgress @showDebugPatches.setter def showTileProgress(self, show): for s in self.imageScenes: s.showTileProgress = show self._showTileProgress = show @property def cacheSize(self): return self._cacheSize @cacheSize.setter def cacheSize(self, cache_size): self._cacheSize = cache_size for s in self.imageScenes: s.setCacheSize(cache_size) @property def navigationInterpreterType(self): return type(self.navInterpret) @navigationInterpreterType.setter def navigationInterpreterType(self, navInt): self.navInterpret = navInt(self.navCtrl) self.eventSwitch.interpreter = self.navInterpret def setNavigationInterpreter(self, navInterpret): self.navInterpret = navInterpret self.eventSwitch.interpreter = self.navInterpret @property def syncAlongAxes(self): """Axes orthogonal to slices, whose values are synced between layers. Returns: a tuple of up to three values, encoding: 0 - time 1 - space 2 - channel for example the meaning of (0,1) is: time and orthogonal space axes are synced for all layers, channel is not. (For the x-y slice, the space axis would be z and so on.) """ return tuple(self._sync_along) @property def dataShape(self): return self.posModel.shape5D @dataShape.setter def dataShape(self, s): self.cropModel.set_volume_shape_3d_cropped([0, 0, 0], s[1:4]) self.cropModel.set_time_shape_cropped(0, s[0]) self.posModel.shape5D = s # for 2D images, disable the slice intersection marker is_2D = (numpy.asarray(s[1:4]) == 1).any() if is_2D: self.navCtrl.indicateSliceIntersection = False else: for i in range(3): self.parent.volumeEditorWidget.quadview.ensureMinimized(i) self.shapeChanged.emit() for i, v in enumerate(self.imageViews): v.sliceShape = self.posModel.sliceShape(axis=i) self.view3d.set_shape(s[1:4]) def lastImageViewFocus(self, axis): self._lastImageViewFocus = axis self.newImageView2DFocus.emit() def __init__( self, layerStackModel, parent, labelsink=None, crosshair=True, is_3d_widget_visible=False, syncAlongAxes=(0, 1) ): super(VolumeEditor, self).__init__(parent=parent) self._sync_along = tuple(syncAlongAxes) ## ## properties ## self._showDebugPatches = False self._showTileProgress = True ## ## base components ## self._undoStack = QUndoStack() 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(is_3d_widget_visible) 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 ## # navigation control self.navCtrl = NavigationController(self.imageViews, self.imagepumps, self.posModel, view3d=self.view3d) self.navInterpret = NavigationInterpreter(self.navCtrl) # event switch self.eventSwitch = EventSwitch(self.imageViews, self.navInterpret) # brushing control if crosshair: self.crosshairController = CrosshairController(self.brushingModel, self.imageViews) self.brushingController = BrushingController( self.brushingModel, self.posModel, labelsink, undoStack=self._undoStack ) 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) # 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 self._setUpShortcuts() def _reset(self): for s in self.imageScenes: s.reset() def scheduleSlicesRedraw(self): for s in self.imageScenes: s._invalidateRect() def setInteractionMode(self, name): modes = { "navigation": self.navInterpret, "brushing": self.brushingInterpreter, "thresholding": self.thresInterpreter, } self.eventSwitch.interpreter = modes[name] def showCropLines(self, visible): for view in self.imageViews: view.showCropLines(visible) def setTileWidth(self, tileWidth): for i in self.imageScenes: i.setTileWidth(tileWidth) def cleanUp(self): QApplication.processEvents() for scene in self._imageViews: scene.close() scene.deleteLater() self._imageViews = [] QApplication.processEvents() def closeEvent(self, event): event.accept() def setLabelSink(self, labelsink): self.brushingController.setDataSink(labelsink) ## ## private ## def _setUpShortcuts(self): mgr = ShortcutManager() ActionInfo = ShortcutManager.ActionInfo def _undo(): idx = self._undoStack.index() self._undoStack.setIndex(idx - 1) def _redo(): cap = self._undoStack.count() idx = self._undoStack.index() self._undoStack.setIndex(min(cap, idx + 1)) mgr.register("Ctrl+Z", ActionInfo("Labeling", "Undo", "Undo last action", _undo, self.parent, None)) mgr.register("Ctrl+Y", ActionInfo("Labeling", "Redo", "Redo last action", _redo, self.parent, None)) def _initImagePumps(self): alongTXC = SliceProjection(abscissa=2, ordinate=3, along=[0, 1, 4]) alongTYC = SliceProjection(abscissa=1, ordinate=3, along=[0, 2, 4]) alongTZC = SliceProjection(abscissa=1, ordinate=2, along=[0, 3, 4]) imagepumps = [] imagepumps.append(volumina.pixelpipeline.imagepump.ImagePump(self.layerStack, alongTXC, self._sync_along)) imagepumps.append(volumina.pixelpipeline.imagepump.ImagePump(self.layerStack, alongTYC, self._sync_along)) imagepumps.append(volumina.pixelpipeline.imagepump.ImagePump(self.layerStack, alongTZC, self._sync_along)) return imagepumps def _initView3d(self, is_3d_widget_visible): from .view3d.overview3d import Overview3D view3d = Overview3D(is_3d_widget_visible=is_3d_widget_visible) def onSliceDragged(): self.posModel.slicingPos = view3d.get_slice() view3d.slice_changed.connect(onSliceDragged) return view3d def _onLayerAdded(self, layer, row): self.navCtrl.layerChangeChannel(layer) layer.channelChanged.connect(partial(self.navCtrl.layerChangeChannel, layer=layer))