Skip to content
Snippets Groups Projects
Commit bda90527 authored by s224362's avatar s224362
Browse files

trying to pull data

parents d5fd2917 fe1915fe
No related branches found
No related tags found
No related merge requests found
import sys
import math
import csv # <-- Added
import numpy as np
# For smoothing the path
......@@ -8,14 +9,242 @@ from scipy.signal import savgol_filter
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QGraphicsView, QGraphicsScene,
QGraphicsEllipseItem, QGraphicsPixmapItem, QPushButton,
QHBoxLayout, QVBoxLayout, QWidget, QFileDialog, QGraphicsTextItem
QHBoxLayout, QVBoxLayout, QWidget, QFileDialog, QGraphicsTextItem,
QSlider, QLabel, QCheckBox, QGridLayout, QSizePolicy
)
from PyQt5.QtGui import QPixmap, QPen, QBrush, QColor, QFont
from PyQt5.QtCore import Qt, QRectF
from PyQt5.QtGui import QPixmap, QPen, QBrush, QColor, QFont, QImage
from PyQt5.QtCore import Qt, QRectF, QSize
# live_wire.py must contain something like:
# from skimage import exposure
# from skimage.filters import gaussian
# def preprocess_image(image, sigma=3, clip_limit=0.01): ...
# def compute_cost_image(path, user_radius, sigma=3, clip_limit=0.01): ...
# def find_path(cost_image, points): ...
# ...
from live_wire import compute_cost_image, find_path, preprocess_image
# ------------------------------------------------------------------------
# A pan & zoom QGraphicsView
# ------------------------------------------------------------------------
class PanZoomGraphicsView(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setDragMode(QGraphicsView.NoDrag) # We'll handle panning manually
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self._panning = False
self._pan_start = None
# Let it expand in layouts
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def wheelEvent(self, event):
""" Zoom in/out with mouse wheel. """
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()
def mousePressEvent(self, event):
""" If left button: Start panning (unless overridden). """
if event.button() == Qt.LeftButton:
self._panning = True
self._pan_start = event.pos()
self.setCursor(Qt.ClosedHandCursor)
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
""" If panning, translate the scene. """
if self._panning and self._pan_start is not None:
delta = event.pos() - self._pan_start
self._pan_start = event.pos()
self.translate(delta.x(), delta.y())
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
""" End panning. """
if event.button() == Qt.LeftButton:
self._panning = False
self.setCursor(Qt.ArrowCursor)
super().mouseReleaseEvent(event)
# ------------------------------------------------------------------------
# A specialized PanZoomGraphicsView for the circle editor
# ------------------------------------------------------------------------
class CircleEditorGraphicsView(PanZoomGraphicsView):
def __init__(self, circle_editor_widget, parent=None):
super().__init__(parent)
self._circle_editor_widget = circle_editor_widget
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# Check if user clicked on the circle item
clicked_item = self.itemAt(event.pos())
if clicked_item is not None:
# climb up parent chain
it = clicked_item
while it is not None and not hasattr(it, "boundingRect"):
it = it.parentItem()
if isinstance(it, DraggableCircleItem):
# Let normal item-dragging occur, no pan
return QGraphicsView.mousePressEvent(self, event)
super().mousePressEvent(event)
def wheelEvent(self, event):
"""
If the mouse is hovering over the circle, we adjust the circle's radius
instead of zooming the image.
"""
pos_in_widget = event.pos()
item_under = self.itemAt(pos_in_widget)
if item_under is not None:
it = item_under
while it is not None and not hasattr(it, "boundingRect"):
it = it.parentItem()
if isinstance(it, DraggableCircleItem):
delta = event.angleDelta().y()
step = 1 if delta > 0 else -1
old_r = it.radius()
new_r = max(1, old_r + step)
it.set_radius(new_r)
self._circle_editor_widget.update_slider_value(new_r)
event.accept()
return
super().wheelEvent(event)
from live_wire import compute_cost_image, find_path
# ------------------------------------------------------------------------
# Draggable circle item (centered at (x, y) with radius)
# ------------------------------------------------------------------------
class DraggableCircleItem(QGraphicsEllipseItem):
def __init__(self, x, y, radius=20, color=Qt.red, parent=None):
super().__init__(0, 0, 2*radius, 2*radius, parent)
self._r = radius
pen = QPen(color)
brush = QBrush(color)
self.setPen(pen)
self.setBrush(brush)
# Enable item-based dragging
self.setFlags(QGraphicsEllipseItem.ItemIsMovable |
QGraphicsEllipseItem.ItemIsSelectable |
QGraphicsEllipseItem.ItemSendsScenePositionChanges)
# Position so that (x, y) is the center
self.setPos(x - radius, y - radius)
def set_radius(self, r):
old_center = self.sceneBoundingRect().center()
self._r = r
self.setRect(0, 0, 2*r, 2*r)
new_center = self.sceneBoundingRect().center()
diff_x = old_center.x() - new_center.x()
diff_y = old_center.y() - new_center.y()
self.moveBy(diff_x, diff_y)
def radius(self):
return self._r
# ------------------------------------------------------------------------
# Circle editor widget with slider + done
# ------------------------------------------------------------------------
class CircleEditorWidget(QWidget):
def __init__(self, pixmap, init_radius=20, done_callback=None, parent=None):
super().__init__(parent)
self._pixmap = pixmap
self._done_callback = done_callback
self._init_radius = init_radius
layout = QVBoxLayout(self)
self.setLayout(layout)
#
# 1) ADD A CENTERED LABEL ABOVE THE IMAGE, WITH BIGGER FONT
#
label_instructions = QLabel("Scale the dot to be of the size of your ridge")
label_instructions.setAlignment(Qt.AlignCenter)
big_font = QFont("Arial", 20)
big_font.setBold(True)
label_instructions.setFont(big_font)
layout.addWidget(label_instructions)
#
# 2) THE SPECIALIZED GRAPHICS VIEW THAT SHOWS THE IMAGE
#
self._graphics_view = CircleEditorGraphicsView(circle_editor_widget=self)
self._scene = QGraphicsScene(self)
self._graphics_view.setScene(self._scene)
layout.addWidget(self._graphics_view)
# Show the image
self._image_item = QGraphicsPixmapItem(self._pixmap)
self._scene.addItem(self._image_item)
# Put circle in center
cx = self._pixmap.width() / 2
cy = self._pixmap.height() / 2
self._circle_item = DraggableCircleItem(cx, cy, radius=self._init_radius, color=Qt.red)
self._scene.addItem(self._circle_item)
# Fit in view
self._graphics_view.setSceneRect(QRectF(self._pixmap.rect()))
self._graphics_view.fitInView(self._image_item, Qt.KeepAspectRatio)
#
# 3) CONTROLS BELOW
#
bottom_layout = QHBoxLayout()
layout.addLayout(bottom_layout)
# label + slider
self._lbl_size = QLabel(f"size ({self._init_radius})")
bottom_layout.addWidget(self._lbl_size)
self._slider = QSlider(Qt.Horizontal)
self._slider.setRange(1, 200)
self._slider.setValue(self._init_radius)
bottom_layout.addWidget(self._slider)
# done button
self._btn_done = QPushButton("Done")
bottom_layout.addWidget(self._btn_done)
# Connect signals
self._slider.valueChanged.connect(self._on_slider_changed)
self._btn_done.clicked.connect(self._on_done_clicked)
def _on_slider_changed(self, value):
self._circle_item.set_radius(value)
self._lbl_size.setText(f"size ({value})")
def _on_done_clicked(self):
final_radius = self._circle_item.radius()
if self._done_callback is not None:
self._done_callback(final_radius)
def update_slider_value(self, new_radius):
self._slider.blockSignals(True)
self._slider.setValue(new_radius)
self._slider.blockSignals(False)
self._lbl_size.setText(f"size ({new_radius})")
def sizeHint(self):
return QSize(800, 600)
# ------------------------------------------------------------------------
# Labeled point item
# ------------------------------------------------------------------------
class LabeledPointItem(QGraphicsEllipseItem):
def __init__(self, x, y, label="", radius=4, color=Qt.red, removable=True, z_value=0, parent=None):
super().__init__(0, 0, 2*radius, 2*radius, parent)
......@@ -84,25 +313,23 @@ class LabeledPointItem(QGraphicsEllipseItem):
return self._removable
class ImageGraphicsView(QGraphicsView):
# ------------------------------------------------------------------------
# The original ImageGraphicsView with pan & zoom
# ------------------------------------------------------------------------
class ImageGraphicsView(PanZoomGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
# Zoom around mouse pointer
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
# Image display
self.image_item = QGraphicsPixmapItem()
self.scene.addItem(self.image_item)
self.anchor_points = [] # List[(x, y)]
self.point_items = [] # LabeledPointItem objects
self.full_path_points = [] # QGraphicsEllipseItems for the path
# We'll store the entire path coords (smoothed) for reference
self._full_path_xy = []
self.point_items = [] # LabeledPointItem
self.full_path_points = [] # QGraphicsEllipseItems for path
self._full_path_xy = [] # entire path coords (smoothed)
self.dot_radius = 4
self.path_radius = 1
......@@ -110,10 +337,6 @@ class ImageGraphicsView(QGraphicsView):
self._img_w = 0
self._img_h = 0
# Pan/Drag
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(Qt.ArrowCursor)
self._mouse_pressed = False
self._press_view_pos = None
self._drag_threshold = 5
......@@ -126,6 +349,29 @@ class ImageGraphicsView(QGraphicsView):
self.cost_image_original = None
self.cost_image = None
# Rainbow toggle => start with OFF
self._rainbow_enabled = False
# Smoothing parameters
self._savgol_window_length = 7
def set_rainbow_enabled(self, enabled: bool):
self._rainbow_enabled = enabled
self._rebuild_full_path()
def toggle_rainbow(self):
self._rainbow_enabled = not self._rainbow_enabled
self._rebuild_full_path()
def set_savgol_window_length(self, wlen: int):
if wlen < 3:
wlen = 3
if wlen % 2 == 0:
wlen += 1
self._savgol_window_length = wlen
self._rebuild_full_path()
# --------------------------------------------------------------------
# LOADING
# --------------------------------------------------------------------
......@@ -152,18 +398,17 @@ class ImageGraphicsView(QGraphicsView):
# ANCHOR POINTS
# --------------------------------------------------------------------
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."""
x_clamped = self._clamp(x, radius, self._img_w - radius)
y_clamped = self._clamp(y, radius, self._img_h - radius)
if idx < 0:
# Insert before E if there's at least 2 anchors
if len(self.anchor_points) >= 2:
idx = len(self.anchor_points) - 1
else:
idx = len(self.anchor_points)
self.anchor_points.insert(idx, (x_clamped, y_clamped))
color = Qt.green if label in ("S", "E") else Qt.red
item = LabeledPointItem(x_clamped, y_clamped,
label=label, radius=radius, color=color,
......@@ -172,33 +417,28 @@ class ImageGraphicsView(QGraphicsView):
self.scene.addItem(item)
def _add_guide_point(self, x, y):
"""User clicked => find the correct sub-path, insert the point in that sub-path."""
# Ensure we clamp properly
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)
self._revert_cost_to_original()
if not self._full_path_xy:
# 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)
self._apply_all_guide_points_to_cost()
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 somehow we have no path yet
if not self._full_path_xy:
self._insert_anchor_point(-1, x_new, y_new)
return
# 1) Find nearest coordinate in the path
# Find nearest point in the current full path
best_idx = None
best_d2 = float('inf')
for i, (px, py) in enumerate(self._full_path_xy):
......@@ -210,26 +450,21 @@ class ImageGraphicsView(QGraphicsView):
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
# Walk left
left_anchor_pt = None
right_anchor_pt = None
# walk left
iL = best_idx
while iL >= 0:
px, py = self._full_path_xy[iL]
......@@ -238,7 +473,8 @@ class ImageGraphicsView(QGraphicsView):
break
iL -= 1
# walk right
# Walk right
right_anchor_pt = None
iR = best_idx
while iR < len(self._full_path_xy):
px, py = self._full_path_xy[iR]
......@@ -247,12 +483,16 @@ class ImageGraphicsView(QGraphicsView):
break
iR += 1
# If we can't find distinct anchors on left & right,
# just insert before E.
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
if left_anchor_pt == right_anchor_pt:
self._insert_anchor_point(-1, x_new, y_new)
return
# 3) Find which anchor_points indices correspond to left_anchor_pt, right_anchor_pt
# Convert anchor coords -> anchor_points indices
left_idx = None
right_idx = None
for i, (ax, ay) in enumerate(self.anchor_points):
......@@ -262,23 +502,14 @@ class ImageGraphicsView(QGraphicsView):
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.
# Insert between them
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)
insert_idx = right_idx + 1
self._insert_anchor_point(insert_idx, x_new, y_new, label="", removable=True,
z_val=1, radius=self.dot_radius)
......@@ -320,8 +551,6 @@ class ImageGraphicsView(QGraphicsView):
# PATH BUILDING
# --------------------------------------------------------------------
def _rebuild_full_path(self):
"""Compute subpaths between anchors, smooth them, store all coords, display all of them."""
# Clear old path visuals
for item in self.full_path_points:
self.scene.removeItem(item)
self.full_path_points.clear()
......@@ -338,33 +567,42 @@ class ImageGraphicsView(QGraphicsView):
if i == 0:
big_xy.extend(sub_xy)
else:
# avoid repeating the shared anchor
if len(sub_xy) > 1:
big_xy.extend(sub_xy[1:])
# Smooth if we have enough points
if len(big_xy) >= 7:
if len(big_xy) >= self._savgol_window_length:
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=self._savgol_window_length,
polyorder=2,
axis=0
)
big_xy = smoothed.tolist()
# 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:
path_item = LabeledPointItem(px, py, label="", radius=self.path_radius,
color=Qt.magenta, removable=False, z_value=0)
n_points = len(big_xy)
for i, (px, py) in enumerate(big_xy):
fraction = i / (n_points - 1) if n_points > 1 else 0
color = Qt.red
if self._rainbow_enabled:
color = self._rainbow_color(fraction)
path_item = LabeledPointItem(px, py, label="",
radius=self.path_radius,
color=color,
removable=False,
z_value=0)
self.full_path_points.append(path_item)
self.scene.addItem(path_item)
# Keep S/E on top
# Keep anchor labels on top
for p_item in self.point_items:
if p_item._text_item:
p_item.setZValue(100)
def _compute_subpath_xy(self, xA, yA, xB, yB):
"""Return the raw path from (xA,yA)->(xB,yB)."""
if self.cost_image is None:
return []
h, w = self.cost_image.shape
......@@ -379,8 +617,15 @@ class ImageGraphicsView(QGraphicsView):
except ValueError as e:
print("Error in find_path:", e)
return []
# Convert from (row, col) to (x, y)
return [(c, r) for (r, c) in path_rc]
def _rainbow_color(self, fraction):
hue = int(300 * fraction)
saturation = 255
value = 255
return QColor.fromHsv(hue, saturation, value)
# --------------------------------------------------------------------
# MOUSE EVENTS
# --------------------------------------------------------------------
......@@ -390,51 +635,40 @@ class ImageGraphicsView(QGraphicsView):
self._was_dragging = False
self._press_view_pos = event.pos()
# See if user is clicking near an existing anchor => drag it
idx = self._find_item_near(event.pos(), threshold=10)
if idx is not None:
self._dragging_idx = idx
self._drag_counter = 0
scene_pos = self.mapToScene(event.pos())
px, py = self.point_items[idx].get_pos()
self._drag_offset = (scene_pos.x() - px, scene_pos.y() - py)
self.setDragMode(QGraphicsView.NoDrag)
self.viewport().setCursor(Qt.ClosedHandCursor)
self.setCursor(Qt.ClosedHandCursor)
return
else:
# no anchor => we'll add a new point
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(Qt.ClosedHandCursor)
elif event.button() == Qt.RightButton:
# Right-click => remove anchor if removable
self._remove_point_by_click(event.pos())
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._dragging_idx is not None:
# Dragging anchor
scene_pos = self.mapToScene(event.pos())
x_new = scene_pos.x() - self._drag_offset[0]
y_new = scene_pos.y() - self._drag_offset[1]
# clamp so user can't drag outside
r = self.point_items[self._dragging_idx]._r
x_clamped = self._clamp(x_new, r, self._img_w - 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._drag_counter += 1
# Update path every 4 moves
if self._drag_counter >= 4:
# partial path update
self._drag_counter = 0
self._revert_cost_to_original()
self._apply_all_guide_points_to_cost()
self.anchor_points[self._dragging_idx] = (x_clamped, y_clamped)
self._rebuild_full_path()
else:
if self._mouse_pressed and (event.buttons() & Qt.LeftButton):
dist = (event.pos() - self._press_view_pos).manhattanLength()
......@@ -447,24 +681,19 @@ class ImageGraphicsView(QGraphicsView):
super().mouseReleaseEvent(event)
if event.button() == Qt.LeftButton and self._mouse_pressed:
self._mouse_pressed = False
self.viewport().setCursor(Qt.ArrowCursor)
self.setCursor(Qt.ArrowCursor)
if self._dragging_idx is not None:
# finished dragging => final update
idx = self._dragging_idx
self._dragging_idx = None
self._drag_offset = (0, 0)
self.setDragMode(QGraphicsView.ScrollHandDrag)
newX, newY = self.point_items[idx].get_pos()
self.anchor_points[idx] = (newX, newY)
self._revert_cost_to_original()
self._apply_all_guide_points_to_cost()
self._rebuild_full_path()
else:
# If user wasn't dragging => add new guide point
# No drag => add point
if not self._was_dragging:
scene_pos = self.mapToScene(event.pos())
x, y = scene_pos.x(), scene_pos.y()
......@@ -476,7 +705,6 @@ class ImageGraphicsView(QGraphicsView):
idx = self._find_item_near(view_pos, threshold=10)
if idx is None:
return
# skip if S/E
if not self.point_items[idx].is_removable():
return
......@@ -503,18 +731,6 @@ class ImageGraphicsView(QGraphicsView):
return closest_idx
return None
# --------------------------------------------------------------------
# ZOOM
# --------------------------------------------------------------------
def wheelEvent(self, event):
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()
# --------------------------------------------------------------------
# UTILS
# --------------------------------------------------------------------
......@@ -533,7 +749,6 @@ class ImageGraphicsView(QGraphicsView):
self._full_path_xy.clear()
def clear_guide_points(self):
"""Remove all removable anchors, keep S/E. Rebuild path."""
i = 0
while i < len(self.anchor_points):
if self.point_items[i].is_removable():
......@@ -553,42 +768,287 @@ class ImageGraphicsView(QGraphicsView):
self._rebuild_full_path()
def get_full_path_xy(self):
"""Return the entire path (x,y) array after smoothing."""
return self._full_path_xy
# ------------------------------------------------------------------------
# Advanced Settings Widget
# ------------------------------------------------------------------------
class AdvancedSettingsWidget(QWidget):
"""
Shows toggle rainbow, circle editor, line smoothing slider, contrast slider,
plus two image previews (contrasted-blurred and cost).
The images should maintain aspect ratio upon resize.
"""
def __init__(self, main_window, parent=None):
super().__init__(parent)
self._main_window = main_window
self._last_cb_pix = None # store QPixmap for contrasted-blurred
self._last_cost_pix = None # store QPixmap for cost
main_layout = QVBoxLayout()
self.setLayout(main_layout)
# A small grid for controls
controls_layout = QGridLayout()
# 1) Rainbow toggle
self.btn_toggle_rainbow = QPushButton("Toggle Rainbow")
self.btn_toggle_rainbow.clicked.connect(self._on_toggle_rainbow)
controls_layout.addWidget(self.btn_toggle_rainbow, 0, 0)
# 2) Circle editor
self.btn_circle_editor = QPushButton("Calibrate Kernel Size")
self.btn_circle_editor.clicked.connect(self._main_window.open_circle_editor)
controls_layout.addWidget(self.btn_circle_editor, 0, 1)
# 3) Line smoothing slider + label
self._lab_smoothing = QLabel("Line smoothing (3)")
controls_layout.addWidget(self._lab_smoothing, 1, 0)
self.line_smoothing_slider = QSlider(Qt.Horizontal)
self.line_smoothing_slider.setRange(3, 51)
self.line_smoothing_slider.setValue(3)
self.line_smoothing_slider.valueChanged.connect(self._on_line_smoothing_slider)
controls_layout.addWidget(self.line_smoothing_slider, 1, 1)
# 4) Contrast slider + label
self._lab_contrast = QLabel("Contrast (0.01)")
controls_layout.addWidget(self._lab_contrast, 2, 0)
self.contrast_slider = QSlider(Qt.Horizontal)
self.contrast_slider.setRange(1, 20)
self.contrast_slider.setValue(1) # i.e. 0.01
self.contrast_slider.setSingleStep(1)
self.contrast_slider.valueChanged.connect(self._on_contrast_slider)
controls_layout.addWidget(self.contrast_slider, 2, 1)
main_layout.addLayout(controls_layout)
# We'll set a minimum width so that the main window expands
# rather than overlapping the image
self.setMinimumWidth(350)
# Now a vertical layout for the two images, each with a label above it
images_layout = QVBoxLayout()
# 1) Contrasted-blurred label + image
self.label_cb_title = QLabel("Contrasted Blurred Image")
self.label_cb_title.setAlignment(Qt.AlignCenter)
images_layout.addWidget(self.label_cb_title)
self.label_contrasted_blurred = QLabel()
self.label_contrasted_blurred.setAlignment(Qt.AlignCenter)
self.label_contrasted_blurred.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
images_layout.addWidget(self.label_contrasted_blurred)
# 2) Cost image label + image
self.label_cost_title = QLabel("Current COST IMAGE")
self.label_cost_title.setAlignment(Qt.AlignCenter)
images_layout.addWidget(self.label_cost_title)
self.label_cost_image = QLabel()
self.label_cost_image.setAlignment(Qt.AlignCenter)
self.label_cost_image.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
images_layout.addWidget(self.label_cost_image)
main_layout.addLayout(images_layout)
def showEvent(self, event):
""" When shown, ask parent to resize to accommodate. """
super().showEvent(event)
if self.parentWidget():
self.parentWidget().adjustSize()
def resizeEvent(self, event):
"""
Keep the images at correct aspect ratio by re-scaling
our stored pixmaps to the new label sizes.
"""
super().resizeEvent(event)
self._update_labels()
def _update_labels(self):
if self._last_cb_pix is not None:
scaled_cb = self._last_cb_pix.scaled(
self.label_contrasted_blurred.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.label_contrasted_blurred.setPixmap(scaled_cb)
if self._last_cost_pix is not None:
scaled_cost = self._last_cost_pix.scaled(
self.label_cost_image.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.label_cost_image.setPixmap(scaled_cost)
def _on_toggle_rainbow(self):
self._main_window.toggle_rainbow()
def _on_line_smoothing_slider(self, value):
self._lab_smoothing.setText(f"Line smoothing ({value})")
self._main_window.image_view.set_savgol_window_length(value)
def _on_contrast_slider(self, value):
clip_limit = value / 100.0
self._lab_contrast.setText(f"Contrast ({clip_limit:.2f})")
self._main_window.update_contrast(clip_limit)
def update_displays(self, contrasted_img_np, cost_img_np):
"""
Called by main_window to refresh the two images in the advanced panel.
We'll store them as QPixmaps, then do the re-scale in _update_labels().
"""
cb_pix = self._np_array_to_qpixmap(contrasted_img_np)
cost_pix = self._np_array_to_qpixmap(cost_img_np, normalize=True)
self._last_cb_pix = cb_pix
self._last_cost_pix = cost_pix
self._update_labels()
def _np_array_to_qpixmap(self, arr, normalize=False):
if arr is None:
return None
arr_ = arr.copy()
if normalize:
mn, mx = arr_.min(), arr_.max()
if abs(mx - mn) < 1e-12:
arr_[:] = 0
else:
arr_ = (arr_ - mn) / (mx - mn)
arr_ = np.clip(arr_, 0, 1)
arr_255 = (arr_ * 255).astype(np.uint8)
h, w = arr_255.shape
qimage = QImage(arr_255.data, w, h, w, QImage.Format_Grayscale8)
return QPixmap.fromImage(qimage)
# ------------------------------------------------------------------------
# Main Window
# ------------------------------------------------------------------------
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Test GUI")
main_widget = QWidget()
main_layout = QVBoxLayout(main_widget)
self._last_loaded_pixmap = None
self._circle_calibrated_radius = 6
self._last_loaded_file_path = None
# For the contrast slider
self._current_clip_limit = 0.01
# Outer widget + layout
self._main_widget = QWidget()
self._main_layout = QHBoxLayout(self._main_widget)
# The "left" part: container for the image area + its controls
self._left_panel = QVBoxLayout()
# We'll make a container widget for the left panel, so we can set stretches:
self._left_container = QWidget()
self._left_container.setLayout(self._left_panel)
# Now we add them to the main layout with 70%:30% ratio
self._main_layout.addWidget(self._left_container, 7) # 70%
# We haven't added the advanced widget yet, but we'll do so with ratio=3 => 30%
self._advanced_widget = AdvancedSettingsWidget(self)
# Hide it initially
self._advanced_widget.hide()
self._main_layout.addWidget(self._advanced_widget, 3)
self.setCentralWidget(self._main_widget)
# The image view
self.image_view = ImageGraphicsView()
main_layout.addWidget(self.image_view)
self._left_panel.addWidget(self.image_view)
# Buttons layout
# Button row
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)
# Export Path
self.btn_export_path = QPushButton("Export Path")
self.btn_export_path.clicked.connect(self.export_path)
btn_layout.addWidget(self.btn_export_path)
# 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)
# "Advanced Settings" toggle
self.btn_advanced = QPushButton("Advanced Settings")
self.btn_advanced.setCheckable(True)
self.btn_advanced.clicked.connect(self._toggle_advanced_settings)
btn_layout.addWidget(self.btn_advanced)
self._left_panel.addLayout(btn_layout)
self.resize(1000, 600)
self._old_central_widget = None
self._editor = None
def _toggle_advanced_settings(self, checked):
if checked:
self._advanced_widget.show()
else:
self._advanced_widget.hide()
# Force re-layout
self.adjustSize()
def open_circle_editor(self):
""" Replace central widget with circle editor. """
if not self._last_loaded_pixmap:
print("No image loaded yet! Cannot open circle editor.")
return
old_widget = self.takeCentralWidget()
self._old_central_widget = old_widget
init_radius = self._circle_calibrated_radius
editor = CircleEditorWidget(
pixmap=self._last_loaded_pixmap,
init_radius=init_radius,
done_callback=self._on_circle_editor_done
)
self._editor = editor
self.setCentralWidget(editor)
def _on_circle_editor_done(self, final_radius):
self._circle_calibrated_radius = final_radius
print(f"Circle Editor done. Radius = {final_radius}")
if self._last_loaded_file_path:
cost_img = compute_cost_image(
self._last_loaded_file_path,
self._circle_calibrated_radius,
clip_limit=self._current_clip_limit
)
self.image_view.cost_image_original = cost_img
self.image_view.cost_image = cost_img.copy()
self.image_view._apply_all_guide_points_to_cost()
self.image_view._rebuild_full_path()
self._update_advanced_images()
editor_widget = self.takeCentralWidget()
if editor_widget is not None:
editor_widget.setParent(None)
if self._old_central_widget is not None:
self.setCentralWidget(self._old_central_widget)
self._old_central_widget = None
if self._editor is not None:
self._editor.deleteLater()
self._editor = None
def toggle_rainbow(self):
self.image_view.toggle_rainbow()
def load_image(self):
options = QFileDialog.Options()
......@@ -599,27 +1059,109 @@ class MainWindow(QMainWindow):
)
if file_path:
self.image_view.load_image(file_path)
cost_img = compute_cost_image(file_path)
cost_img = compute_cost_image(
file_path,
self._circle_calibrated_radius,
clip_limit=self._current_clip_limit
)
self.image_view.cost_image_original = cost_img
self.image_view.cost_image = cost_img.copy()
pm = QPixmap(file_path)
if not pm.isNull():
self._last_loaded_pixmap = pm
self._last_loaded_file_path = file_path
self._update_advanced_images()
def update_contrast(self, clip_limit):
self._current_clip_limit = clip_limit
if self._last_loaded_file_path:
cost_img = compute_cost_image(
self._last_loaded_file_path,
self._circle_calibrated_radius,
clip_limit=clip_limit
)
self.image_view.cost_image_original = cost_img
self.image_view.cost_image = cost_img.copy()
self.image_view._apply_all_guide_points_to_cost()
self.image_view._rebuild_full_path()
self._update_advanced_images()
def _update_advanced_images(self):
if not self._last_loaded_pixmap:
return
pm_np = self._qpixmap_to_gray_float(self._last_loaded_pixmap)
contrasted_blurred = preprocess_image(
pm_np,
sigma=3,
clip_limit=self._current_clip_limit
)
cost_img_np = self.image_view.cost_image
self._advanced_widget.update_displays(contrasted_blurred, cost_img_np)
def _qpixmap_to_gray_float(self, qpix):
img = qpix.toImage()
img = img.convertToFormat(QImage.Format_ARGB32)
ptr = img.bits()
ptr.setsize(img.byteCount())
arr = np.frombuffer(ptr, np.uint8).reshape((img.height(), img.width(), 4))
rgb = arr[..., :3].astype(np.float32)
gray = rgb.mean(axis=2) / 255.0
return gray
def export_path(self):
"""Export the full path (x,y) as a .npy file."""
"""
Exports the path as a CSV in the format: x, y, TYPE,
ensuring that each anchor influences exactly one path point.
"""
full_xy = self.image_view.get_full_path_xy()
if not full_xy:
print("No path to export.")
return
# We'll consider each anchor point as "USER-PLACED".
# But unlike a distance-threshold approach, we assign each anchor
# to exactly one closest path point.
anchor_points = self.image_view.anchor_points
# For each anchor, find the index of the closest path point
user_placed_indices = set()
for ax, ay in anchor_points:
min_dist = float('inf')
closest_idx = None
for i, (px, py) in enumerate(full_xy):
dist = math.hypot(px - ax, py - ay)
if dist < min_dist:
min_dist = dist
closest_idx = i
if closest_idx is not None:
user_placed_indices.add(closest_idx)
# Ask user for the CSV filename
options = QFileDialog.Options()
file_path, _ = QFileDialog.getSaveFileName(
self, "Export Path", "",
"NumPy Files (*.npy);;All Files (*)",
"CSV Files (*.csv);;All Files (*)",
options=options
)
if file_path:
arr = np.array(full_xy)
np.save(file_path, arr)
print(f"Exported path with {len(arr)} points to {file_path}")
if not file_path:
return
import csv
with open(file_path, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["x", "y", "TYPE"])
for i, (x, y) in enumerate(full_xy):
ptype = "USER-PLACED" if i in user_placed_indices else "PATH"
writer.writerow([x, y, ptype])
print(f"Exported path with {len(full_xy)} points to {file_path}")
def clear_points(self):
self.image_view.clear_guide_points()
......
File added
File added
data/agamodon_slice.png

87.1 KiB

File added
data/angustifrons_slice.png

91.3 KiB

File added
File added
data/bipes_slice.png

57.4 KiB

File added
data/test_image.jpg

84.7 KiB

import time
import cv2
import numpy as np
import matplotlib.pyplot as plt
......@@ -8,62 +7,40 @@ from skimage.feature import canny
from skimage.graph import route_through_array
from scipy.signal import convolve2d
'''
### Canny Edge cost image
def compute_cost_image(path, sigma=3):
### Disk live wire cost image
### Load image
image = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
def compute_disk_size(user_radius, upscale_factor=1.2):
return int(np.ceil(upscale_factor * 2 * user_radius + 1) // 2 * 2 + 1)
def load_image(path):
return cv2.imread(path, cv2.IMREAD_GRAYSCALE)
def preprocess_image(image, sigma=3, clip_limit=0.01):
# Apply histogram equalization
image_contrasted = exposure.equalize_adapthist(image, clip_limit=0.01)
image_contrasted = exposure.equalize_adapthist(image, clip_limit=clip_limit)
# Apply smoothing
smoothed_img = gaussian(image_contrasted, sigma=sigma)
# Apply Canny edge detection
canny_img = canny(smoothed_img)
# Create cost image
cost_img = 1.0 / (canny_img + 1e-5) # Invert edges: higher cost where edges are stronger
return smoothed_img
return cost_img
def find_path(cost_image, points):
def compute_cost_image(path, user_radius, sigma=3, clip_limit=0.01):
if len(points) != 2:
raise ValueError("Points should be a list of 2 points: seed and target.")
seed_rc, target_rc = points
path_rc, cost = route_through_array(
cost_image,
start=seed_rc,
end=target_rc,
fully_connected=True
)
return path_rc
'''
### Disk live wire cost image
def compute_cost_image(path, sigma=3, disk_size=15):
disk_size = compute_disk_size(user_radius)
### Load image
image = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
# Apply histogram equalization
image_contrasted = exposure.equalize_adapthist(image, clip_limit=0.01)
image = load_image(path)
# Apply smoothing
smoothed_img = gaussian(image_contrasted, sigma=sigma)
smoothed_img = preprocess_image(image, sigma=sigma, clip_limit=clip_limit)
# Apply Canny edge detection
canny_img = canny(smoothed_img)
# Do disk thing
binary_img = canny_img
k_size = 17
kernel = circle_edge_kernel(k_size=disk_size)
convolved = convolve2d(binary_img, kernel, mode='same', boundary='fill')
......@@ -128,12 +105,7 @@ def circle_edge_kernel(k_size=5, radius=None):
return kernel
# Other functions
# Other functions (to be implemented?)
def downscale(img, points, scale_percent):
"""
Downsample `img` to `scale_percent` size and scale the given points accordingly.
......@@ -161,38 +133,3 @@ def downscale(img, points, scale_percent):
scaled_target_xy = (int(target_xy[0] * scale_x), int(target_xy[1] * scale_y))
return downsampled_img, (scaled_seed_xy, scaled_target_xy)
\ No newline at end of file
def compute_cost(image, sigma=3.0, epsilon=1e-5):
"""
Smooth the image, run Canny edge detection, then invert the edge map into a cost image.
"""
# Apply histogram equalization
image_contrasted = exposure.equalize_adapthist(image, clip_limit=0.01)
# Apply smoothing
smoothed_img = gaussian(image_contrasted, sigma=sigma)
# Apply Canny edge detection
canny_img = canny(smoothed_img)
# Create cost image
cost_img = 1.0 / (canny_img + epsilon) # Invert edges: higher cost where edges are stronger
return cost_img, canny_img
def backtrack_pixels_on_image(img_color, path_coords, bgr_color=(0, 0, 255)):
"""
Color the path on the (already converted BGR) image in the specified color.
`path_coords` should be a list of (row, col) or (y, x).
"""
for (row, col) in path_coords:
img_color[row, col] = bgr_color
return img_color
def export_path(path_coords, path_name):
"""
Export the path to a np array.
"""
np.save(path_name, path_coords)
return None
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment