From b69dbdf728c158b7f6edae08fd3daf3f35cfa66a Mon Sep 17 00:00:00 2001 From: s224389 <s224389@student.dtu.dk> Date: Sun, 12 Jan 2025 16:46:11 +0100 Subject: [PATCH] Added "delete all points" button and functionality --- GUI_draft.py | 547 ++++++++++++++++++++++++++------------------------- 1 file changed, 280 insertions(+), 267 deletions(-) diff --git a/GUI_draft.py b/GUI_draft.py index 966829f..7cd2f3a 100644 --- a/GUI_draft.py +++ b/GUI_draft.py @@ -1,267 +1,280 @@ -import sys -import numpy as np - -from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QGraphicsView, QGraphicsScene, - QGraphicsEllipseItem, QGraphicsPixmapItem, QPushButton, - QHBoxLayout, QVBoxLayout, QWidget, QFileDialog -) -from PyQt5.QtGui import QPixmap, QPen, QBrush -from PyQt5.QtCore import Qt, QRectF - - -class ImageGraphicsView(QGraphicsView): - """ - Custom class inheriting from QGraphicsView for displaying an image and placing red dots. - """ - def __init__(self, parent=None): - super().__init__(parent) - - # Create scene and add it to the view - self.scene = QGraphicsScene(self) - self.setScene(self.scene) - - # Zoom around mouse pointer - self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) - - # Image item - self.image_item = QGraphicsPixmapItem() - self.scene.addItem(self.image_item) - - # Points and dot items - self.points = [] - self.point_items = [] - self.editor_mode = False - self.dot_radius = 4 - - # Enable built-in panning around image, but force arrow cursor initially - self.setDragMode(QGraphicsView.ScrollHandDrag) - self.viewport().setCursor(Qt.ArrowCursor) - - # Track clicking vs. dragging - self._mouse_pressed = False - self._press_view_pos = None - self._drag_threshold = 5 - self._was_dragging = False - - def load_image(self, image_path): - """Load an image and fit it in the view.""" - pixmap = QPixmap(image_path) - - if not pixmap.isNull(): - self.image_item.setPixmap(pixmap) - - # Avoid TypeError by converting to QRectF - self.setSceneRect(QRectF(pixmap.rect())) - - # Clear existing dots from previous image - self.points.clear() - self._clear_point_items() - - # Reset transform then fit image in view - self.resetTransform() - self.fitInView(self.image_item, Qt.KeepAspectRatio) - - def set_editor_mode(self, mode: bool): - """If True: place/remove dots; if False: do nothing on click.""" - self.editor_mode = mode - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton: - self._mouse_pressed = True - self._was_dragging = False - self._press_view_pos = event.pos() - - # Switch to closed-hand cursor while left mouse is down - self.viewport().setCursor(Qt.ClosedHandCursor) - - elif event.button() == Qt.RightButton: - # If Editor Mode is on remove the nearest dot - if self.editor_mode: - self._remove_point(event.pos()) - - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - """ - If movement > _drag_threshold: consider it a drag. - The actual panning is handled by QGraphicsView in ScrollHandDrag mode. - """ - # Check if the mouse is being dragged - if self._mouse_pressed and (event.buttons() & Qt.LeftButton): - # If the mouse moved more than the threshold, consider it a drag - dist = (event.pos() - self._press_view_pos).manhattanLength() - if dist > self._drag_threshold: - self._was_dragging = True - - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - """ - After releasing the left button, go back to arrow cursor. - If it wasn't a drag, treat as a click (Editor Mode: add dot). - """ - # Let QGraphicsView handle release first - super().mouseReleaseEvent(event) - - if event.button() == Qt.LeftButton and self._mouse_pressed: - self._mouse_pressed = False - - # Always go back to arrow cursor AFTER letting QGraphicsView handle release - self.viewport().setCursor(Qt.ArrowCursor) - - if not self._was_dragging: - # It's a click: if editor mode is ON add a dot - if self.editor_mode: - self._add_point(event.pos()) - - self._was_dragging = False - - def wheelEvent(self, event): - """Mouse wheel = zoom.""" - zoom_in_factor = 1.25 - zoom_out_factor = 1 / zoom_in_factor - - if event.angleDelta().y() > 0: - self.scale(zoom_in_factor, zoom_in_factor) - else: - self.scale(zoom_out_factor, zoom_out_factor) - - event.accept() - - - # -------------- Red Dots -------------- - def _add_point(self, view_pos): - """Add a red dot at scene coords corresponding to view_pos.""" - scene_pos = self.mapToScene(view_pos) - x, y = scene_pos.x(), scene_pos.y() - - self.points.append((x, y)) - dot = self._create_dot_item(x, y) - self.point_items.append(dot) - self.scene.addItem(dot) - - def _remove_point(self, view_pos): - """Right-click: remove nearest dot if within threshold.""" - scene_pos = self.mapToScene(view_pos) - x_click, y_click = scene_pos.x(), scene_pos.y() - - # Define threshold for removing a point - threshold = 10 - closest_idx = None - min_dist = float('inf') - - # Find the closest point to the click - for i, (x, y) in enumerate(self.points): - dist_sq = (x - x_click)**2 + (y - y_click)**2 - if dist_sq < min_dist: - min_dist = dist_sq - closest_idx = i - - # Remove the closest point if it's within the threshold - if closest_idx is not None and min_dist <= threshold**2: - self.scene.removeItem(self.point_items[closest_idx]) - del self.point_items[closest_idx] - del self.points[closest_idx] - - def _create_dot_item(self, x, y): - """Helper for creating a small red ellipse item.""" - r = self.dot_radius - ellipse = QGraphicsEllipseItem(x - r, y - r, 2*r, 2*r) - ellipse.setBrush(QBrush(Qt.red)) - ellipse.setPen(QPen(Qt.red)) - return ellipse - - def _clear_point_items(self): - """Remove all dot items from the scene.""" - for item in self.point_items: - self.scene.removeItem(item) - self.point_items = [] - - -class MainWindow(QMainWindow): - """ - Main window with: - - Button to load in image - - Editor mode toggle button - - Button for exporting placed points - """ - def __init__(self): - super().__init__() - self.setWindowTitle("Test GUI") - - main_widget = QWidget() - main_layout = QVBoxLayout(main_widget) - - self.image_view = ImageGraphicsView() - main_layout.addWidget(self.image_view) - - btn_layout = QHBoxLayout() - - # Load Image - self.btn_load_image = QPushButton("Load Image") - self.btn_load_image.clicked.connect(self.load_image) - btn_layout.addWidget(self.btn_load_image) - - # Editor Mode - self.btn_editor_mode = QPushButton("Editor Mode: OFF") - self.btn_editor_mode.setCheckable(True) - self.btn_editor_mode.setStyleSheet("background-color: lightgray;") - self.btn_editor_mode.clicked.connect(self.toggle_editor_mode) - btn_layout.addWidget(self.btn_editor_mode) - - # Export Points - self.btn_export_points = QPushButton("Export Points") - self.btn_export_points.clicked.connect(self.export_points) - btn_layout.addWidget(self.btn_export_points) - - main_layout.addLayout(btn_layout) - self.setCentralWidget(main_widget) - self.resize(900, 600) - - def load_image(self): - """Open a file dialog to pick an image then load it.""" - options = QFileDialog.Options() - file_path, _ = QFileDialog.getOpenFileName( - self, "Open Image", "", - "Images (*.png *.jpg *.jpeg *.bmp *.tif)", - options=options - ) - if file_path: - self.image_view.load_image(file_path) - - def toggle_editor_mode(self): - """Toggle whether left-click places dots and right-click removes dots.""" - is_checked = self.btn_editor_mode.isChecked() - self.image_view.set_editor_mode(is_checked) - - if is_checked: - self.btn_editor_mode.setText("Editor Mode: ON") - self.btn_editor_mode.setStyleSheet("background-color: #ffcccc;") - else: - self.btn_editor_mode.setText("Editor Mode: OFF") - self.btn_editor_mode.setStyleSheet("background-color: lightgray;") - - def export_points(self): - """Save the list of dot coords to a .npy file.""" - options = QFileDialog.Options() - file_path, _ = QFileDialog.getSaveFileName( - self, "Export Points", "", - "NumPy Files (*.npy);;All Files (*)", - options=options - ) - if file_path: - points_array = np.array(self.image_view.points) - np.save(file_path, points_array) - print(f"Exported {len(points_array)} points to {file_path}") - - -def main(): - app = QApplication(sys.argv) - window = MainWindow() - window.show() - sys.exit(app.exec_()) - - -if __name__ == '__main__': - main() +import sys +import numpy as np + +from PyQt5.QtWidgets import ( + QApplication, QMainWindow, QGraphicsView, QGraphicsScene, + QGraphicsEllipseItem, QGraphicsPixmapItem, QPushButton, + QHBoxLayout, QVBoxLayout, QWidget, QFileDialog +) +from PyQt5.QtGui import QPixmap, QPen, QBrush +from PyQt5.QtCore import Qt, QRectF + + +class ImageGraphicsView(QGraphicsView): + """ + Custom class inheriting from QGraphicsView for displaying an image and placing red dots. + """ + def __init__(self, parent=None): + super().__init__(parent) + + # Create scene and add it to the view + self.scene = QGraphicsScene(self) + self.setScene(self.scene) + + # Zoom around mouse pointer + self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + + # Image item + self.image_item = QGraphicsPixmapItem() + self.scene.addItem(self.image_item) + + # Points and dot items + self.points = [] + self.point_items = [] + self.editor_mode = False + self.dot_radius = 4 + + # Enable built-in panning around image, but force arrow cursor initially + self.setDragMode(QGraphicsView.ScrollHandDrag) + self.viewport().setCursor(Qt.ArrowCursor) + + # Track clicking vs. dragging + self._mouse_pressed = False + self._press_view_pos = None + self._drag_threshold = 5 + self._was_dragging = False + + def load_image(self, image_path): + """Load an image and fit it in the view.""" + pixmap = QPixmap(image_path) + + if not pixmap.isNull(): + self.image_item.setPixmap(pixmap) + + # Avoid TypeError by converting to QRectF + self.setSceneRect(QRectF(pixmap.rect())) + + # Clear existing dots from previous image + self.points.clear() + self._clear_point_items() + + # Reset transform then fit image in view + self.resetTransform() + self.fitInView(self.image_item, Qt.KeepAspectRatio) + + def set_editor_mode(self, mode: bool): + """If True: place/remove dots; if False: do nothing on click.""" + self.editor_mode = mode + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self._mouse_pressed = True + self._was_dragging = False + self._press_view_pos = event.pos() + + # Switch to closed-hand cursor while left mouse is down + self.viewport().setCursor(Qt.ClosedHandCursor) + + elif event.button() == Qt.RightButton: + # If Editor Mode is on remove the nearest dot + if self.editor_mode: + self._remove_point(event.pos()) + + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """ + If movement > _drag_threshold: consider it a drag. + The actual panning is handled by QGraphicsView in ScrollHandDrag mode. + """ + # Check if the mouse is being dragged + if self._mouse_pressed and (event.buttons() & Qt.LeftButton): + # If the mouse moved more than the threshold, consider it a drag + dist = (event.pos() - self._press_view_pos).manhattanLength() + if dist > self._drag_threshold: + self._was_dragging = True + + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """ + After releasing the left button, go back to arrow cursor. + If it wasn't a drag, treat as a click (Editor Mode: add dot). + """ + # Let QGraphicsView handle release first + super().mouseReleaseEvent(event) + + if event.button() == Qt.LeftButton and self._mouse_pressed: + self._mouse_pressed = False + + # Always go back to arrow cursor AFTER letting QGraphicsView handle release + self.viewport().setCursor(Qt.ArrowCursor) + + if not self._was_dragging: + # It's a click: if editor mode is ON add a dot + if self.editor_mode: + self._add_point(event.pos()) + + self._was_dragging = False + + def wheelEvent(self, event): + """Mouse wheel = zoom.""" + zoom_in_factor = 1.25 + zoom_out_factor = 1 / zoom_in_factor + + if event.angleDelta().y() > 0: + self.scale(zoom_in_factor, zoom_in_factor) + else: + self.scale(zoom_out_factor, zoom_out_factor) + + event.accept() + + + # -------------- Red Dots -------------- + def _add_point(self, view_pos): + """Add a red dot at scene coords corresponding to view_pos.""" + scene_pos = self.mapToScene(view_pos) + x, y = scene_pos.x(), scene_pos.y() + + self.points.append((x, y)) + dot = self._create_dot_item(x, y) + self.point_items.append(dot) + self.scene.addItem(dot) + + def _remove_point(self, view_pos): + """Right-click: remove nearest dot if within threshold.""" + scene_pos = self.mapToScene(view_pos) + x_click, y_click = scene_pos.x(), scene_pos.y() + + # Define threshold for removing a point + threshold = 10 + closest_idx = None + min_dist = float('inf') + + # Find the closest point to the click + for i, (x, y) in enumerate(self.points): + dist_sq = (x - x_click)**2 + (y - y_click)**2 + if dist_sq < min_dist: + min_dist = dist_sq + closest_idx = i + + # Remove the closest point if it's within the threshold + if closest_idx is not None and min_dist <= threshold**2: + self.scene.removeItem(self.point_items[closest_idx]) + del self.point_items[closest_idx] + del self.points[closest_idx] + + def _create_dot_item(self, x, y): + """Helper for creating a small red ellipse item.""" + r = self.dot_radius + ellipse = QGraphicsEllipseItem(x - r, y - r, 2*r, 2*r) + ellipse.setBrush(QBrush(Qt.red)) + ellipse.setPen(QPen(Qt.red)) + return ellipse + + def _clear_point_items(self): + """Remove all dot items from the scene.""" + for item in self.point_items: + self.scene.removeItem(item) + self.point_items = [] + + +class MainWindow(QMainWindow): + """ + Main window with: + - Button to load in image + - Editor mode toggle button + - Button for exporting placed points + """ + def __init__(self): + super().__init__() + self.setWindowTitle("Test GUI") + + main_widget = QWidget() + main_layout = QVBoxLayout(main_widget) + + self.image_view = ImageGraphicsView() + main_layout.addWidget(self.image_view) + + btn_layout = QHBoxLayout() + + # Load Image + self.btn_load_image = QPushButton("Load Image") + self.btn_load_image.clicked.connect(self.load_image) + btn_layout.addWidget(self.btn_load_image) + + # Editor Mode + self.btn_editor_mode = QPushButton("Editor Mode: OFF") + self.btn_editor_mode.setCheckable(True) + self.btn_editor_mode.setStyleSheet("background-color: lightgray;") + self.btn_editor_mode.clicked.connect(self.toggle_editor_mode) + btn_layout.addWidget(self.btn_editor_mode) + + # Export Points + self.btn_export_points = QPushButton("Export Points") + self.btn_export_points.clicked.connect(self.export_points) + btn_layout.addWidget(self.btn_export_points) + + # Remove Points + self.btn_clear_points = QPushButton("Clear Points") + self.btn_clear_points.clicked.connect(self.clear_points) + btn_layout.addWidget(self.btn_clear_points) + + + main_layout.addLayout(btn_layout) + self.setCentralWidget(main_widget) + self.resize(900, 600) + + def load_image(self): + """Open a file dialog to pick an image then load it.""" + options = QFileDialog.Options() + file_path, _ = QFileDialog.getOpenFileName( + self, "Open Image", "", + "Images (*.png *.jpg *.jpeg *.bmp *.tif)", + options=options + ) + if file_path: + self.image_view.load_image(file_path) + + def toggle_editor_mode(self): + """Toggle whether left-click places dots and right-click removes dots.""" + is_checked = self.btn_editor_mode.isChecked() + self.image_view.set_editor_mode(is_checked) + + if is_checked: + self.btn_editor_mode.setText("Editor Mode: ON") + self.btn_editor_mode.setStyleSheet("background-color: #ffcccc;") + else: + self.btn_editor_mode.setText("Editor Mode: OFF") + self.btn_editor_mode.setStyleSheet("background-color: lightgray;") + + def export_points(self): + """Save the list of dot coords to a .npy file.""" + options = QFileDialog.Options() + file_path, _ = QFileDialog.getSaveFileName( + self, "Export Points", "", + "NumPy Files (*.npy);;All Files (*)", + options=options + ) + if file_path: + points_array = np.array(self.image_view.points) + np.save(file_path, points_array) + print(f"Exported {len(points_array)} points to {file_path}") + + + def clear_points(self): + """Remove all placed points (list & scene).""" + self.image_view.points.clear() + self.image_view._clear_point_items() + + + +def main(): + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec_()) + + +if __name__ == '__main__': + main() -- GitLab