Skip to content
Snippets Groups Projects
Commit 89813eea authored by Christian's avatar Christian
Browse files

Fixed bug allowing points outside of image and removed editor mode entirely

parent c33b8c2c
No related branches found
No related tags found
No related merge requests found
...@@ -69,6 +69,7 @@ class LabeledPointItem(QGraphicsEllipseItem): ...@@ -69,6 +69,7 @@ class LabeledPointItem(QGraphicsEllipseItem):
self._text_item.setPos(tx, ty) self._text_item.setPos(tx, ty)
def set_pos(self, x, y): def set_pos(self, x, y):
"""Positions the circle so that its center is at (x, y)."""
self._x = x self._x = x
self._y = y self._y = y
self.setPos(x - self._r, y - self._r) self.setPos(x - self._r, y - self._r)
...@@ -89,25 +90,24 @@ class ImageGraphicsView(QGraphicsView): ...@@ -89,25 +90,24 @@ class ImageGraphicsView(QGraphicsView):
self.scene = QGraphicsScene(self) self.scene = QGraphicsScene(self)
self.setScene(self.scene) self.setScene(self.scene)
# Allow zoom around mouse pointer # Zoom around mouse pointer
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
# Image display item # Image display item
self.image_item = QGraphicsPixmapItem() self.image_item = QGraphicsPixmapItem()
self.scene.addItem(self.image_item) self.scene.addItem(self.image_item)
# Parallel lists # Parallel lists: anchor_points + LabeledPointItem
self.anchor_points = [] # List[(x, y)] self.anchor_points = [] # List of (x, y)
self.point_items = [] # List[LabeledPointItem] self.point_items = [] # List of LabeledPointItem
self.editor_mode = False
self.dot_radius = 4 self.dot_radius = 4
self.path_radius = 1 self.path_radius = 1
self.radius_cost_image = 2 # cost-lowering radius self.radius_cost_image = 2 # cost-lowering radius
self._img_w = 0 self._img_w = 0
self._img_h = 0 self._img_h = 0
# For pan/drag # For panning/dragging
self.setDragMode(QGraphicsView.ScrollHandDrag) self.setDragMode(QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(Qt.ArrowCursor) self.viewport().setCursor(Qt.ArrowCursor)
...@@ -117,15 +117,13 @@ class ImageGraphicsView(QGraphicsView): ...@@ -117,15 +117,13 @@ class ImageGraphicsView(QGraphicsView):
self._was_dragging = False self._was_dragging = False
self._dragging_idx = None self._dragging_idx = None
self._drag_offset = (0, 0) self._drag_offset = (0, 0)
self._drag_counter = 0 # throttles path updates while dragging
# NEW: We'll count how many times we've updated the drag => partial path update # We will keep two copies of the cost image
self._drag_counter = 0
# Keep original cost image to revert changes
self.cost_image_original = None self.cost_image_original = None
self.cost_image = None self.cost_image = None
# The path is displayed as small magenta circles in self.full_path_points # Path circles displayed
self.full_path_points = [] self.full_path_points = []
# -------------------------------------------------------------------- # --------------------------------------------------------------------
...@@ -144,49 +142,57 @@ class ImageGraphicsView(QGraphicsView): ...@@ -144,49 +142,57 @@ class ImageGraphicsView(QGraphicsView):
self.resetTransform() self.resetTransform()
self.fitInView(self.image_item, Qt.KeepAspectRatio) self.fitInView(self.image_item, Qt.KeepAspectRatio)
# Create S/E # Place S/E at left and right
s_x, s_y = 0.15 * self._img_w, 0.5 * self._img_h s_x, s_y = 0.15 * self._img_w, 0.5 * self._img_h
e_x, e_y = 0.85 * self._img_w, 0.5 * self._img_h e_x, e_y = 0.85 * self._img_w, 0.5 * self._img_h
# S => not removable # Insert S/E
self._insert_anchor_point(-1, s_x, s_y, label="S", removable=False, z_val=100, radius=6) self._insert_anchor_point(-1, s_x, s_y, label="S", removable=False, z_val=100, radius=6)
# E => not removable
self._insert_anchor_point(-1, e_x, e_y, label="E", removable=False, z_val=100, radius=6) self._insert_anchor_point(-1, e_x, e_y, label="E", removable=False, z_val=100, radius=6)
def set_editor_mode(self, mode: bool):
self.editor_mode = mode
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# ANCHOR POINTS # ANCHOR POINTS
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _insert_anchor_point(self, idx, x, y, label="", removable=True, z_val=0, radius=4): def _insert_anchor_point(self, idx, x, y, label="", removable=True, z_val=0, radius=4):
""" """
Insert at index=idx, or -1 => append just before E if E exists. Insert at index=idx, or -1 => append just before E if E exists.
Clamps x,y so points can't go outside the image.
""" """
x_clamped = self._clamp(x, radius, self._img_w - radius)
y_clamped = self._clamp(y, radius, self._img_h - radius)
if idx < 0: if idx < 0:
# If we have at least 2 anchors, the last is E => insert before that # If we have at least 2 anchors, the last is E => insert before it
if len(self.anchor_points) >= 2: if len(self.anchor_points) >= 2:
idx = len(self.anchor_points) - 1 idx = len(self.anchor_points) - 1
else: else:
idx = len(self.anchor_points) idx = len(self.anchor_points)
self.anchor_points.insert(idx, (x, y)) self.anchor_points.insert(idx, (x_clamped, y_clamped))
color = Qt.green if label in ("S", "E") else Qt.red color = Qt.green if label in ("S", "E") else Qt.red
item = LabeledPointItem(x, y, label=label, radius=radius, color=color, item = LabeledPointItem(
removable=removable, z_value=z_val) x_clamped, y_clamped, label=label,
radius=radius, color=color, removable=removable, z_value=z_val
)
self.point_items.insert(idx, item) self.point_items.insert(idx, item)
self.scene.addItem(item) self.scene.addItem(item)
def _add_guide_point(self, x, y): def _add_guide_point(self, x, y):
""" """Called when user left-clicks an empty spot. Insert a red guide point, recalc path."""
User added a red guide point => lower cost, insert anchor, rebuild path. # clamp to image boundaries
""" x_clamped = self._clamp(x, self.dot_radius, self._img_w - self.dot_radius)
# 1) Revert cost y_clamped = self._clamp(y, self.dot_radius, self._img_h - self.dot_radius)
# 1) revert cost
self._revert_cost_to_original() self._revert_cost_to_original()
# 2) Insert new anchor (removable)
self._insert_anchor_point(-1, x, y, label="", removable=True, z_val=1, radius=self.dot_radius) # 2) Insert new anchor
# 3) Re-apply cost-lowering for all existing guide points self._insert_anchor_point(-1, x_clamped, y_clamped, label="", removable=True, z_val=1, radius=self.dot_radius)
# 3) Re-apply cost-lowering
self._apply_all_guide_points_to_cost() self._apply_all_guide_points_to_cost()
# 4) Rebuild path # 4) Rebuild path
self._rebuild_full_path() self._rebuild_full_path()
...@@ -194,12 +200,11 @@ class ImageGraphicsView(QGraphicsView): ...@@ -194,12 +200,11 @@ class ImageGraphicsView(QGraphicsView):
# COST IMAGE # COST IMAGE
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _revert_cost_to_original(self): def _revert_cost_to_original(self):
"""self.cost_image <- copy of self.cost_image_original"""
if self.cost_image_original is not None: if self.cost_image_original is not None:
self.cost_image = self.cost_image_original.copy() self.cost_image = self.cost_image_original.copy()
def _apply_all_guide_points_to_cost(self): def _apply_all_guide_points_to_cost(self):
"""Lower cost around every removable anchor.""" """Lower cost around every REMOVABLE anchor (the red ones)."""
if self.cost_image is None: if self.cost_image is None:
return return
for i, (ax, ay) in enumerate(self.anchor_points): for i, (ax, ay) in enumerate(self.anchor_points):
...@@ -207,7 +212,6 @@ class ImageGraphicsView(QGraphicsView): ...@@ -207,7 +212,6 @@ class ImageGraphicsView(QGraphicsView):
self._lower_cost_in_circle(ax, ay, self.radius_cost_image) self._lower_cost_in_circle(ax, ay, self.radius_cost_image)
def _lower_cost_in_circle(self, x_f, y_f, radius): def _lower_cost_in_circle(self, x_f, y_f, radius):
"""Set cost_image row,col in circle of radius -> global min."""
if self.cost_image is None: if self.cost_image is None:
return return
h, w = self.cost_image.shape h, w = self.cost_image.shape
...@@ -247,7 +251,7 @@ class ImageGraphicsView(QGraphicsView): ...@@ -247,7 +251,7 @@ class ImageGraphicsView(QGraphicsView):
if i == 0: if i == 0:
big_xy.extend(sub_xy) big_xy.extend(sub_xy)
else: else:
# avoid duplicating the point between subpaths # avoid duplicating the shared anchor
if len(sub_xy) > 1: if len(sub_xy) > 1:
big_xy.extend(sub_xy[1:]) big_xy.extend(sub_xy[1:])
...@@ -264,7 +268,7 @@ class ImageGraphicsView(QGraphicsView): ...@@ -264,7 +268,7 @@ class ImageGraphicsView(QGraphicsView):
self.full_path_points.append(path_item) self.full_path_points.append(path_item)
self.scene.addItem(path_item) self.scene.addItem(path_item)
# Ensure S/E stay on top # Ensure S/E remain on top
for p_item in self.point_items: for p_item in self.point_items:
if p_item._text_item: if p_item._text_item:
p_item.setZValue(100) p_item.setZValue(100)
...@@ -287,7 +291,7 @@ class ImageGraphicsView(QGraphicsView): ...@@ -287,7 +291,7 @@ class ImageGraphicsView(QGraphicsView):
return [(c, r) for (r, c) in path_rc] return [(c, r) for (r, c) in path_rc]
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# MOUSE EVENTS (dragging, adding, removing points) # MOUSE EVENTS
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def mousePressEvent(self, event): def mousePressEvent(self, event):
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
...@@ -295,12 +299,11 @@ class ImageGraphicsView(QGraphicsView): ...@@ -295,12 +299,11 @@ class ImageGraphicsView(QGraphicsView):
self._was_dragging = False self._was_dragging = False
self._press_view_pos = event.pos() self._press_view_pos = event.pos()
if self.editor_mode: # Check if user clicked near an existing anchor => drag
idx = self._find_item_near(event.pos(), 10) idx = self._find_item_near(event.pos(), threshold=10)
if idx is not None: if idx is not None:
# drag existing anchor # drag existing anchor
self._dragging_idx = idx self._dragging_idx = idx
# Reset drag counter
self._drag_counter = 0 self._drag_counter = 0
scene_pos = self.mapToScene(event.pos()) scene_pos = self.mapToScene(event.pos())
...@@ -310,15 +313,12 @@ class ImageGraphicsView(QGraphicsView): ...@@ -310,15 +313,12 @@ class ImageGraphicsView(QGraphicsView):
self.viewport().setCursor(Qt.ClosedHandCursor) self.viewport().setCursor(Qt.ClosedHandCursor)
return return
else: else:
# If no anchor near, user might be panning # If no anchor near => user is placing a new point
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(Qt.ClosedHandCursor)
else:
self.setDragMode(QGraphicsView.ScrollHandDrag) self.setDragMode(QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(Qt.ClosedHandCursor) self.viewport().setCursor(Qt.ClosedHandCursor)
elif event.button() == Qt.RightButton: elif event.button() == Qt.RightButton:
if self.editor_mode: # Right-click => remove point if it's removable
self._remove_point_by_click(event.pos()) self._remove_point_by_click(event.pos())
super().mousePressEvent(event) super().mousePressEvent(event)
...@@ -329,32 +329,30 @@ class ImageGraphicsView(QGraphicsView): ...@@ -329,32 +329,30 @@ class ImageGraphicsView(QGraphicsView):
scene_pos = self.mapToScene(event.pos()) scene_pos = self.mapToScene(event.pos())
x_new = scene_pos.x() - self._drag_offset[0] x_new = scene_pos.x() - self._drag_offset[0]
y_new = scene_pos.y() - self._drag_offset[1] y_new = scene_pos.y() - self._drag_offset[1]
# clamp so user can't drag outside
r = self.point_items[self._dragging_idx]._r r = self.point_items[self._dragging_idx]._r
x_clamped = self._clamp(x_new, r, self._img_w - r) x_clamped = self._clamp(x_new, r, self._img_w - r)
y_clamped = self._clamp(y_new, r, self._img_h - r) y_clamped = self._clamp(y_new, r, self._img_h - r)
self.point_items[self._dragging_idx].set_pos(x_clamped, y_clamped) self.point_items[self._dragging_idx].set_pos(x_clamped, y_clamped)
# THROTTLE: only recalc path every few frames
self._drag_counter += 1 self._drag_counter += 1
if self._drag_counter >= 4: if self._drag_counter >= 4:
# partial path update => revert cost, reapply, rebuild
self._drag_counter = 0 self._drag_counter = 0
# do partial path update:
# (We won't revert cost if you want the user to see the “final” cost-lowered path only at the end
# or you can do the entire revert+reapply if you like)
self._revert_cost_to_original() self._revert_cost_to_original()
self._apply_all_guide_points_to_cost() self._apply_all_guide_points_to_cost()
# update anchor_points
newX, newY = self.point_items[self._dragging_idx].get_pos()
self.anchor_points[self._dragging_idx] = (newX, newY)
self._rebuild_full_path()
# anchor_points updated
self.anchor_points[self._dragging_idx] = (x_clamped, y_clamped)
self._rebuild_full_path()
return return
else: else:
# if movement > threshold => pan
if self._mouse_pressed and (event.buttons() & Qt.LeftButton): if self._mouse_pressed and (event.buttons() & Qt.LeftButton):
dist = (event.pos() - self._press_view_pos).manhattanLength() dist = (event.pos() - self._press_view_pos).manhattanLength()
if dist > self._drag_threshold: if dist > self._drag_threshold:
self._was_dragging = True self._was_dragging = True
super().mouseMoveEvent(event) super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
...@@ -362,23 +360,23 @@ class ImageGraphicsView(QGraphicsView): ...@@ -362,23 +360,23 @@ class ImageGraphicsView(QGraphicsView):
if event.button() == Qt.LeftButton and self._mouse_pressed: if event.button() == Qt.LeftButton and self._mouse_pressed:
self._mouse_pressed = False self._mouse_pressed = False
self.viewport().setCursor(Qt.ArrowCursor) self.viewport().setCursor(Qt.ArrowCursor)
if self._dragging_idx is not None: if self._dragging_idx is not None:
# done dragging => final path update
idx = self._dragging_idx idx = self._dragging_idx
self._dragging_idx = None self._dragging_idx = None
self._drag_offset = (0, 0) self._drag_offset = (0, 0)
self.setDragMode(QGraphicsView.ScrollHandDrag) self.setDragMode(QGraphicsView.ScrollHandDrag)
# Final big update
newX, newY = self.point_items[idx].get_pos() newX, newY = self.point_items[idx].get_pos()
self.anchor_points[idx] = (newX, newY) self.anchor_points[idx] = (newX, newY)
self._revert_cost_to_original() self._revert_cost_to_original()
self._apply_all_guide_points_to_cost() self._apply_all_guide_points_to_cost()
self._rebuild_full_path() self._rebuild_full_path()
else: else:
if not self._was_dragging and self.editor_mode: # If not dragging => place a new guide point
# user clicked an empty spot => add a guide point if not self._was_dragging:
scene_pos = self.mapToScene(event.pos()) scene_pos = self.mapToScene(event.pos())
x, y = scene_pos.x(), scene_pos.y() x, y = scene_pos.x(), scene_pos.y()
self._add_guide_point(x, y) self._add_guide_point(x, y)
...@@ -389,16 +387,14 @@ class ImageGraphicsView(QGraphicsView): ...@@ -389,16 +387,14 @@ class ImageGraphicsView(QGraphicsView):
idx = self._find_item_near(view_pos, threshold=10) idx = self._find_item_near(view_pos, threshold=10)
if idx is None: if idx is None:
return return
# check if removable => skip S/E # skip if it's S/E
if not self.point_items[idx].is_removable(): if not self.point_items[idx].is_removable():
return # do nothing return
# remove anchor
self.scene.removeItem(self.point_items[idx]) self.scene.removeItem(self.point_items[idx])
self.point_items.pop(idx) self.point_items.pop(idx)
self.anchor_points.pop(idx) self.anchor_points.pop(idx)
# revert + re-apply cost, rebuild path
self._revert_cost_to_original() self._revert_cost_to_original()
self._apply_all_guide_points_to_cost() self._apply_all_guide_points_to_cost()
self._rebuild_full_path() self._rebuild_full_path()
...@@ -421,13 +417,10 @@ class ImageGraphicsView(QGraphicsView): ...@@ -421,13 +417,10 @@ class ImageGraphicsView(QGraphicsView):
# ZOOM # ZOOM
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def wheelEvent(self, event): def wheelEvent(self, event):
""" """Zoom in/out with mouse wheel."""
Zoom in/out with mouse wheel
"""
zoom_in_factor = 1.25 zoom_in_factor = 1.25
zoom_out_factor = 1 / zoom_in_factor zoom_out_factor = 1 / zoom_in_factor
# If the user scrolls upward => zoom in. Otherwise => zoom out.
if event.angleDelta().y() > 0: if event.angleDelta().y() > 0:
self.scale(zoom_in_factor, zoom_in_factor) self.scale(zoom_in_factor, zoom_in_factor)
else: else:
...@@ -451,10 +444,7 @@ class ImageGraphicsView(QGraphicsView): ...@@ -451,10 +444,7 @@ class ImageGraphicsView(QGraphicsView):
self.full_path_points.clear() self.full_path_points.clear()
def clear_guide_points(self): def clear_guide_points(self):
""" """Remove all removable (guide) anchors, keep S/E. Then rebuild."""
Removes all anchors that are 'removable' (guide points),
keeps S/E in place. Then reverts cost, re-applies, rebuilds path.
"""
i = 0 i = 0
while i < len(self.anchor_points): while i < len(self.anchor_points):
if self.point_items[i].is_removable(): if self.point_items[i].is_removable():
...@@ -484,22 +474,20 @@ class MainWindow(QMainWindow): ...@@ -484,22 +474,20 @@ class MainWindow(QMainWindow):
self.image_view = ImageGraphicsView() self.image_view = ImageGraphicsView()
main_layout.addWidget(self.image_view) main_layout.addWidget(self.image_view)
# Buttons layout
btn_layout = QHBoxLayout() btn_layout = QHBoxLayout()
# Load Image
self.btn_load_image = QPushButton("Load Image") self.btn_load_image = QPushButton("Load Image")
self.btn_load_image.clicked.connect(self.load_image) self.btn_load_image.clicked.connect(self.load_image)
btn_layout.addWidget(self.btn_load_image) btn_layout.addWidget(self.btn_load_image)
self.btn_editor_mode = QPushButton("Editor Mode: OFF") # Export Points
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)
self.btn_export_points = QPushButton("Export Points") self.btn_export_points = QPushButton("Export Points")
self.btn_export_points.clicked.connect(self.export_points) self.btn_export_points.clicked.connect(self.export_points)
btn_layout.addWidget(self.btn_export_points) btn_layout.addWidget(self.btn_export_points)
# Clear Points
self.btn_clear_points = QPushButton("Clear Points") self.btn_clear_points = QPushButton("Clear Points")
self.btn_clear_points.clicked.connect(self.clear_points) self.btn_clear_points.clicked.connect(self.clear_points)
btn_layout.addWidget(self.btn_clear_points) btn_layout.addWidget(self.btn_clear_points)
...@@ -509,6 +497,7 @@ class MainWindow(QMainWindow): ...@@ -509,6 +497,7 @@ class MainWindow(QMainWindow):
self.resize(900, 600) self.resize(900, 600)
def load_image(self): def load_image(self):
"""Open file dialog, load image, compute cost image, store in view."""
options = QFileDialog.Options() options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName( file_path, _ = QFileDialog.getOpenFileName(
self, "Open Image", "", self, "Open Image", "",
...@@ -521,16 +510,6 @@ class MainWindow(QMainWindow): ...@@ -521,16 +510,6 @@ class MainWindow(QMainWindow):
self.image_view.cost_image_original = cost_img self.image_view.cost_image_original = cost_img
self.image_view.cost_image = cost_img.copy() self.image_view.cost_image = cost_img.copy()
def toggle_editor_mode(self):
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): def export_points(self):
if not self.image_view.anchor_points: if not self.image_view.anchor_points:
print("No anchor points to export.") print("No anchor points to export.")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment