Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
pt2d
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Iterations
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package registry
Container registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
QIM
Tools
pt2d
Commits
aacaf6df
Commit
aacaf6df
authored
6 months ago
by
s224389
Browse files
Options
Downloads
Patches
Plain Diff
Delete GUI_draft_live.py
parent
f07540f5
No related branches found
No related tags found
No related merge requests found
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
GUI_draft_live.py
+0
-1181
0 additions, 1181 deletions
GUI_draft_live.py
with
0 additions
and
1181 deletions
GUI_draft_live.py
deleted
100644 → 0
+
0
−
1181
View file @
f07540f5
import
sys
import
math
import
csv
# <-- Added
import
numpy
as
np
# For smoothing the path
from
scipy.signal
import
savgol_filter
from
PyQt5.QtWidgets
import
(
QApplication
,
QMainWindow
,
QGraphicsView
,
QGraphicsScene
,
QGraphicsEllipseItem
,
QGraphicsPixmapItem
,
QPushButton
,
QHBoxLayout
,
QVBoxLayout
,
QWidget
,
QFileDialog
,
QGraphicsTextItem
,
QSlider
,
QLabel
,
QCheckBox
,
QGridLayout
,
QSizePolicy
)
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
)
# ------------------------------------------------------------------------
# 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
)
self
.
_x
=
x
self
.
_y
=
y
self
.
_r
=
radius
self
.
_removable
=
removable
pen
=
QPen
(
color
)
brush
=
QBrush
(
color
)
self
.
setPen
(
pen
)
self
.
setBrush
(
brush
)
self
.
setZValue
(
z_value
)
self
.
_text_item
=
None
if
label
:
self
.
_text_item
=
QGraphicsTextItem
(
self
)
self
.
_text_item
.
setPlainText
(
label
)
self
.
_text_item
.
setDefaultTextColor
(
QColor
(
"
black
"
))
font
=
QFont
(
"
Arial
"
,
14
)
font
.
setBold
(
True
)
self
.
_text_item
.
setFont
(
font
)
self
.
_scale_text_to_fit
()
self
.
set_pos
(
x
,
y
)
def
_scale_text_to_fit
(
self
):
if
not
self
.
_text_item
:
return
self
.
_text_item
.
setScale
(
1.0
)
circle_diam
=
2
*
self
.
_r
raw_rect
=
self
.
_text_item
.
boundingRect
()
text_w
=
raw_rect
.
width
()
text_h
=
raw_rect
.
height
()
if
text_w
>
circle_diam
or
text_h
>
circle_diam
:
scale_factor
=
min
(
circle_diam
/
text_w
,
circle_diam
/
text_h
)
self
.
_text_item
.
setScale
(
scale_factor
)
self
.
_center_label
()
def
_center_label
(
self
):
if
not
self
.
_text_item
:
return
ellipse_w
=
2
*
self
.
_r
ellipse_h
=
2
*
self
.
_r
raw_rect
=
self
.
_text_item
.
boundingRect
()
scale_factor
=
self
.
_text_item
.
scale
()
scaled_w
=
raw_rect
.
width
()
*
scale_factor
scaled_h
=
raw_rect
.
height
()
*
scale_factor
tx
=
(
ellipse_w
-
scaled_w
)
*
0.5
ty
=
(
ellipse_h
-
scaled_h
)
*
0.5
self
.
_text_item
.
setPos
(
tx
,
ty
)
def
set_pos
(
self
,
x
,
y
):
"""
Positions the circle so its center is at (x, y).
"""
self
.
_x
=
x
self
.
_y
=
y
self
.
setPos
(
x
-
self
.
_r
,
y
-
self
.
_r
)
def
get_pos
(
self
):
return
(
self
.
_x
,
self
.
_y
)
def
distance_to
(
self
,
x_other
,
y_other
):
return
math
.
sqrt
((
self
.
_x
-
x_other
)
**
2
+
(
self
.
_y
-
y_other
)
**
2
)
def
is_removable
(
self
):
return
self
.
_removable
# ------------------------------------------------------------------------
# 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
)
# Image display
self
.
image_item
=
QGraphicsPixmapItem
()
self
.
scene
.
addItem
(
self
.
image_item
)
self
.
anchor_points
=
[]
# List[(x, y)]
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
self
.
radius_cost_image
=
2
self
.
_img_w
=
0
self
.
_img_h
=
0
self
.
_mouse_pressed
=
False
self
.
_press_view_pos
=
None
self
.
_drag_threshold
=
5
self
.
_was_dragging
=
False
self
.
_dragging_idx
=
None
self
.
_drag_offset
=
(
0
,
0
)
self
.
_drag_counter
=
0
# Cost images
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
# --------------------------------------------------------------------
def
load_image
(
self
,
path
):
pixmap
=
QPixmap
(
path
)
if
not
pixmap
.
isNull
():
self
.
image_item
.
setPixmap
(
pixmap
)
self
.
setSceneRect
(
QRectF
(
pixmap
.
rect
()))
self
.
_img_w
=
pixmap
.
width
()
self
.
_img_h
=
pixmap
.
height
()
self
.
_clear_all_points
()
self
.
resetTransform
()
self
.
fitInView
(
self
.
image_item
,
Qt
.
KeepAspectRatio
)
# By default, add S/E
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
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
)
# --------------------------------------------------------------------
# ANCHOR POINTS
# --------------------------------------------------------------------
def
_insert_anchor_point
(
self
,
idx
,
x
,
y
,
label
=
""
,
removable
=
True
,
z_val
=
0
,
radius
=
4
):
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
,
removable
=
removable
,
z_value
=
z_val
)
self
.
point_items
.
insert
(
idx
,
item
)
self
.
scene
.
addItem
(
item
)
def
_add_guide_point
(
self
,
x
,
y
):
# 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
:
self
.
_insert_anchor_point
(
-
1
,
x_clamped
,
y_clamped
,
label
=
""
,
removable
=
True
,
z_val
=
1
,
radius
=
self
.
dot_radius
)
else
:
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
):
# If somehow we have no path yet
if
not
self
.
_full_path_xy
:
self
.
_insert_anchor_point
(
-
1
,
x_new
,
y_new
)
return
# 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
):
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
:
self
.
_insert_anchor_point
(
-
1
,
x_new
,
y_new
)
return
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
for
(
ax
,
ay
)
in
self
.
anchor_points
:
if
approx_equal
(
ax
,
ay
,
cx
,
cy
):
return
True
return
False
# Walk left
left_anchor_pt
=
None
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
right_anchor_pt
=
None
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 we can't find distinct anchors on left & right,
# just insert before E.
if
not
left_anchor_pt
or
not
right_anchor_pt
:
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
# Convert anchor coords -> anchor_points indices
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
:
self
.
_insert_anchor_point
(
-
1
,
x_new
,
y_new
)
return
# Insert between them
if
left_idx
<
right_idx
:
insert_idx
=
left_idx
+
1
else
:
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
)
# --------------------------------------------------------------------
# COST IMAGE
# --------------------------------------------------------------------
def
_revert_cost_to_original
(
self
):
if
self
.
cost_image_original
is
not
None
:
self
.
cost_image
=
self
.
cost_image_original
.
copy
()
def
_apply_all_guide_points_to_cost
(
self
):
if
self
.
cost_image
is
None
:
return
for
i
,
(
ax
,
ay
)
in
enumerate
(
self
.
anchor_points
):
if
self
.
point_items
[
i
].
is_removable
():
self
.
_lower_cost_in_circle
(
ax
,
ay
,
self
.
radius_cost_image
)
def
_lower_cost_in_circle
(
self
,
x_f
,
y_f
,
radius
):
if
self
.
cost_image
is
None
:
return
h
,
w
=
self
.
cost_image
.
shape
row_c
=
int
(
round
(
y_f
))
col_c
=
int
(
round
(
x_f
))
if
not
(
0
<=
row_c
<
h
and
0
<=
col_c
<
w
):
return
global_min
=
self
.
cost_image
.
min
()
r_s
=
max
(
0
,
row_c
-
radius
)
r_e
=
min
(
h
,
row_c
+
radius
+
1
)
c_s
=
max
(
0
,
col_c
-
radius
)
c_e
=
min
(
w
,
col_c
+
radius
+
1
)
for
rr
in
range
(
r_s
,
r_e
):
for
cc
in
range
(
c_s
,
c_e
):
dist
=
math
.
sqrt
((
rr
-
row_c
)
**
2
+
(
cc
-
col_c
)
**
2
)
if
dist
<=
radius
:
self
.
cost_image
[
rr
,
cc
]
=
global_min
# --------------------------------------------------------------------
# PATH BUILDING
# --------------------------------------------------------------------
def
_rebuild_full_path
(
self
):
for
item
in
self
.
full_path_points
:
self
.
scene
.
removeItem
(
item
)
self
.
full_path_points
.
clear
()
self
.
_full_path_xy
.
clear
()
if
len
(
self
.
anchor_points
)
<
2
or
self
.
cost_image
is
None
:
return
big_xy
=
[]
for
i
in
range
(
len
(
self
.
anchor_points
)
-
1
):
xA
,
yA
=
self
.
anchor_points
[
i
]
xB
,
yB
=
self
.
anchor_points
[
i
+
1
]
sub_xy
=
self
.
_compute_subpath_xy
(
xA
,
yA
,
xB
,
yB
)
if
i
==
0
:
big_xy
.
extend
(
sub_xy
)
else
:
if
len
(
sub_xy
)
>
1
:
big_xy
.
extend
(
sub_xy
[
1
:])
if
len
(
big_xy
)
>=
self
.
_savgol_window_length
:
arr_xy
=
np
.
array
(
big_xy
)
smoothed
=
savgol_filter
(
arr_xy
,
window_length
=
self
.
_savgol_window_length
,
polyorder
=
2
,
axis
=
0
)
big_xy
=
smoothed
.
tolist
()
self
.
_full_path_xy
=
big_xy
[:]
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 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
):
if
self
.
cost_image
is
None
:
return
[]
h
,
w
=
self
.
cost_image
.
shape
rA
,
cA
=
int
(
round
(
yA
)),
int
(
round
(
xA
))
rB
,
cB
=
int
(
round
(
yB
)),
int
(
round
(
xB
))
rA
=
max
(
0
,
min
(
rA
,
h
-
1
))
cA
=
max
(
0
,
min
(
cA
,
w
-
1
))
rB
=
max
(
0
,
min
(
rB
,
h
-
1
))
cB
=
max
(
0
,
min
(
cB
,
w
-
1
))
try
:
path_rc
=
find_path
(
self
.
cost_image
,
[(
rA
,
cA
),
(
rB
,
cB
)])
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
# --------------------------------------------------------------------
def
mousePressEvent
(
self
,
event
):
if
event
.
button
()
==
Qt
.
LeftButton
:
self
.
_mouse_pressed
=
True
self
.
_was_dragging
=
False
self
.
_press_view_pos
=
event
.
pos
()
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
.
setCursor
(
Qt
.
ClosedHandCursor
)
return
elif
event
.
button
()
==
Qt
.
RightButton
:
self
.
_remove_point_by_click
(
event
.
pos
())
super
().
mousePressEvent
(
event
)
def
mouseMoveEvent
(
self
,
event
):
if
self
.
_dragging_idx
is
not
None
:
scene_pos
=
self
.
mapToScene
(
event
.
pos
())
x_new
=
scene_pos
.
x
()
-
self
.
_drag_offset
[
0
]
y_new
=
scene_pos
.
y
()
-
self
.
_drag_offset
[
1
]
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
:
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
()
if
dist
>
self
.
_drag_threshold
:
self
.
_was_dragging
=
True
super
().
mouseMoveEvent
(
event
)
def
mouseReleaseEvent
(
self
,
event
):
super
().
mouseReleaseEvent
(
event
)
if
event
.
button
()
==
Qt
.
LeftButton
and
self
.
_mouse_pressed
:
self
.
_mouse_pressed
=
False
self
.
setCursor
(
Qt
.
ArrowCursor
)
if
self
.
_dragging_idx
is
not
None
:
idx
=
self
.
_dragging_idx
self
.
_dragging_idx
=
None
self
.
_drag_offset
=
(
0
,
0
)
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
:
# No drag => add point
if
not
self
.
_was_dragging
:
scene_pos
=
self
.
mapToScene
(
event
.
pos
())
x
,
y
=
scene_pos
.
x
(),
scene_pos
.
y
()
self
.
_add_guide_point
(
x
,
y
)
self
.
_was_dragging
=
False
def
_remove_point_by_click
(
self
,
view_pos
):
idx
=
self
.
_find_item_near
(
view_pos
,
threshold
=
10
)
if
idx
is
None
:
return
if
not
self
.
point_items
[
idx
].
is_removable
():
return
self
.
scene
.
removeItem
(
self
.
point_items
[
idx
])
self
.
point_items
.
pop
(
idx
)
self
.
anchor_points
.
pop
(
idx
)
self
.
_revert_cost_to_original
()
self
.
_apply_all_guide_points_to_cost
()
self
.
_rebuild_full_path
()
def
_find_item_near
(
self
,
view_pos
,
threshold
=
10
):
scene_pos
=
self
.
mapToScene
(
view_pos
)
x_click
,
y_click
=
scene_pos
.
x
(),
scene_pos
.
y
()
closest_idx
=
None
min_dist
=
float
(
'
inf
'
)
for
i
,
itm
in
enumerate
(
self
.
point_items
):
d
=
itm
.
distance_to
(
x_click
,
y_click
)
if
d
<
min_dist
:
min_dist
=
d
closest_idx
=
i
if
closest_idx
is
not
None
and
min_dist
<=
threshold
:
return
closest_idx
return
None
# --------------------------------------------------------------------
# UTILS
# --------------------------------------------------------------------
def
_clamp
(
self
,
val
,
mn
,
mx
):
return
max
(
mn
,
min
(
val
,
mx
))
def
_clear_all_points
(
self
):
for
it
in
self
.
point_items
:
self
.
scene
.
removeItem
(
it
)
self
.
point_items
.
clear
()
self
.
anchor_points
.
clear
()
for
p
in
self
.
full_path_points
:
self
.
scene
.
removeItem
(
p
)
self
.
full_path_points
.
clear
()
self
.
_full_path_xy
.
clear
()
def
clear_guide_points
(
self
):
i
=
0
while
i
<
len
(
self
.
anchor_points
):
if
self
.
point_items
[
i
].
is_removable
():
self
.
scene
.
removeItem
(
self
.
point_items
[
i
])
del
self
.
point_items
[
i
]
del
self
.
anchor_points
[
i
]
else
:
i
+=
1
for
it
in
self
.
full_path_points
:
self
.
scene
.
removeItem
(
it
)
self
.
full_path_points
.
clear
()
self
.
_full_path_xy
.
clear
()
self
.
_revert_cost_to_original
()
self
.
_apply_all_guide_points_to_cost
()
self
.
_rebuild_full_path
()
def
get_full_path_xy
(
self
):
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
:
.
2
f
}
)
"
)
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
"
)
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
()
self
.
_left_panel
.
addWidget
(
self
.
image_view
)
# Button row
btn_layout
=
QHBoxLayout
()
self
.
btn_load_image
=
QPushButton
(
"
Load Image
"
)
self
.
btn_load_image
.
clicked
.
connect
(
self
.
load_image
)
btn_layout
.
addWidget
(
self
.
btn_load_image
)
self
.
btn_export_path
=
QPushButton
(
"
Export Path
"
)
self
.
btn_export_path
.
clicked
.
connect
(
self
.
export_path
)
btn_layout
.
addWidget
(
self
.
btn_export_path
)
self
.
btn_clear_points
=
QPushButton
(
"
Clear Points
"
)
self
.
btn_clear_points
.
clicked
.
connect
(
self
.
clear_points
)
btn_layout
.
addWidget
(
self
.
btn_clear_points
)
# "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
()
file_path
,
_
=
QFileDialog
.
getOpenFileName
(
self
,
"
Open Image
"
,
""
,
"
Images (*.png *.jpg *.jpeg *.bmp *.tif)
"
,
options
=
options
)
if
file_path
:
self
.
image_view
.
load_image
(
file_path
)
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
):
"""
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
"
,
""
,
"
CSV Files (*.csv);;All Files (*)
"
,
options
=
options
)
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
()
def
closeEvent
(
self
,
event
):
super
().
closeEvent
(
event
)
def
main
():
app
=
QApplication
(
sys
.
argv
)
window
=
MainWindow
()
window
.
show
()
sys
.
exit
(
app
.
exec_
())
if
__name__
==
"
__main__
"
:
main
()
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment