def clear(self): QtImageViewer.clear(self) self.selected_blobs = [] self.undo_data = Undo() for blob in self.annotations.seg_blobs: self.undrawBlob(blob) del blob self.annotations = Annotation()
def __init__(self, taglab_dir): QtImageViewer.__init__(self) self.logfile = None #MUST be inited in Taglab.py self.project = Project() self.image = None self.channel = None self.annotations = Annotation() self.selected_blobs = [] self.taglab_dir = taglab_dir self.tools = Tools(self) self.tools.createTools() self.undo_data = Undo() self.dragSelectionStart = None self.dragSelectionRect = None self.dragSelectionStyle = QPen(Qt.white, 1, Qt.DashLine) self.dragSelectionStyle.setCosmetic(True) # Set scrollbar self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # DRAWING SETTINGS self.border_pen = QPen(Qt.black, 3) # pen.setJoinStyle(Qt.MiterJoin) # pen.setCapStyle(Qt.RoundCap) self.border_pen.setCosmetic(True) self.border_selected_pen = QPen(Qt.white, 3) self.border_selected_pen.setCosmetic(True) self.showCrossair = False self.mouseCoords = QPointF(0, 0) self.crackWidget = None self.setContextMenuPolicy(Qt.CustomContextMenu) self.refine_grow = 0.0 #maybe should in in tools self.refine_original_mask = None self.refine_original_blob = None self.active_label = None
def __init__(self, rect=[0.0, 0.0, 0.0, 0.0], map_px_to_mm_factor=1.0, width=None, height=None, channels=[], id=None, name=None, acquisition_date="", georef_filename="", workspace=[], metadata={}, annotations={}, working_area=[]): #we have to select a standanrd enforced! #in image standard (x, y, width height) #in numpy standard (y, x, height, width) #no the mixed format we use now I REFUSE to use it. #in range np format: (top, left, bottom, right) #in GIS standard (bottom, left, top, right) self.rect = rect #coordinates of the image. (in the spatial reference system) self.map_px_to_mm_factor = map_px_to_mm_factor #if we have a references system we should be able to recover this numner # otherwise we need to specify it. self.width = width self.height = height #in pixels! self.annotations = Annotation() for data in annotations: blob = Blob(None, 0, 0, 0) blob.fromDict(data) self.annotations.addBlob(blob) self.channels = list(map(lambda c: Channel(**c), channels)) self.id = id # internal id used in correspondences it will never changes self.name = name # a label for an annotated image self.workspace = workspace # a polygon in spatial reference system self.working_area = working_area # this is the working area of data exports for training self.acquisition_date = acquisition_date # acquisition date is mandatory (format YYYY-MM-DD) self.georef_filename = georef_filename # image file (GeoTiff) contained the georeferencing information self.metadata = metadata # this follows image_metadata_template, do we want to allow freedom to add custome values?
class Image(object): def __init__(self, rect=[0.0, 0.0, 0.0, 0.0], map_px_to_mm_factor=1.0, width=None, height=None, channels=[], id=None, name=None, acquisition_date="", georef_filename="", workspace=[], metadata={}, annotations={}, working_area=[]): #we have to select a standanrd enforced! #in image standard (x, y, width height) #in numpy standard (y, x, height, width) #no the mixed format we use now I REFUSE to use it. #in range np format: (top, left, bottom, right) #in GIS standard (bottom, left, top, right) self.rect = rect #coordinates of the image. (in the spatial reference system) self.map_px_to_mm_factor = map_px_to_mm_factor #if we have a references system we should be able to recover this numner # otherwise we need to specify it. self.width = width self.height = height #in pixels! self.annotations = Annotation() for data in annotations: blob = Blob(None, 0, 0, 0) blob.fromDict(data) self.annotations.addBlob(blob) self.channels = list(map(lambda c: Channel(**c), channels)) self.id = id # internal id used in correspondences it will never changes self.name = name # a label for an annotated image self.workspace = workspace # a polygon in spatial reference system self.working_area = working_area # this is the working area of data exports for training self.acquisition_date = acquisition_date # acquisition date is mandatory (format YYYY-MM-DD) self.georef_filename = georef_filename # image file (GeoTiff) contained the georeferencing information self.metadata = metadata # this follows image_metadata_template, do we want to allow freedom to add custome values? def pixelSize(self): if self.map_px_to_mm_factor == "": return 1.0 else: return float(self.map_px_to_mm_factor) def loadGeoInfo(self, filename): """ Update the georeferencing information. """ img = rio.open(filename) if img.crs is not None: # this image contains georeference information self.georef_filename = filename def addChannel(self, filename, type): """ This image add a channel to this image. The functions update the size (in pixels) and the georeferencing information (if the image if georeferenced). The image data is loaded when the image channel is used for the first time. """ img = rio.open(filename) # check image size consistency (all the channels must have the same size) if self.width is not None and self.height is not None: if self.width != img.width or self.height != img.height: raise Exception( "Size of the images is not consistent! It is " + str(img.width) + "x" + str(img.height) + ", should have been: " + str(self.width) + "x" + str(self.height)) return # check image size limits if img.width > 32767 or img.height > 32767: raise Exception( "This map exceeds the image dimension handled by TagLab (the maximum size is 32767 x 32767)." ) return if img.crs is not None: # this image contains georeference information self.georef_filename = filename self.width = img.width self.height = img.height self.channels.append(Channel(filename, type)) def hasDEM(self): """ It returns True if the image has a DEM channel, False otherwise. """ for channel in self.channels: if channel.type == "DEM": return True return False def getChannel(self, type): for channel in self.channels: if channel.type == type: return channel return None def getChannelIndex(self, channel): try: index = self.channels.index(channel) return index except: return -1 def getRGBChannel(self): """ It returns the RGB channel (if exists). """ return self.getChannel("RGB") def getDEMChannel(self): """ It returns the DEM channel (if exists). """ return self.getChannel("DEM") def save(self): data = self.__dict__.copy() return data
class QtImageViewerPlus(QtImageViewer): """ PyQt image viewer widget with annotation capabilities. QGraphicsView handles a scene composed by an image plus shapes (rectangles, polygons, blobs). The input image (it must be a QImage) is internally converted into a QPixmap. """ # Mouse button signals emit image scene (x, y) coordinates. leftMouseButtonPressed = pyqtSignal(float, float) rightMouseButtonPressed = pyqtSignal(float, float) leftMouseButtonReleased = pyqtSignal(float, float) rightMouseButtonReleased = pyqtSignal(float, float) #leftMouseButtonDoubleClicked = pyqtSignal(float, float) rightMouseButtonDoubleClicked = pyqtSignal(float, float) mouseMoveLeftPressed = pyqtSignal(float, float) # custom signal updateInfoPanel = pyqtSignal(Blob) activated = pyqtSignal() newSelection = pyqtSignal() def __init__(self): QtImageViewer.__init__(self) self.logfile = None #MUST be inited in Taglab.py self.project = Project() self.image = None self.channel = None self.annotations = Annotation() self.selected_blobs = [] self.tools = Tools(self) self.tools.createTools() self.undo_data = Undo() self.dragSelectionStart = None self.dragSelectionRect = None self.dragSelectionStyle = QPen(Qt.white, 1, Qt.DashLine) self.dragSelectionStyle.setCosmetic(True) # Set scrollbar self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # DRAWING SETTINGS self.border_pen = QPen(Qt.black, 3) # pen.setJoinStyle(Qt.MiterJoin) # pen.setCapStyle(Qt.RoundCap) self.border_pen.setCosmetic(True) self.border_selected_pen = QPen(Qt.white, 3) self.border_selected_pen.setCosmetic(True) self.showCrossair = False self.mouseCoords = QPointF(0, 0) self.crackWidget = None self.setContextMenuPolicy(Qt.CustomContextMenu) self.refine_grow = 0.0 #maybe should in in tools self.refine_original_mask = None self.refine_original_blob = None def setProject(self, project): self.project = project def setImage(self, image, channel_idx=0): """ Set the image to visualize. The first channel is visualized unless otherwise specified. """ self.clear() self.image = image self.annotations = image.annotations self.selected_blobs = [] for blob in self.annotations.seg_blobs: self.drawBlob(blob) self.scene.invalidate() self.tools.tools['RULER'].setPxToMM(image.map_px_to_mm_factor) self.px_to_mm = image.map_px_to_mm_factor self.setChannel(image.channels[channel_idx]) self.activated.emit() def setChannel(self, channel, switch=False): """ Set the image channel to visualize. If the channel has not been previously loaded it is loaded and cached. """ if self.image is None: raise ("Image has not been previously set in ViewerPlus") self.channel = channel if channel.qimage is not None: img = channel.qimage else: QApplication.setOverrideCursor(Qt.WaitCursor) img = channel.loadData() QApplication.restoreOverrideCursor() if img.isNull(): (channel.filename, filter) = QFileDialog.getOpenFileName( self, "Couldn't find the map, please select it:", QFileInfo(channel.filename).dir().path(), "Image Files (*.png *.jpg)") dir = QDir(os.getcwd()) self.map_image_filename = dir.relativeFilePath(channel.filename) img = QImage(channel.filename) if img.isNull(): raise Exception("Could not load or find the image: " + channel.filename) if switch: self.setChannelImg(img, self.zoom_factor) else: self.setChannelImg(img) def setChannelImg(self, channel_img, zoomf=0.0): """ Set the scene's current image (input image must be a QImage) For calculating the zoom factor automatically set it to 0.0. """ self.setImg(channel_img, zoomf) def clear(self): QtImageViewer.clear(self) self.selected_blobs = [] self.undo_data = Undo() for blob in self.annotations.seg_blobs: self.undrawBlob(blob) del blob self.annotations = Annotation() def drawBlob(self, blob, prev=False): # if it has just been created remove the current graphics item in order to set it again if blob.qpath_gitem is not None: self.scene.removeItem(blob.qpath_gitem) self.scene.removeItem(blob.id_item) del blob.qpath_gitem del blob.id_item blob.qpath_gitem = None blob.id_item = None blob.setupForDrawing() if prev is True: pen = self.border_pen_for_appended_blobs else: pen = self.border_selected_pen if blob in self.selected_blobs else self.border_pen brush = self.project.classBrushFromName(blob) blob.qpath_gitem = self.scene.addPath(blob.qpath, pen, brush) blob.qpath_gitem.setZValue(1) font_size = 12 blob.id_item = TextItem(str(blob.id), QFont("Arial", font_size, QFont.Bold)) self.scene.addItem(blob.id_item) blob.id_item.setPos(blob.centroid[0], blob.centroid[1]) blob.id_item.setTransformOriginPoint( QPointF(blob.centroid[0] + 14.0, blob.centroid[1] + 14.0)) blob.id_item.setZValue(2) blob.id_item.setBrush(Qt.white) #blob.id_item.setDefaultTextColor(Qt.white) #blob.id_item.setFlag(QGraphicsItem.ItemIgnoresTransformations) #blob.qpath_gitem.setOpacity(self.transparency_value) def undrawBlob(self, blob): self.scene.removeItem(blob.qpath_gitem) self.scene.removeItem(blob.id_item) blob.qpath = None blob.qpath_gitem = None blob.id_item = None self.scene.invalidate() def applyTransparency(self, value): self.transparency_value = value / 100.0 # current annotations for blob in self.annotations.seg_blobs: blob.qpath_gitem.setOpacity(self.transparency_value) #used for crossair cursor def drawForeground(self, painter, rect): if self.showCrossair: painter.setClipRect(rect) pen = QPen(Qt.white, 1) pen.setCosmetic(True) painter.setPen(pen) painter.drawLine(self.mouseCoords.x(), rect.top(), self.mouseCoords.x(), rect.bottom()) painter.drawLine(rect.left(), self.mouseCoords.y(), rect.right(), self.mouseCoords.y()) #TOOLS and SELECTIONS def setTool(self, tool): self.tools.setTool(tool) if tool in [ "FREEHAND", "RULER", "DEEPEXTREME" ] or (tool in ["CUT", "EDITBORDER"] and len(self.selected_blobs) > 1): self.resetSelection() if tool == "DEEPEXTREME": self.showCrossair = True else: self.showCrossair = False if tool == "MOVE": self.enablePan() else: self.disablePan() def resetTools(self): self.tools.resetTools() self.showCrossair = False self.scene.invalidate(self.scene.sceneRect()) self.setDragMode(QGraphicsView.NoDrag) #TODO not necessarily a slot @pyqtSlot(float, float) def selectOp(self, x, y): """ Selection operation. """ self.logfile.info("[SELECTION][DOUBLE-CLICK] Selection starts..") if self.tools.tool in ["RULER", "DEEPEXTREME"]: return if not (Qt.ShiftModifier & QApplication.queryKeyboardModifiers()): self.resetSelection() selected_blob = self.annotations.clickedBlob(x, y) if selected_blob: if selected_blob in self.selected_blobs: self.removeFromSelectedList(selected_blob) else: self.addToSelectedList(selected_blob) self.updateInfoPanel.emit(selected_blob) if len(self.selected_blobs) == 1: self.newSelection.emit() self.logfile.info("[SELECTION][DOUBLE-CLICK] Selection ends.") #MOUSE EVENTS def mousePressEvent(self, event): """ Start mouse pan or zoom mode. """ self.activated.emit() scenePos = self.mapToScene(event.pos()) mods = event.modifiers() if event.button() == Qt.LeftButton: (x, y) = self.clipScenePos(scenePos) #used from area selection and pen drawing, if (self.panEnabled and not (mods & Qt.ShiftModifier)) or ( mods & Qt.ControlModifier): self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.tools.tool == "MATCH": self.tools.leftPressed(x, y, mods) elif mods & Qt.ShiftModifier: self.dragSelectionStart = [x, y] self.logfile.info("[SELECTION][DRAG] Selection starts..") else: self.tools.leftPressed(x, y) #self.leftMouseButtonPressed.emit(clippedCoords[0], clippedCoords[1]) # PANNING IS ALWAYS POSSIBLE WITH WHEEL BUTTON PRESSED (!) # if event.button() == Qt.MiddleButton: # self.setDragMode(QGraphicsView.ScrollHandDrag) if event.button() == Qt.RightButton: clippedCoords = self.clipScenePos(scenePos) self.rightMouseButtonPressed.emit(clippedCoords[0], clippedCoords[1]) QGraphicsView.mousePressEvent(self, event) def mouseReleaseEvent(self, event): """ Stop mouse pan or zoom mode (apply zoom if valid). """ QGraphicsView.mouseReleaseEvent(self, event) scenePos = self.mapToScene(event.pos()) if event.button() == Qt.LeftButton: self.setDragMode(QGraphicsView.NoDrag) (x, y) = self.clipScenePos(scenePos) if self.dragSelectionStart: if abs(x - self.dragSelectionStart[0]) < 5 and abs( y - self.dragSelectionStart[1]) < 5: self.selectOp(x, y) else: self.dragSelectBlobs(x, y) self.dragSelectionStart = None if self.dragSelectionRect: self.scene.removeItem(self.dragSelectionRect) del self.dragSelectionRect self.dragSelectionRect = None self.logfile.info("[SELECTION][DRAG] Selection ends.") else: self.tools.leftReleased(x, y) def mouseMoveEvent(self, event): QGraphicsView.mouseMoveEvent(self, event) scenePos = self.mapToScene(event.pos()) if self.showCrossair == True: self.mouseCoords = scenePos self.scene.invalidate(self.sceneRect(), QGraphicsScene.ForegroundLayer) if event.buttons() == Qt.LeftButton: (x, y) = self.clipScenePos(scenePos) if self.dragSelectionStart: start = self.dragSelectionStart if not self.dragSelectionRect: self.dragSelectionRect = self.scene.addRect( start[0], start[1], x - start[0], y - start[1], self.dragSelectionStyle) self.dragSelectionRect.setRect(start[0], start[1], x - start[0], y - start[1]) return if Qt.ControlModifier & QApplication.queryKeyboardModifiers(): return self.tools.mouseMove(x, y) def mouseDoubleClickEvent(self, event): scenePos = self.mapToScene(event.pos()) if event.button() == Qt.LeftButton: self.selectOp(scenePos.x(), scenePos.y()) def wheelEvent(self, event): """ Zoom in/zoom out. """ if self.zoomEnabled: pt = event.angleDelta() #self.zoom_factor = self.zoom_factor + pt.y() / 2400.0 #uniform zoom. self.zoom_factor = self.zoom_factor * pow(pow(2, 1 / 2), pt.y() / 100) if self.zoom_factor < self.ZOOM_FACTOR_MIN: self.zoom_factor = self.ZOOM_FACTOR_MIN if self.zoom_factor > self.ZOOM_FACTOR_MAX: self.zoom_factor = self.ZOOM_FACTOR_MAX self.updateViewer() # PAY ATTENTION !! THE WHEEL INTERACT ALSO WITH THE SCROLL BAR !! #QGraphicsView.wheelEvent(self, event) #VISIBILITY AND SELECTION def dragSelectBlobs(self, x, y): sx = self.dragSelectionStart[0] sy = self.dragSelectionStart[1] self.resetSelection() for blob in self.annotations.seg_blobs: visible = self.project.isLabelVisible(blob.class_name) if not visible: continue box = blob.bbox if sx > box[1] or sy > box[ 0] or x < box[1] + box[2] or y < box[0] + box[3]: continue self.addToSelectedList(blob) @pyqtSlot(str) def setActiveLabel(self, label): self.tools.tools["ASSIGN"].setActiveLabel(label) def setBlobVisible(self, blob, visibility): if blob.qpath_gitem is not None: blob.qpath_gitem.setVisible(visibility) if blob.id_item is not None: blob.id_item.setVisible(visibility) def updateVisibility(self): for blob in self.annotations.seg_blobs: visibility = self.project.isLabelVisible(blob.class_name) self.setBlobVisible(blob, visibility) #SELECTED BLOBS MANAGEMENT def addToSelectedList(self, blob): """ Add the given blob to the list of selected blob. """ if blob in self.selected_blobs: self.logfile.info( "[SELECTION] An already selected blob has been added to the current selection." ) else: self.selected_blobs.append(blob) str = "[SELECTION] A new blob (" + blob.blob_name + ";" + blob.class_name + ") has been selected." self.logfile.info(str) if not blob.qpath_gitem is None: blob.qpath_gitem.setPen(self.border_selected_pen) blob.qpath_gitem.setZValue(3) blob.id_item.setZValue(4) else: print("blob qpath_qitem is None!") self.scene.invalidate() def removeFromSelectedList(self, blob): try: # safer if iterating over selected_blobs and calling this function. self.selected_blobs = [ x for x in self.selected_blobs if not x == blob ] if not blob.qpath_gitem is None: blob.qpath_gitem.setPen(self.border_pen) blob.qpath_gitem.setZValue(1) blob.id_item.setZValue(2) self.scene.invalidate() except Exception as e: print("Exception: e", e) pass def resetSelection(self): for blob in self.selected_blobs: if blob.qpath_gitem is None: print("Selected item with no path!") else: blob.qpath_gitem.setPen(self.border_pen) blob.qpath_gitem.setZValue(1) blob.id_item.setZValue(2) self.selected_blobs.clear() self.scene.invalidate(self.scene.sceneRect()) #CREATION and DESTRUCTION of BLOBS def addBlob(self, blob, selected=False): """ The only function to add annotations. will take care of undo and QGraphicItems. """ self.undo_data.addBlob(blob) #self.undo_data_operation['remove'].append(blob) self.annotations.addBlob(blob) self.drawBlob(blob) if selected: self.addToSelectedList(blob) def removeBlob(self, blob): """ The only function to remove annotations. """ self.removeFromSelectedList(blob) self.undrawBlob(blob) self.undo_data.removeBlob(blob) self.annotations.removeBlob(blob) def deleteSelectedBlobs(self): for blob in self.selected_blobs: self.removeBlob(blob) self.saveUndo() def setBlobClass(self, blob, class_name): if blob.class_name == class_name: return self.undo_data.setBlobClass(blob, class_name) blob.class_name = class_name #THIS should be removed: the color comes from the labels! blob.class_color = self.project.labels[blob.class_name].fill brush = self.project.classBrushFromName(blob) blob.qpath_gitem.setBrush(brush) self.scene.invalidate() #UNDO STUFF #UNDO STUFF def saveUndo(self): self.undo_data.saveUndo() def undo(self): operation = self.undo_data.undo() if operation is None: return for blob in operation['remove']: message = "[UNDO][REMOVE] BLOBID={:d} VERSION={:d}".format( blob.id, blob.version) self.logfile.info(message) self.removeFromSelectedList(blob) self.undrawBlob(blob) self.annotations.removeBlob(blob) for blob in operation['add']: message = "[UNDO][ADD] BLOBID={:d} VERSION={:d}".format( blob.id, blob.version) self.logfile.info(message) self.annotations.addBlob(blob) self.selected_blobs.append(blob) self.drawBlob(blob) for (blob, class_name) in operation['class']: blob.class_name = class_name brush = self.project.classBrushFromName(blob) blob.qpath_gitem.setBrush(brush) self.updateVisibility() def redo(self): operation = self.undo_data.redo() if operation is None: return for blob in operation['add']: message = "[REDO][ADD] BLOBID={:d} VERSION={:d}".format( blob.id, blob.version) self.logfile.info(message) self.removeFromSelectedList(blob) self.undrawBlob(blob) self.annotations.removeBlob(blob) for blob in operation['remove']: message = "[REDO][REMOVE] BLOBID={:d} VERSION={:d}".format( blob.id, blob.version) self.logfile.info(message) self.annotations.addBlob(blob) self.selected_blobs.append(blob) self.drawBlob(blob) for (blob, class_name) in operation['newclass']: blob.class_name = class_name brush = self.project.classBrushFromName(blob) blob.qpath_gitem.setBrush(brush) self.updateVisibility()