class QImageEdit(QLabel): def __init__(self, parentQWidget=None): super(QImageEdit, self).__init__(parentQWidget) self.rubberBand = None self.move_rubberBand = False self.rubberBand_offset = None self.originPoint = None def setImage(self, image: QPixmap): self.setPixmap(image) def getImage(self) -> QPixmap: if self.rubberBand is not None: currentRect = self.rubberBand.geometry() return self.pixmap().copy(currentRect) else: return self.pixmap() def clear(self): super(QImageEdit, self).clear() if self.rubberBand is not None: self.rubberBand.deleteLater() self.rubberBand = None self.move_rubberBand = False self.rubberBand_offset = None self.originPoint = None def mousePressEvent(self, event): self.originPoint = event.pos() if self.rubberBand is None: self.rubberBand = QRubberBand(QRubberBand.Rectangle, self) self.rubberBand.setGeometry(QRect(self.originPoint, QSize())) self.rubberBand.show() else: if self.rubberBand.geometry().contains(self.originPoint): self.rubberBand_offset = \ self.originPoint - self.rubberBand.pos() self.move_rubberBand = True else: self.rubberBand.hide() self.rubberBand.deleteLater() self.rubberBand = None self.move_rubberBand = False self.rubberBand_offset = None self.mousePressEvent(event) def mouseMoveEvent(self, event): newPoint = event.pos() if self.move_rubberBand: self.rubberBand.move(newPoint - self.rubberBand_offset) else: self.rubberBand.setGeometry( QRect(self.originPoint, newPoint).normalized()) def mouseReleaseEvent(self, event): self.move_rubberBand = False
class PageScrollArea(QScrollArea): reachbottom = pyqtSignal() reachtop = pyqtSignal() areaSelected = pyqtSignal(QRect) def __init__(self, parent): super().__init__(parent) def wheelEvent(self, event): super().wheelEvent(event) if self.verticalScrollBar().value() == self.verticalScrollBar( ).maximum() and event.angleDelta().y() == -120: self.reachbottom.emit() if self.verticalScrollBar().value() == 0 and event.angleDelta().y( ) == 120: self.reachtop.emit() def mousePressEvent(self, event): self.rubberorigin = event.pos() self.rubberband = QRubberBand(QRubberBand.Rectangle, self) self.rubberband.setGeometry(QRect(self.rubberorigin, QSize())) self.rubberband.show() def mouseReleaseEvent(self, event): rect = QRect(self.rubberband.pos(), self.rubberband.size()) self.areaSelected.emit(rect) self.rubberband.hide() def mouseMoveEvent(self, event): super().mouseMoveEvent(event) self.rubberband.setGeometry( QRect(self.rubberorigin, event.pos()).normalized()) def keyPressEvent(self, event): super().keyPressEvent(event) if self.verticalScrollBar().value() == self.verticalScrollBar( ).maximum() and (event.key() == 16777237 or event.key() == 16777239): self.reachbottom.emit() if self.verticalScrollBar().value() == 0 and ( event.key() == 16777235 or event.key() == 16777238): self.reachtop.emit()
class SlideViewer(QWidget): eventSignal = pyqtSignal(PyQt5.QtCore.QEvent) def __init__(self, parent: QWidget = None, viewer_top_else_left=True): super().__init__(parent) self.init_view() self.init_labels(word_wrap=viewer_top_else_left) self.init_layout(viewer_top_else_left) def init_view(self): self.scene = MyGraphicsScene() self.view = QGraphicsView() self.view.setScene(self.scene) self.view.setTransformationAnchor(QGraphicsView.NoAnchor) self.view.viewport().installEventFilter(self) self.rubber_band = QRubberBand(QRubberBand.Rectangle, self) self.mouse_press_view = QPoint() self.view.horizontalScrollBar().sliderMoved.connect( self.on_view_changed) self.view.verticalScrollBar().sliderMoved.connect(self.on_view_changed) self.scale_initializer_deffered_function = None self.slide_view_params = None self.slide_helper = None def init_labels(self, word_wrap): # word_wrap = True self.level_downsample_label = QLabel() self.level_downsample_label.setWordWrap(word_wrap) self.level_size_label = QLabel() self.level_size_label.setWordWrap(word_wrap) self.selected_rect_label = QLabel() self.selected_rect_label.setWordWrap(word_wrap) self.mouse_pos_scene_label = QLabel() self.mouse_pos_scene_label.setWordWrap(word_wrap) self.view_rect_scene_label = QLabel() self.view_rect_scene_label.setWordWrap(word_wrap) self.labels_layout = QVBoxLayout() self.labels_layout.setAlignment(Qt.AlignTop) self.labels_layout.addWidget(self.level_downsample_label) self.labels_layout.addWidget(self.level_size_label) self.labels_layout.addWidget(self.mouse_pos_scene_label) # self.labels_layout.addWidget(self.selected_rect_label) self.labels_layout.addWidget(self.view_rect_scene_label) def init_layout(self, viewer_top_else_left=True): main_layout = QVBoxLayout( self) if viewer_top_else_left else QHBoxLayout(self) main_layout.addWidget(self.view, ) main_layout.addLayout(self.labels_layout) # main_layout.setContentsMargins(0, 0, 0, 0) self.setLayout(main_layout) """ If you want to start view frome some point at some level, specify <level> and <level_rect> params. level_rect : rect in dimensions of slide at level=level. If None - fits the whole size of slide """ def load(self, slide_view_params: SlideViewParams, preffered_rects_count=2000, zoom_step=1.15): self.zoom_step = zoom_step self.slide_view_params = slide_view_params self.slide_helper = SlideHelper(slide_view_params.slide_path) self.slide_graphics = SlideGraphicsGroup(slide_view_params, preffered_rects_count) self.scene.clear() self.scene.addItem(self.slide_graphics) if self.slide_view_params.level == -1 or self.slide_view_params.level is None: self.slide_view_params.level = self.slide_helper.get_max_level() self.slide_graphics.update_visible_level(self.slide_view_params.level) self.scene.setSceneRect( self.slide_helper.get_rect_for_level(self.slide_view_params.level)) def scale_initializer_deffered_function(): self.view.resetTransform() # print("size when loading: ", self.view.viewport().size()) if self.slide_view_params.level_rect: # self.view.fitInView(QRectF(*self.slide_view_params.level_rect), Qt.KeepAspectRatioByExpanding) self.view.fitInView(QRectF(*self.slide_view_params.level_rect), Qt.KeepAspectRatio) # print("after fit: ", self.get_current_view_scene_rect()) else: start_margins = QMarginsF(200, 200, 200, 200) start_image_rect_ = self.slide_helper.get_rect_for_level( self.slide_view_params.level) self.view.fitInView(start_image_rect_ + start_margins, Qt.KeepAspectRatio) self.scale_initializer_deffered_function = scale_initializer_deffered_function def eventFilter(self, qobj: 'QObject', event: QEvent): self.eventSignal.emit(event) event_processed = False # print("size when event: ", event, event.type(), self.view.viewport().size()) if isinstance(event, QShowEvent): """ we need it deffered because fitInView logic depends on current viewport size. Expecting at this point widget is finally resized before being shown at first """ if self.scale_initializer_deffered_function: # TODO labels start to occupy some space after view was already fitted, and labels will reduce size of viewport # self.update_labels() self.scale_initializer_deffered_function() self.on_view_changed() self.scale_initializer_deffered_function = None elif isinstance(event, QWheelEvent): event_processed = self.process_viewport_wheel_event(event) # we handle wheel event to prevent GraphicsView interpret it as scrolling elif isinstance(event, QMouseEvent): event_processed = self.process_mouse_event(event) return event_processed def process_viewport_wheel_event(self, event: QWheelEvent): # print("size when wheeling: ", self.view.viewport().size()) zoom_in = self.zoom_step zoom_out = 1 / zoom_in zoom_ = zoom_in if event.angleDelta().y() > 0 else zoom_out self.update_scale(event.pos(), zoom_) event.accept() self.on_view_changed() return True def process_mouse_event(self, event: QMouseEvent): if self.slide_helper is None: return False if event.button() == Qt.MiddleButton: if event.type() == QEvent.MouseButtonPress: self.slide_graphics.update_grid_visibility( not self.slide_graphics.slide_view_params.grid_visible) # items=self.scene.items() # QMessageBox.information(None, "Items", str(items)) return True # self.update_scale(QPoint(), 1.15) elif event.button() == Qt.LeftButton: if event.type() == QEvent.MouseButtonPress: self.mouse_press_view = QPoint(event.pos()) self.rubber_band.setGeometry( QRect(self.mouse_press_view, QSize())) self.rubber_band.show() return True elif event.type() == QEvent.MouseButtonRelease: self.rubber_band.hide() self.remember_selected_rect_params() self.slide_graphics.update_selected_rect_0_level( self.slide_view_params.selected_rect_0_level) self.update_labels() self.scene.invalidate() return True elif event.type() == QEvent.MouseMove: self.mouse_pos_scene_label.setText( "mouse_scene: " + point_to_str(self.view.mapToScene(event.pos()))) if not self.mouse_press_view.isNull(): self.rubber_band.setGeometry( QRect(self.mouse_press_view, event.pos()).normalized()) return True return False def remember_selected_rect_params(self): pos_scene = self.view.mapToScene(self.rubber_band.pos()) rect_scene = self.view.mapToScene( self.rubber_band.rect()).boundingRect() downsample = self.slide_helper.get_downsample_for_level( self.slide_view_params.level) selected_qrectf_0_level = QRectF(pos_scene * downsample, rect_scene.size() * downsample) self.slide_view_params.selected_rect_0_level = selected_qrectf_0_level.getRect( ) def update_scale(self, mouse_pos: QPoint, zoom): old_mouse_pos_scene = self.view.mapToScene(mouse_pos) old_view_scene_rect = self.view.mapToScene( self.view.viewport().rect()).boundingRect() old_level = self.get_best_level_for_scale( self.get_current_view_scale()) old_level_downsample = self.slide_helper.get_downsample_for_level( old_level) new_level = self.get_best_level_for_scale( self.get_current_view_scale() * zoom) new_level_downsample = self.slide_helper.get_downsample_for_level( new_level) level_scale_delta = 1 / (new_level_downsample / old_level_downsample) r = old_view_scene_rect.topLeft() m = old_mouse_pos_scene new_view_scene_rect_top_left = (m - (m - r) / zoom) * level_scale_delta new_view_scene_rect = QRectF( new_view_scene_rect_top_left, old_view_scene_rect.size() * level_scale_delta / zoom) new_scale = self.get_current_view_scale( ) * zoom * new_level_downsample / old_level_downsample transform = QTransform().scale(new_scale, new_scale).translate( -new_view_scene_rect.x(), -new_view_scene_rect.y()) new_rect = self.slide_helper.get_rect_for_level(new_level) self.scene.setSceneRect(new_rect) self.slide_view_params.level = new_level self.reset_view_transform() self.view.setTransform(transform, False) self.slide_graphics.update_visible_level(new_level) self.update_labels() def get_best_level_for_scale(self, scale): scene_width = self.scene.sceneRect().size().width() candidates = [0] for level in self.slide_helper.get_levels(): w, h = self.slide_helper.get_level_size(level) if scene_width * scale <= w: candidates.append(level) best_level = max(candidates) return best_level def update_labels(self): level_downsample = self.slide_helper.get_downsample_for_level( self.slide_view_params.level) level_size = self.slide_helper.get_level_size( self.slide_view_params.level) self.level_downsample_label.setText( "level, downsample: {}, {:.0f}".format( self.slide_view_params.level, level_downsample)) self.level_size_label.setText( "level_size: ({}, {})".format(*level_size)) self.view_rect_scene_label.setText( "view_scene: ({:.0f},{:.0f},{:.0f},{:.0f})".format( *self.get_current_view_scene_rect().getRect())) if self.slide_view_params.selected_rect_0_level: self.selected_rect_label.setText( "selected rect (0-level): ({:.0f},{:.0f},{:.0f},{:.0f})". format(*self.slide_view_params.selected_rect_0_level)) def on_view_changed(self): if self.scale_initializer_deffered_function is None and self.slide_view_params: self.slide_view_params.level_rect = self.get_current_view_scene_rect( ).getRect() self.update_labels() def reset_view_transform(self): self.view.resetTransform() self.view.horizontalScrollBar().setValue(0) self.view.verticalScrollBar().setValue(0) def get_current_view_scene_rect(self): return self.view.mapToScene(self.view.viewport().rect()).boundingRect() def get_current_view_scale(self): scale = self.view.transform().m11() return scale
class TcamScreen(QtWidgets.QGraphicsView): new_pixmap = pyqtSignal(QtGui.QPixmap) new_pixel_under_mouse = pyqtSignal(bool, int, int, QtGui.QColor) destroy_widget = pyqtSignal() fit_in_view = pyqtSignal() def __init__(self, parent=None): super(TcamScreen, self).__init__(parent) self.setMouseTracking(True) self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self.setDragMode(QGraphicsView.ScrollHandDrag) self.setFrameStyle(0) self.scene = QGraphicsScene(self) self.setScene(self.scene) self.new_pixmap.connect(self.on_new_pixmap) self.fit_in_view.connect(self.fit_view) self.pix = ViewItem() self.scene.addItem(self.pix) self.scene.setSceneRect(self.pix.boundingRect()) self.is_fullscreen = False # Flag to differentiate between actual images # and 'fake images' i.e. color background + text while # waiting for first trigger image self.display_real_image = True self.text_item = None self.fit_in_view_called = False self.mouse_position_x = -1 self.mouse_position_y = -1 self.zoom_factor = 1.0 self.first_image = True self.image_counter = 0 self.capture_roi = False self.roi_obj = None self.roi_origin = None self.roi_widgets = [] self.selection_area = None self.capture_widget = None self.origin = None def fit_view(self): """ """ self.reset_zoom() self.scene.setSceneRect(self.pix.boundingRect()) self.scene.update() self.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio) def reset_zoom(self): self.zoom_factor = 1.0 # this resets the view internal transformation matrix self.setTransform(QtGui.QTransform()) def on_new_pixmap(self, pixmap): self.image_counter += 1 self.pix.setPixmap(pixmap) if not self.display_real_image: self.text_item.hide() self.scene.removeItem(self.text_item) self.display_real_image = True if self.image_counter == 1: self.resize(self.size()) self.scene.setSceneRect(self.pix.boundingRect()) self.update() self.reset_zoom() self.first_image = False # wait for the second image # resizeEvents, etc appear before the scene has adjusted # to the actual image size. By waiting for the 2. image # we circumvent this by having the first image making all # adjustments for us. The only scenario where this will # cause problems is triggering. if self.is_fullscreen and self.image_counter == 2: self.fit_view() self.send_mouse_pixel() # don't call repaint here # it causes problems once the screen goes blank due to screensavers, etc # self.repaint() def wait_for_first_image(self): if not self.display_real_image: return self.reset_zoom() self.display_real_image = False self.text_item = QGraphicsTextItem() self.text_item.setDefaultTextColor(QColor("white")) self.text_item.setPos(100, 70) self.text_item.setPlainText("In Trigger Mode. Waiting for first image...") bg = QPixmap(1280, 720) bg.fill(QColor("grey")) self.pix.setPixmap(bg) self.image_counter += 1 self.scene.addItem(self.text_item) def send_mouse_pixel(self): # mouse positions start at 0 # we want the lower right corner to have the correct coordinates # e.g. an 1920x1080 image should have the coordinates # 1920x1080 for the last pixel self.new_pixel_under_mouse.emit(self.pix.legal_coordinates(self.mouse_position_x, self.mouse_position_y), self.mouse_position_x + 1, self.mouse_position_y + 1, self.pix.get_color_at_position(self.mouse_position_x, self.mouse_position_y)) def mouseMoveEvent(self, event): mouse_position = self.mapToScene(event.pos()) self.mouse_position_x = mouse_position.x() self.mouse_position_y = mouse_position.y() if self.selection_area: # adjust rect since we want to pull in all directions # origin can well be bottom left, thus recalc def calc_selection_rect(): x = min(self.origin.x(), event.pos().x()) y = min(self.origin.y(), event.pos().y()) x2 = max(self.origin.x(), event.pos().x()) y2 = max(self.origin.y(), event.pos().y()) return QPoint(x, y), QPoint(x2, y2) p1, p2 = calc_selection_rect() self.selection_area.setGeometry(QRect(p1, p2)) super().mouseMoveEvent(event) def mousePressEvent(self, event): """""" if self.capture_widget: self.selection_area = QRubberBand(QRubberBand.Rectangle, self) self.selection_area.setGeometry(QRect(event.pos(), QSize())) self.origin = event.pos() self.selection_area.show() super().mousePressEvent(event) def mouseReleaseEvent(self, event): if self.capture_widget: selectionBBox = self.selection_area.rect() rect = QRect(self.selection_area.pos(), selectionBBox.size()) selectionBBox = self.mapToScene(rect).boundingRect().toRect() self.capture_widget.emit(selectionBBox) self.selection_area.hide() self.selection_area = None QApplication.restoreOverrideCursor() self.capture_widget = None self.selection_area = None super().mouseReleaseEvent(event) def is_scene_larger_than_image(self): """ checks if the entire ViewItem is visible in the scene """ port_rect = self.viewport().rect() scene_rect = self.mapToScene(port_rect).boundingRect() item_rect = self.pix.mapRectFromScene(scene_rect) isec = item_rect.intersected(self.pix.boundingRect()) res = self.pix.get_resolution() if (isec.size().width() >= QSizeF(res).width() and isec.size().height() >= QSizeF(res).height()): return True return False def wheelEvent(self, event): if not self.display_real_image: return # Zoom Factor zoomInFactor = 1.25 zoomOutFactor = 1 / zoomInFactor # Set Anchors self.setTransformationAnchor(QGraphicsView.NoAnchor) self.setResizeAnchor(QGraphicsView.NoAnchor) # Save the scene pos oldPos = self.mapToScene(event.pos()) # Zoom if event.angleDelta().y() > 0: zoomFactor = zoomInFactor else: zoomFactor = zoomOutFactor if (self.is_scene_larger_than_image() and zoomFactor < 1.0): return self.zoom_factor *= zoomFactor # we scale the view itself to get infinite zoom # so that we can inspect a single pixel self.scale(zoomFactor, zoomFactor) # Get the new position newPos = self.mapToScene(event.pos()) # Move scene to old position delta = newPos - oldPos self.translate(delta.x(), delta.y()) self.scene.setSceneRect(self.pix.boundingRect()) def set_scale_position(self, scale_factor, x, y): self.scale(scale_factor, scale_factor) self.translate(x, y) def keyPressEvent(self, event): if self.isFullScreen(): if (event.key() == Qt.Key_F11 or event.key() == Qt.Key_Escape or event.key() == Qt.Key_F): self.destroy_widget.emit() elif self.capture_widget and event.key() == Qt.Key_Escape: self.abort_roi_capture() else: # Ignore event so that parent widgets can use it. # This is only called when we are not fullscreen. # Fullscreen causes us to have no parents. event.ignore() def start_roi_capture(self, finished_signal): """ Capture a region of interest """ self.capture_widget = finished_signal QApplication.setOverrideCursor(Qt.CrossCursor) def abort_roi_capture(self): """ Abort the capture of a regoin of interest """ self.capture_widget = None self.origin = None if self.selection_area: self.selection_area.hide() self.selection_area = None QApplication.restoreOverrideCursor() def add_roi(self, roi_widget): """ Add roi_widget to the QGraphicsScene for display """ if not roi_widget: return self.roi_widgets.append(roi_widget) self.scene.addItem(roi_widget) roi_widget.show() def remove_roi(self, roi_widget): """ Remove given roi widget from the scene """ if not roi_widget: return roi_widget.hide() try: self.roi_widgets.remove(roi_widget) except ValueError as e: # This means the widget is not in the list pass