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