From dd79c02e5eabbde11192619a6e59ad9670925f38 Mon Sep 17 00:00:00 2001 From: Christian <s224389@dtu.dk> Date: Wed, 8 Jan 2025 16:10:18 +0100 Subject: [PATCH] Created a draft for the GUI --- GUI_draft.py | 267 ++++++++++++++++++++++++++++++++++++++++++++++++ GUI_draft.py.py | 267 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 534 insertions(+) create mode 100644 GUI_draft.py create mode 100644 GUI_draft.py.py diff --git a/GUI_draft.py b/GUI_draft.py new file mode 100644 index 0000000..966829f --- /dev/null +++ b/GUI_draft.py @@ -0,0 +1,267 @@ +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() diff --git a/GUI_draft.py.py b/GUI_draft.py.py new file mode 100644 index 0000000..966829f --- /dev/null +++ b/GUI_draft.py.py @@ -0,0 +1,267 @@ +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() -- GitLab