How can I highlight sections of an image between two right-clicks?
-
I've got the general visualizer so far, but I'm stuck at how to implement a multi-column spanning highlighter, if that makes sense (I'll attach an example of what I mean).
My goal is to display multiple photographic images stacked vertically (Photo 1.jpg, Photo 2.jpg), with a transparent overlay applied over them (Overlay 1.png, Overlay 2.png).
The resulting highlight should be like this (as a purple highlighter here, between the two green dots representing consecutive right-clicks), although not necessarily purple (I'd prefer to just increase that section's brightness, actually).
from PyQt6.QtCore import QPointF, Qt, pyqtSignal from PyQt6.QtGui import QImage, QMouseEvent, QPixmap, QTransform, QWheelEvent from PyQt6.QtOpenGLWidgets import QOpenGLWidget from PyQt6.QtWidgets import QApplication, QGraphicsPixmapItem, QGraphicsScene, QGraphicsView, QLabel, QPushButton, QSlider, QVBoxLayout, QWidget class ImageOverlayApp(QWidget): def __init__(self, corebox_images: list[str], overlay_images: list[str]) -> None: super().__init__() self.setWindowTitle("Viewer") # Initial opacity self.alpha = 0.5 # Prepare the scene self.scene = QGraphicsScene() # Lists to hold pixmap items and images self.pixmap_items_core: list[QGraphicsPixmapItem] = [] self.pixmap_items_overlay: list[QGraphicsPixmapItem] = [] self.overlay_images: list[QImage] = [] # Transformation for rotation transform = QTransform() transform.rotate(-90) # Rotate 90 degrees counterclockwise y_offset = 0 # Vertical position offset for core_img_path, overlay_img_path in zip(corebox_images, overlay_images, strict=True): # Load images as QImage core_photo = QImage(core_img_path) false_color = QImage(overlay_img_path) # Rotate images core_photo = core_photo.transformed(transform, Qt.TransformationMode.FastTransformation) false_color = false_color.transformed(transform, Qt.TransformationMode.FastTransformation) # Resize overlay image to match core photo false_color = false_color.scaled(core_photo.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation) # Create pixmap items pixmap_item_core = QGraphicsPixmapItem(QPixmap.fromImage(core_photo)) pixmap_item_overlay = QGraphicsPixmapItem(QPixmap.fromImage(false_color)) pixmap_item_overlay.setOpacity(self.alpha) # Position pixmap items vertically pixmap_item_core.setPos(0, y_offset) pixmap_item_overlay.setPos(0, y_offset) # Add items to scene self.scene.addItem(pixmap_item_core) self.scene.addItem(pixmap_item_overlay) # Store pixmap items and images self.pixmap_items_core.append(pixmap_item_core) self.pixmap_items_overlay.append(pixmap_item_overlay) self.overlay_images.append(false_color) # Update y_offset for the next image y_offset += core_photo.height() # Set up QGraphicsView with OpenGL for performance self.view = GraphicsView(self.scene) self.view.setViewport(QOpenGLWidget()) self.view.setBackgroundBrush(Qt.GlobalColor.transparent) self.view.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) self.view.setOptimizationFlags(QGraphicsView.OptimizationFlag.DontSavePainterState) self.view.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.SmartViewportUpdate) # Connect signals self.view.clicked.connect(self.on_image_clicked) # Slider for adjusting transparency self.slider = QSlider(Qt.Orientation.Horizontal) self.slider.setRange(0, 100) self.slider.setValue(int(self.alpha * 100)) self.slider.valueChanged.connect(self.slider_changed) # Label for transparency slider self.slider_label = QLabel("Transparency") # Button to reset zoom self.reset_button = QPushButton("Reset Zoom") self.reset_button.clicked.connect(self.reset_zoom) # Layout layout = QVBoxLayout() layout.addWidget(self.view) layout.addWidget(self.slider_label) layout.addWidget(self.slider) layout.addWidget(self.reset_button) self.setLayout(layout) def reset_zoom(self) -> None: """Reset the zoom to the original state.""" self.view.resetTransform() self.view.zoom = 0 def slider_changed(self, value: float) -> None: """Adjust the opacity of the overlay images.""" self.alpha = value / 100 for pixmap_item_overlay in self.pixmap_items_overlay: pixmap_item_overlay.setOpacity(self.alpha) def on_image_clicked(self, scene_pos: QPointF) -> None: """Handle clicks on the image.""" # Now check which image was clicked for index, pixmap_item_overlay in enumerate(self.pixmap_items_overlay): # Get the bounding rectangle of the item in scene coordinates item_rect = pixmap_item_overlay.mapRectToScene(pixmap_item_overlay.boundingRect()) if item_rect.contains(scene_pos): # Map the scene position to the item's local coordinates item_pos = pixmap_item_overlay.mapFromScene(scene_pos) x = item_pos.x() y = item_pos.y() # Optionally, get the pixel value from the original image pixel_value = self.overlay_images[index].pixelColor(int(x), int(y)) print(f"Clicked on image {index} at coordinates: ({int(x)}, {int(y)})") print(f"Pixel color: {pixel_value}") # Process the coordinates as needed break class GraphicsView(QGraphicsView): clicked = pyqtSignal(QPointF) def __init__(self, scene: QGraphicsScene) -> None: super().__init__(scene) self.zoom = 0 self.setMouseTracking(True) self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) def wheelEvent(self, event: QWheelEvent) -> None: """Handle mouse wheel events for panning and zooming.""" if event.modifiers() & Qt.KeyboardModifier.ControlModifier: # Handle zooming zoom_in_factor = 1.25 zoom_out_factor = 1.0 / zoom_in_factor # Save the scene pos old_pos = self.mapToScene(event.position().toPoint()) # Zoom if event.angleDelta().y() > 0: zoom_factor = zoom_in_factor self.zoom += 1 else: zoom_factor = zoom_out_factor self.zoom -= 1 # Scale the viewer self.scale(zoom_factor, zoom_factor) # Get the new position new_pos = self.mapToScene(event.position().toPoint()) # Move scene to old position to achieve zoom at cursor delta = new_pos - old_pos self.translate(delta.x(), delta.y()) else: # Handle panning with the scroll wheel delta = event.pixelDelta() if not event.pixelDelta().isNull() else event.angleDelta() / 8 # Invert delta to match the scroll direction self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x()) self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y()) def mousePressEvent(self, event: QMouseEvent) -> None: """Handle mouse press events.""" if event.button() == Qt.MouseButton.LeftButton: scene_pos = self.mapToScene(event.position().toPoint()) self.clicked.emit(scene_pos) super().mousePressEvent(event) def mouseReleaseEvent(self, event: QMouseEvent) -> None: """Disable dragging when mouse is released.""" if event.button() == Qt.MouseButton.LeftButton: self.setDragMode(QGraphicsView.DragMode.NoDrag) super().mouseReleaseEvent(event) # Run the application if __name__ == "__main__": app = QApplication([]) window = ImageOverlayApp( [ r"Photo 1.jpg", r"Photo 2.jpg", ], [ r"Overlay 1.png", r"Overlay 2.png", ], ) window.show() app.exec()
-
Hi,
It seems you would need an item on top of your images that you would paint transparently and then use something like QPainterPath to create the shape for your selection. The QGraphicsPathItem might be enough for your needs.
Hope it helps