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

Added export path feature

parent ac48bd9a
No related branches found
No related tags found
No related merge requests found
...@@ -69,7 +69,7 @@ class LabeledPointItem(QGraphicsEllipseItem): ...@@ -69,7 +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).""" """Positions the circle so 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)
...@@ -93,21 +93,24 @@ class ImageGraphicsView(QGraphicsView): ...@@ -93,21 +93,24 @@ class ImageGraphicsView(QGraphicsView):
# Zoom around mouse pointer # Zoom around mouse pointer
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
# Image display item # Image display
self.image_item = QGraphicsPixmapItem() self.image_item = QGraphicsPixmapItem()
self.scene.addItem(self.image_item) self.scene.addItem(self.image_item)
# Parallel lists: anchor_points + LabeledPointItem self.anchor_points = [] # List[(x, y)]
self.anchor_points = [] # List of (x, y) self.point_items = [] # LabeledPointItem objects
self.point_items = [] # List of LabeledPointItem self.full_path_points = [] # QGraphicsEllipseItems for the path
# We'll store the entire path coords (smoothed) for reference
self._full_path_xy = []
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
self._img_w = 0 self._img_w = 0
self._img_h = 0 self._img_h = 0
# For panning/dragging # Pan/Drag
self.setDragMode(QGraphicsView.ScrollHandDrag) self.setDragMode(QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(Qt.ArrowCursor) self.viewport().setCursor(Qt.ArrowCursor)
...@@ -117,15 +120,12 @@ class ImageGraphicsView(QGraphicsView): ...@@ -117,15 +120,12 @@ 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 self._drag_counter = 0
# We will keep two copies of the cost image # Cost images
self.cost_image_original = None self.cost_image_original = None
self.cost_image = None self.cost_image = None
# Path circles displayed
self.full_path_points = []
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# LOADING # LOADING
# -------------------------------------------------------------------- # --------------------------------------------------------------------
...@@ -142,11 +142,9 @@ class ImageGraphicsView(QGraphicsView): ...@@ -142,11 +142,9 @@ class ImageGraphicsView(QGraphicsView):
self.resetTransform() self.resetTransform()
self.fitInView(self.image_item, Qt.KeepAspectRatio) self.fitInView(self.image_item, Qt.KeepAspectRatio)
# Place S/E at left and right # By default, add S/E
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
# 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)
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)
...@@ -154,15 +152,11 @@ class ImageGraphicsView(QGraphicsView): ...@@ -154,15 +152,11 @@ class ImageGraphicsView(QGraphicsView):
# 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 anchor at index=idx (or -1 => before E). Clamps x,y to image bounds."""
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) x_clamped = self._clamp(x, radius, self._img_w - radius)
y_clamped = self._clamp(y, radius, self._img_h - 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 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:
...@@ -171,31 +165,124 @@ class ImageGraphicsView(QGraphicsView): ...@@ -171,31 +165,124 @@ class ImageGraphicsView(QGraphicsView):
self.anchor_points.insert(idx, (x_clamped, y_clamped)) 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( item = LabeledPointItem(x_clamped, y_clamped,
x_clamped, y_clamped, label=label, label=label, radius=radius, color=color,
radius=radius, color=color, removable=removable, z_value=z_val 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 clicked => find the correct sub-path, insert the point in that sub-path."""
# clamp to image boundaries
x_clamped = self._clamp(x, self.dot_radius, self._img_w - self.dot_radius) x_clamped = self._clamp(x, self.dot_radius, self._img_w - self.dot_radius)
y_clamped = self._clamp(y, self.dot_radius, self._img_h - self.dot_radius) 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 if not self._full_path_xy:
self._insert_anchor_point(-1, x_clamped, y_clamped, label="", removable=True, z_val=1, radius=self.dot_radius) # If there's no existing path built, we just insert normally
self._insert_anchor_point(-1, x_clamped, y_clamped,
label="", removable=True, z_val=1, radius=self.dot_radius)
else:
# Insert the new anchor in between the correct anchors,
# by finding nearest coordinate in _full_path_xy, then
# walking left+right until we find bounding anchors.
self._insert_anchor_between_subpath(x_clamped, y_clamped)
# 3) Re-apply cost-lowering
self._apply_all_guide_points_to_cost() self._apply_all_guide_points_to_cost()
# 4) Rebuild path
self._rebuild_full_path() self._rebuild_full_path()
def _insert_anchor_between_subpath(self, x_new, y_new):
"""Find the subpath bounding (x_new,y_new) and insert the new anchor accordingly."""
# If no path, fallback
if not self._full_path_xy:
self._insert_anchor_point(-1, x_new, y_new)
return
# 1) Find nearest coordinate in the path
best_idx = None
best_d2 = float('inf')
for i, (px, py) in enumerate(self._full_path_xy):
dx = px - x_new
dy = py - y_new
d2 = dx*dx + dy*dy
if d2 < best_d2:
best_d2 = d2
best_idx = i
if best_idx is None:
# fallback
self._insert_anchor_point(-1, x_new, y_new)
return
# 2) Identify bounding anchors by walking left / right
def approx_equal(xa, ya, xb, yb, tol=1e-3):
return (abs(xa - xb) < tol) and (abs(ya - yb) < tol)
def is_anchor(coord):
cx, cy = coord
# check if (cx,cy) is approx any anchor in self.anchor_points
for (ax, ay) in self.anchor_points:
if approx_equal(ax, ay, cx, cy):
return True
return False
left_anchor_pt = None
right_anchor_pt = None
# walk left
iL = best_idx
while iL >= 0:
px, py = self._full_path_xy[iL]
if is_anchor((px, py)):
left_anchor_pt = (px, py)
break
iL -= 1
# walk right
iR = best_idx
while iR < len(self._full_path_xy):
px, py = self._full_path_xy[iR]
if is_anchor((px, py)):
right_anchor_pt = (px, py)
break
iR += 1
if not left_anchor_pt or not right_anchor_pt:
# If we can't find bounding anchors => fallback
self._insert_anchor_point(-1, x_new, y_new)
return
# 3) Find which anchor_points indices correspond to left_anchor_pt, right_anchor_pt
left_idx = None
right_idx = None
for i, (ax, ay) in enumerate(self.anchor_points):
if approx_equal(ax, ay, left_anchor_pt[0], left_anchor_pt[1]):
left_idx = i
if approx_equal(ax, ay, right_anchor_pt[0], right_anchor_pt[1]):
right_idx = i
if left_idx is None or right_idx is None:
# fallback
self._insert_anchor_point(-1, x_new, y_new)
return
# We want the new anchor to be inserted right after left_idx,
# so that the subpath between left_idx and right_idx
# is effectively subdivided.
# This ensures anchor_points = [..., left_anchor, new_point, ..., right_anchor, ...]
insert_idx = right_idx
# But if left_idx < right_idx => we do insert_idx=left_idx+1
# in case we want them consecutive.
if left_idx < right_idx:
insert_idx = left_idx + 1
else:
# means the path might be reversed, or there's some tricky indexing
# We'll just do min or max
insert_idx = max(right_idx, left_idx)
self._insert_anchor_point(insert_idx, x_new, y_new, label="", removable=True,
z_val=1, radius=self.dot_radius)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# COST IMAGE # COST IMAGE
# -------------------------------------------------------------------- # --------------------------------------------------------------------
...@@ -204,7 +291,6 @@ class ImageGraphicsView(QGraphicsView): ...@@ -204,7 +291,6 @@ class ImageGraphicsView(QGraphicsView):
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 (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):
...@@ -234,12 +320,13 @@ class ImageGraphicsView(QGraphicsView): ...@@ -234,12 +320,13 @@ class ImageGraphicsView(QGraphicsView):
# PATH BUILDING # PATH BUILDING
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _rebuild_full_path(self): def _rebuild_full_path(self):
# Remove old path items """Compute subpaths between anchors, smooth them, store all coords, display all of them."""
# Clear old path visuals
for item in self.full_path_points: for item in self.full_path_points:
self.scene.removeItem(item) self.scene.removeItem(item)
self.full_path_points.clear() self.full_path_points.clear()
self._full_path_xy.clear()
# Build subpaths
if len(self.anchor_points) < 2 or self.cost_image is None: if len(self.anchor_points) < 2 or self.cost_image is None:
return return
...@@ -251,29 +338,33 @@ class ImageGraphicsView(QGraphicsView): ...@@ -251,29 +338,33 @@ class ImageGraphicsView(QGraphicsView):
if i == 0: if i == 0:
big_xy.extend(sub_xy) big_xy.extend(sub_xy)
else: else:
# avoid duplicating the shared anchor # avoid repeating 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:])
# Smoothing with Savitzky-Golay # Smooth if we have enough points
if len(big_xy) >= 7: if len(big_xy) >= 7:
arr_xy = np.array(big_xy) # shape (N,2) arr_xy = np.array(big_xy)
smoothed = savgol_filter(arr_xy, window_length=7, polyorder=1, axis=0) smoothed = savgol_filter(arr_xy, window_length=7, polyorder=1, axis=0)
big_xy = smoothed.tolist() big_xy = smoothed.tolist()
# Draw them # We now store the entire path in _full_path_xy
self._full_path_xy = big_xy[:]
# Display ALL points
for (px, py) in big_xy: for (px, py) in big_xy:
path_item = LabeledPointItem(px, py, label="", radius=self.path_radius, path_item = LabeledPointItem(px, py, label="", radius=self.path_radius,
color=Qt.magenta, removable=False, z_value=0) color=Qt.magenta, removable=False, z_value=0)
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 remain on top # Keep S/E 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)
def _compute_subpath_xy(self, xA, yA, xB, yB): def _compute_subpath_xy(self, xA, yA, xB, yB):
"""Return the raw path from (xA,yA)->(xB,yB)."""
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
...@@ -299,10 +390,9 @@ class ImageGraphicsView(QGraphicsView): ...@@ -299,10 +390,9 @@ class ImageGraphicsView(QGraphicsView):
self._was_dragging = False self._was_dragging = False
self._press_view_pos = event.pos() self._press_view_pos = event.pos()
# Check if user clicked near an existing anchor => drag # See if user is clicking near an existing anchor => drag it
idx = self._find_item_near(event.pos(), threshold=10) idx = self._find_item_near(event.pos(), threshold=10)
if idx is not None: if idx is not None:
# drag existing anchor
self._dragging_idx = idx self._dragging_idx = idx
self._drag_counter = 0 self._drag_counter = 0
...@@ -313,19 +403,19 @@ class ImageGraphicsView(QGraphicsView): ...@@ -313,19 +403,19 @@ class ImageGraphicsView(QGraphicsView):
self.viewport().setCursor(Qt.ClosedHandCursor) self.viewport().setCursor(Qt.ClosedHandCursor)
return return
else: else:
# If no anchor near => user is placing a new point # no anchor => we'll add a new point
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:
# Right-click => remove point if it's removable # Right-click => remove anchor if removable
self._remove_point_by_click(event.pos()) self._remove_point_by_click(event.pos())
super().mousePressEvent(event) super().mousePressEvent(event)
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
if self._dragging_idx is not None: if self._dragging_idx is not None:
# dragging an anchor # Dragging anchor
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]
...@@ -338,15 +428,13 @@ class ImageGraphicsView(QGraphicsView): ...@@ -338,15 +428,13 @@ class ImageGraphicsView(QGraphicsView):
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 # partial path update
self._drag_counter = 0 self._drag_counter = 0
self._revert_cost_to_original() self._revert_cost_to_original()
self._apply_all_guide_points_to_cost() self._apply_all_guide_points_to_cost()
# anchor_points updated
self.anchor_points[self._dragging_idx] = (x_clamped, y_clamped) self.anchor_points[self._dragging_idx] = (x_clamped, y_clamped)
self._rebuild_full_path() self._rebuild_full_path()
return
else: else:
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()
...@@ -362,7 +450,7 @@ class ImageGraphicsView(QGraphicsView): ...@@ -362,7 +450,7 @@ class ImageGraphicsView(QGraphicsView):
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 # finished dragging => final 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)
...@@ -374,8 +462,9 @@ class ImageGraphicsView(QGraphicsView): ...@@ -374,8 +462,9 @@ class ImageGraphicsView(QGraphicsView):
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 dragging => place a new guide point # If user wasn't dragging => add new guide point
if not self._was_dragging: 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()
...@@ -387,7 +476,7 @@ class ImageGraphicsView(QGraphicsView): ...@@ -387,7 +476,7 @@ 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
# skip if it's S/E # skip if S/E
if not self.point_items[idx].is_removable(): if not self.point_items[idx].is_removable():
return return
...@@ -402,8 +491,9 @@ class ImageGraphicsView(QGraphicsView): ...@@ -402,8 +491,9 @@ class ImageGraphicsView(QGraphicsView):
def _find_item_near(self, view_pos, threshold=10): def _find_item_near(self, view_pos, threshold=10):
scene_pos = self.mapToScene(view_pos) scene_pos = self.mapToScene(view_pos)
x_click, y_click = scene_pos.x(), scene_pos.y() x_click, y_click = scene_pos.x(), scene_pos.y()
min_dist = float('inf')
closest_idx = None closest_idx = None
min_dist = float('inf')
for i, itm in enumerate(self.point_items): for i, itm in enumerate(self.point_items):
d = itm.distance_to(x_click, y_click) d = itm.distance_to(x_click, y_click)
if d < min_dist: if d < min_dist:
...@@ -417,10 +507,8 @@ class ImageGraphicsView(QGraphicsView): ...@@ -417,10 +507,8 @@ class ImageGraphicsView(QGraphicsView):
# ZOOM # ZOOM
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def wheelEvent(self, event): def wheelEvent(self, event):
"""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 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:
...@@ -442,9 +530,10 @@ class ImageGraphicsView(QGraphicsView): ...@@ -442,9 +530,10 @@ class ImageGraphicsView(QGraphicsView):
for p in self.full_path_points: for p in self.full_path_points:
self.scene.removeItem(p) self.scene.removeItem(p)
self.full_path_points.clear() self.full_path_points.clear()
self._full_path_xy.clear()
def clear_guide_points(self): def clear_guide_points(self):
"""Remove all removable (guide) anchors, keep S/E. Then rebuild.""" """Remove all removable anchors, keep S/E. Rebuild 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():
...@@ -454,14 +543,19 @@ class ImageGraphicsView(QGraphicsView): ...@@ -454,14 +543,19 @@ class ImageGraphicsView(QGraphicsView):
else: else:
i += 1 i += 1
for item in self.full_path_points: for it in self.full_path_points:
self.scene.removeItem(item) self.scene.removeItem(it)
self.full_path_points.clear() self.full_path_points.clear()
self._full_path_xy.clear()
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()
def get_full_path_xy(self):
"""Return the entire path (x,y) array after smoothing."""
return self._full_path_xy
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
...@@ -482,10 +576,10 @@ class MainWindow(QMainWindow): ...@@ -482,10 +576,10 @@ class MainWindow(QMainWindow):
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)
# Export Points # Export Path
self.btn_export_points = QPushButton("Export Points") self.btn_export_path = QPushButton("Export Path")
self.btn_export_points.clicked.connect(self.export_points) self.btn_export_path.clicked.connect(self.export_path)
btn_layout.addWidget(self.btn_export_points) btn_layout.addWidget(self.btn_export_path)
# Clear Points # Clear Points
self.btn_clear_points = QPushButton("Clear Points") self.btn_clear_points = QPushButton("Clear Points")
...@@ -497,7 +591,6 @@ class MainWindow(QMainWindow): ...@@ -497,7 +591,6 @@ 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", "",
...@@ -510,23 +603,25 @@ class MainWindow(QMainWindow): ...@@ -510,23 +603,25 @@ 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 export_points(self): def export_path(self):
if not self.image_view.anchor_points: """Export the full path (x,y) as a .npy file."""
print("No anchor points to export.") full_xy = self.image_view.get_full_path_xy()
if not full_xy:
print("No path to export.")
return return
options = QFileDialog.Options() options = QFileDialog.Options()
file_path, _ = QFileDialog.getSaveFileName( file_path, _ = QFileDialog.getSaveFileName(
self, "Export Points", "", self, "Export Path", "",
"NumPy Files (*.npy);;All Files (*)", "NumPy Files (*.npy);;All Files (*)",
options=options options=options
) )
if file_path: if file_path:
points_array = np.array(self.image_view.anchor_points) arr = np.array(full_xy)
np.save(file_path, points_array) np.save(file_path, arr)
print(f"Exported {len(points_array)} points to {file_path}") print(f"Exported path with {len(arr)} points to {file_path}")
def clear_points(self): def clear_points(self):
"""Remove all removable anchors (guide points), keep S/E in place."""
self.image_view.clear_guide_points() self.image_view.clear_guide_points()
def closeEvent(self, event): def closeEvent(self, event):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment