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