Skip to content
Snippets Groups Projects
Commit 0f455b57 authored by s224389's avatar s224389
Browse files

Added specific Start/End points. Replaced point class.

parent 7f9fa646
No related branches found
No related tags found
No related merge requests found
import sys
import math
import numpy as np
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QGraphicsView, QGraphicsScene,
QGraphicsEllipseItem, QGraphicsPixmapItem, QPushButton,
QHBoxLayout, QVBoxLayout, QWidget, QFileDialog
QHBoxLayout, QVBoxLayout, QWidget, QFileDialog, QGraphicsSimpleTextItem
)
from PyQt5.QtGui import QPixmap, QPen, QBrush
from PyQt5.QtCore import Qt, QRectF
import math
class PointItem(QGraphicsEllipseItem):
class LabeledPointItem(QGraphicsEllipseItem):
"""
Represents a single draggable point on the scene.
A point item with optional label (e.g. 'S' or 'E'), color, and a flag for removability.
"""
def __init__(self, x, y, radius=4, parent=None):
def __init__(self, x, y, label="", radius=4, color=Qt.red, removable=True, parent=None):
super().__init__(x - radius, y - radius, 2*radius, 2*radius, parent)
self._x = x
self._y = y
self._r = radius
self.setBrush(QBrush(Qt.red))
self.setPen(QPen(Qt.red))
self._removable = removable # If False, point cannot be removed by right-click
# Ellipse styling
pen = QPen(color)
brush = QBrush(color)
self.setPen(pen)
self.setBrush(brush)
# If we have a label, add a child text item
self._text_item = None
if label:
self._text_item = QGraphicsSimpleTextItem(label, self)
# So the text doesn't scale/rotate with zoom:
self._text_item.setFlag(QGraphicsSimpleTextItem.ItemIgnoresTransformations)
# Position label inside the ellipse
text_rect = self._text_item.boundingRect()
text_x = (self.rect().width() - text_rect.width()) * 0.5
text_y = (self.rect().height() - text_rect.height()) * 0.5
self._text_item.setPos(text_x, text_y)
def is_removable(self):
"""Return True if this point can be removed by user, False otherwise."""
return self._removable
def get_pos(self):
"""Return the (x, y) of this point in scene coords."""
......@@ -32,23 +52,29 @@ class PointItem(QGraphicsEllipseItem):
def set_pos(self, x, y):
"""
Move point to (x, y).
This also updates the ellipse rectangle so the visual dot moves.
Also update ellipse and label position accordingly.
"""
self._x = x
self._y = y
self.setRect(x - self._r, y - self._r, 2*self._r, 2*self._r)
if self._text_item:
# Recenter text
text_rect = self._text_item.boundingRect()
text_x = (self.rect().width() - text_rect.width()) * 0.5
text_y = (self.rect().height() - text_rect.height()) * 0.5
self._text_item.setPos(text_x, text_y)
def distance_to(self, x_other, y_other):
"""Euclidean distance from this point to arbitrary (x_other, y_other)."""
"""Euclidean distance from this point to (x_other, y_other)."""
dx = self._x - x_other
dy = self._y - y_other
return math.sqrt(dx*dx + dy*dy)
class ImageGraphicsView(QGraphicsView):
"""
Custom class inheriting from QGraphicsView for displaying an image and placing red dots.
Custom class for displaying an image and placing/dragging points.
"""
def __init__(self, parent=None):
super().__init__(parent)
......@@ -64,7 +90,7 @@ class ImageGraphicsView(QGraphicsView):
self.image_item = QGraphicsPixmapItem()
self.scene.addItem(self.image_item)
# Points and dot items
# Points
self.points = []
self.editor_mode = False
self.dot_radius = 4
......@@ -82,24 +108,33 @@ class ImageGraphicsView(QGraphicsView):
self._drag_offset = (0, 0)
def load_image(self, image_path):
"""Load an image and fit it in the view."""
"""Load an image, clear old points, add 'S'/'E' points, fit 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()
# Remove old points from scene
self._clear_point_items(remove_all=True)
# Reset transform then fit image in view
# Reset transform + fit
self.resetTransform()
self.fitInView(self.image_item, Qt.KeepAspectRatio)
# Add two special green labeled points
# Choose coordinates (50,50) and (150,150) or any suitable coords
s_point = LabeledPointItem(
50, 50, label="S", radius=8, color=Qt.green, removable=False
)
e_point = LabeledPointItem(
150, 150, label="E", radius=8, color=Qt.green, removable=False
)
self.points = [s_point, e_point]
self.scene.addItem(s_point)
self.scene.addItem(e_point)
def set_editor_mode(self, mode: bool):
"""If True: place/remove dots; if False: do nothing on click."""
"""If True: place/remove/drag dots; if False: do nothing on click."""
self.editor_mode = mode
def mousePressEvent(self, event):
......@@ -114,7 +149,6 @@ class ImageGraphicsView(QGraphicsView):
if idx is not None:
# Start dragging that point
self._dragging_idx = idx
# Compute offset so point doesn't jump if clicked off-center
scene_pos = self.mapToScene(event.pos())
px, py = self.points[idx].get_pos()
self._drag_offset = (scene_pos.x() - px, scene_pos.y() - py)
......@@ -124,33 +158,31 @@ class ImageGraphicsView(QGraphicsView):
self.viewport().setCursor(Qt.ClosedHandCursor)
return
else:
# Not near a point, so we do normal panning
# Not near a point => normal panning
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(Qt.ClosedHandCursor)
else:
# Editor mode is off => always do normal panning
# Editor mode off => normal panning
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(Qt.ClosedHandCursor)
elif event.button() == Qt.RightButton:
# If Editor Mode is on remove the nearest dot
# If Editor Mode is ON, try removing nearest dot
if self.editor_mode:
self._remove_point(event.pos())
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._dragging_idx is not None:
# Move that point to new coords
# Move the dragged point
scene_pos = self.mapToScene(event.pos())
x_new = scene_pos.x() - self._drag_offset[0]
y_new = scene_pos.y() - self._drag_offset[1]
self.points[self._dragging_idx].set_pos(x_new, y_new)
return # Skip QGraphicsView's panning logic
return
else:
# Old logic: if movement > threshold -> set _was_dragging = True
# If movement > threshold => treat as panning
if self._mouse_pressed and (event.buttons() & Qt.LeftButton):
dist = (event.pos() - self._press_view_pos).manhattanLength()
if dist > self._drag_threshold:
......@@ -158,7 +190,6 @@ class ImageGraphicsView(QGraphicsView):
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
......@@ -166,21 +197,20 @@ class ImageGraphicsView(QGraphicsView):
self._mouse_pressed = False
self.viewport().setCursor(Qt.ArrowCursor)
# If we were dragging a point, stop.
# If we were dragging a point, stop dragging
if self._dragging_idx is not None:
self._dragging_idx = None
self._drag_offset = (0, 0)
self.setDragMode(QGraphicsView.ScrollHandDrag)
else:
# We were NOT dragging a point => check if it was a click to add a new point
# Not dragging => maybe add a new point
if not self._was_dragging and self.editor_mode:
self._add_point(event.pos())
self._was_dragging = False
def wheelEvent(self, event):
"""Mouse wheel = zoom."""
"""Mouse wheel => zoom."""
zoom_in_factor = 1.25
zoom_out_factor = 1 / zoom_in_factor
......@@ -191,19 +221,25 @@ class ImageGraphicsView(QGraphicsView):
event.accept()
# ----------- Points -----------
# -------------- Red Dots --------------
def _add_point(self, view_pos):
"""Add a red dot at scene coords corresponding to view_pos."""
"""Add a removable red dot at the clicked location."""
scene_pos = self.mapToScene(view_pos)
x, y = scene_pos.x(), scene_pos.y()
dot = PointItem(x, y, radius=self.dot_radius)
dot = LabeledPointItem(
x, y,
label="", # no label
radius=self.dot_radius,
color=Qt.red,
removable=True
)
self.points.append(dot)
self.scene.addItem(dot)
def _remove_point(self, view_pos):
"""Right-click: remove nearest dot if within threshold."""
"""Right-click => remove nearest dot if within threshold, if it's removable."""
scene_pos = self.mapToScene(view_pos)
x_click, y_click = scene_pos.x(), scene_pos.y()
......@@ -217,26 +253,14 @@ class ImageGraphicsView(QGraphicsView):
min_dist = dist
closest_idx = i
# Remove if within threshold
# Remove if near enough and is_removable
if closest_idx is not None and min_dist <= threshold:
if self.points[closest_idx].is_removable():
self.scene.removeItem(self.points[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 p in self.points:
self.scene.removeItem(p)
self.points.clear()
def _find_point_near(self, view_pos, threshold=10):
"""Return idx of nearest point if within threshold, else None."""
scene_pos = self.mapToScene(view_pos)
x_click, y_click = scene_pos.x(), scene_pos.y()
......@@ -253,16 +277,34 @@ class ImageGraphicsView(QGraphicsView):
return closest_idx
return None
def _clear_point_items(self, remove_all=False):
"""
Remove points from the scene.
- If remove_all=True, remove *all* points (including S/E).
- Otherwise, remove only the removable (red) ones.
"""
if remove_all:
for p in self.points:
self.scene.removeItem(p)
self.points.clear()
else:
# Keep non-removable (like S/E)
still_needed = []
for p in self.points:
if p.is_removable():
self.scene.removeItem(p)
else:
still_needed.append(p)
self.points = still_needed
class MainWindow(QMainWindow):
"""
Main window with:
- Button to load in image
- Editor mode toggle button
- Button for exporting placed points
- Load Image
- Editor mode toggle
- Export points
- Clear Points
"""
def __init__(self):
super().__init__()
......@@ -293,18 +335,17 @@ class MainWindow(QMainWindow):
self.btn_export_points.clicked.connect(self.export_points)
btn_layout.addWidget(self.btn_export_points)
# Remove Points
# Clear 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."""
"""Open file dialog to pick an image, then load it."""
options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName(
self, "Open Image", "",
......@@ -315,7 +356,7 @@ class MainWindow(QMainWindow):
self.image_view.load_image(file_path)
def toggle_editor_mode(self):
"""Toggle whether left-click places dots and right-click removes dots."""
"""Toggle whether left-click places/drags dots and right-click removes dots."""
is_checked = self.btn_editor_mode.isChecked()
self.image_view.set_editor_mode(is_checked)
......@@ -327,7 +368,14 @@ class MainWindow(QMainWindow):
self.btn_editor_mode.setStyleSheet("background-color: lightgray;")
def export_points(self):
"""Save the list of dot coords to a .npy file."""
"""
Save the (x, y) of each point to a .npy file.
(Excludes label, color, etc. Just x,y.)
"""
if not self.image_view.points:
print("No points to export.")
return
options = QFileDialog.Options()
file_path, _ = QFileDialog.getSaveFileName(
self, "Export Points", "",
......@@ -335,17 +383,17 @@ class MainWindow(QMainWindow):
options=options
)
if file_path:
points_array = np.array(self.image_view.points)
coords = [p.get_pos() for p in self.image_view.points]
points_array = np.array(coords)
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()
"""
Remove all *removable* points from the scene;
keep S/E if they were added (non-removable).
"""
self.image_view._clear_point_items(remove_all=False)
def main():
app = QApplication(sys.argv)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment