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
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
QIM
Tools
pt2d
Commits
14d289df
Commit
14d289df
authored
3 months ago
by
s224389
Browse files
Options
Downloads
Patches
Plain Diff
Added:
- Added text to start/end points and fixed bugs - Ensured points can only exist in the image
parent
0f455b57
No related branches found
No related tags found
No related merge requests found
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
GUI_draft.py
+136
-108
136 additions, 108 deletions
GUI_draft.py
with
136 additions
and
108 deletions
GUI_draft.py
+
136
−
108
View file @
14d289df
...
...
@@ -5,81 +5,113 @@ import numpy as np
from
PyQt5.QtWidgets
import
(
QApplication
,
QMainWindow
,
QGraphicsView
,
QGraphicsScene
,
QGraphicsEllipseItem
,
QGraphicsPixmapItem
,
QPushButton
,
QHBoxLayout
,
QVBoxLayout
,
QWidget
,
QFileDialog
,
QGraphics
Simple
TextItem
QHBoxLayout
,
QVBoxLayout
,
QWidget
,
QFileDialog
,
QGraphicsTextItem
)
from
PyQt5.QtGui
import
QPixmap
,
QPen
,
QBrush
from
PyQt5.QtGui
import
QPixmap
,
QPen
,
QBrush
,
QColor
,
QFont
from
PyQt5.QtCore
import
Qt
,
QRectF
class
LabeledPointItem
(
QGraphicsEllipseItem
):
"""
A point item with optional label (e.g.
'
S
'
or
'
E
'
), color, and a flag for removability.
A circle with optional (bold) label (e.g.
'
S
'
/
'
E
'
),
which automatically scales the text if it
'
s bigger than the circle.
"""
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
.
_removable
=
removable
# If False, point cannot be removed by right-click
super
().
__init__
(
0
,
0
,
2
*
radius
,
2
*
radius
,
parent
)
self
.
_x
=
x
# Center x
self
.
_y
=
y
# Center y
self
.
_r
=
radius
# Circle radius
self
.
_removable
=
removable
#
Ellips
e styling
#
Circl
e styling
pen
=
QPen
(
color
)
brush
=
QBrush
(
color
)
self
.
setPen
(
pen
)
self
.
setBrush
(
brush
)
#
If we have a label, add a child
text
item
#
Optional
text
label
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
)
self
.
_text_item
=
QGraphicsTextItem
(
self
)
self
.
_text_item
.
setPlainText
(
label
)
self
.
_text_item
.
setDefaultTextColor
(
QColor
(
"
black
"
))
# Bold text
font
=
QFont
(
"
Arial
"
,
14
)
font
.
setBold
(
True
)
self
.
_text_item
.
setFont
(
font
)
self
.
_scale_text_to_fit
()
# Move so center is at (x, y)
self
.
set_pos
(
x
,
y
)
def
_scale_text_to_fit
(
self
):
"""
Scale the text down so it fits fully within the circle
'
s diameter.
"""
if
not
self
.
_text_item
:
return
# 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
)
# Reset scale first
self
.
_text_item
.
setScale
(
1.0
)
def
is_removable
(
self
):
"""
Return True if this point can be removed by user, False otherwise.
"""
return
self
.
_removable
circle_diam
=
2
*
self
.
_r
raw_rect
=
self
.
_text_item
.
boundingRect
()
text_w
=
raw_rect
.
width
()
text_h
=
raw_rect
.
height
()
def
get_pos
(
self
):
"""
Return the (x, y) of this point in scene coords.
"""
return
(
self
.
_x
,
self
.
_y
)
if
text_w
>
circle_diam
or
text_h
>
circle_diam
:
scale_w
=
circle_diam
/
text_w
scale_h
=
circle_diam
/
text_h
scale_factor
=
min
(
scale_w
,
scale_h
)
self
.
_text_item
.
setScale
(
scale_factor
)
self
.
_center_label
()
def
_center_label
(
self
):
"""
Center the text in the circle, taking into account any scaling.
"""
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
):
"""
Move point to (x, y).
Also update ellipse and label position accordingly.
Move so the circle
'
s center is at (x,y) in scene coords.
"""
self
.
_x
=
x
self
.
_y
=
y
self
.
setRect
(
x
-
self
.
_r
,
y
-
self
.
_r
,
2
*
self
.
_r
,
2
*
self
.
_r
)
# Because our ellipse is (0,0,2*r,2*r) in local coords,
# we shift by (x-r, y-r).
self
.
setPos
(
x
-
self
.
_r
,
y
-
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
get_pos
(
self
):
return
(
self
.
_x
,
self
.
_y
)
def
distance_to
(
self
,
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
)
def
is_removable
(
self
):
return
self
.
_removable
class
ImageGraphicsView
(
QGraphicsView
):
"""
Custom class for displaying an image and placing/dragging points.
Displays an image and allows placing/dragging labeled points.
Ensures points can
'
t go outside the image boundary.
"""
def
__init__
(
self
,
parent
=
None
):
super
().
__init__
(
parent
)
# Create scene and add it to the view
self
.
scene
=
QGraphicsScene
(
self
)
self
.
setScene
(
self
.
scene
)
...
...
@@ -90,16 +122,19 @@ class ImageGraphicsView(QGraphicsView):
self
.
image_item
=
QGraphicsPixmapItem
()
self
.
scene
.
addItem
(
self
.
image_item
)
# Points
self
.
points
=
[]
self
.
editor_mode
=
False
self
.
dot_radius
=
4
# Enable built-in panning around image, but force arrow cursor initially
# For normal red dots
self
.
dot_radius
=
4
# Keep track of image size
self
.
_img_w
=
0
self
.
_img_h
=
0
self
.
setDragMode
(
QGraphicsView
.
ScrollHandDrag
)
self
.
viewport
().
setCursor
(
Qt
.
ArrowCursor
)
# Track clicking vs. dragging
self
.
_mouse_pressed
=
False
self
.
_press_view_pos
=
None
self
.
_drag_threshold
=
5
...
...
@@ -108,35 +143,56 @@ class ImageGraphicsView(QGraphicsView):
self
.
_drag_offset
=
(
0
,
0
)
def
load_image
(
self
,
image_path
):
"""
Load an image, clear old points, add
'
S
'
/
'
E
'
points, fit in the view.
"""
pixmap
=
QPixmap
(
image_path
)
if
not
pixmap
.
isNull
():
self
.
image_item
.
setPixmap
(
pixmap
)
self
.
setSceneRect
(
QRectF
(
pixmap
.
rect
()))
# Remove old points from scene
self
.
_clear_point_items
(
remove_all
=
True
)
# Save image dimensions
self
.
_img_w
=
pixmap
.
width
()
self
.
_img_h
=
pixmap
.
height
()
# Reset transform + fit
self
.
_clear_point_items
(
remove_all
=
True
)
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
)
# Positions for S/E
s_x
=
self
.
_img_w
*
0.15
s_y
=
self
.
_img_h
*
0.5
e_x
=
self
.
_img_w
*
0.85
e_y
=
self
.
_img_h
*
0.5
# Create green S/E with radius=6
s_point
=
self
.
_create_point
(
s_x
,
s_y
,
"
S
"
,
6
,
Qt
.
green
,
removable
=
False
)
e_point
=
self
.
_create_point
(
e_x
,
e_y
,
"
E
"
,
6
,
Qt
.
green
,
removable
=
False
)
# Put S in front, E in back
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/drag dots; if False: do nothing on click.
"""
self
.
editor_mode
=
mode
def
_create_point
(
self
,
x
,
y
,
label
,
radius
,
color
,
removable
=
True
):
"""
Helper to create a LabeledPointItem at (x,y), but clamp inside image first.
"""
# Clamp coordinates so center doesn't go outside
cx
=
self
.
_clamp
(
x
,
radius
,
self
.
_img_w
-
radius
)
cy
=
self
.
_clamp
(
y
,
radius
,
self
.
_img_h
-
radius
)
return
LabeledPointItem
(
cx
,
cy
,
label
=
label
,
radius
=
radius
,
color
=
color
,
removable
=
removable
)
def
_clamp
(
self
,
val
,
min_val
,
max_val
):
return
max
(
min_val
,
min
(
val
,
max_val
))
def
mousePressEvent
(
self
,
event
):
if
event
.
button
()
==
Qt
.
LeftButton
:
self
.
_mouse_pressed
=
True
...
...
@@ -144,30 +200,24 @@ class ImageGraphicsView(QGraphicsView):
self
.
_press_view_pos
=
event
.
pos
()
if
self
.
editor_mode
:
# Check if we're near a point
idx
=
self
.
_find_point_near
(
event
.
pos
(),
threshold
=
10
)
if
idx
is
not
None
:
# Start dragging that point
self
.
_dragging_idx
=
idx
scene_pos
=
self
.
mapToScene
(
event
.
pos
())
px
,
py
=
self
.
points
[
idx
].
get_pos
()
self
.
_drag_offset
=
(
scene_pos
.
x
()
-
px
,
scene_pos
.
y
()
-
py
)
# Temporarily disable QGraphicsView's panning
self
.
setDragMode
(
QGraphicsView
.
NoDrag
)
self
.
viewport
().
setCursor
(
Qt
.
ClosedHandCursor
)
return
else
:
# Not near a point => normal panning
self
.
setDragMode
(
QGraphicsView
.
ScrollHandDrag
)
self
.
viewport
().
setCursor
(
Qt
.
ClosedHandCursor
)
else
:
# Editor mode off => normal panning
self
.
setDragMode
(
QGraphicsView
.
ScrollHandDrag
)
self
.
viewport
().
setCursor
(
Qt
.
ClosedHandCursor
)
elif
event
.
button
()
==
Qt
.
RightButton
:
# If Editor Mode is ON, try removing nearest dot
if
self
.
editor_mode
:
self
.
_remove_point
(
event
.
pos
())
...
...
@@ -175,42 +225,44 @@ class ImageGraphicsView(QGraphicsView):
def
mouseMoveEvent
(
self
,
event
):
if
self
.
_dragging_idx
is
not
None
:
#
Move the dragged
point
#
Dragging an existing
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
)
# Clamp center so it doesn't go out of the image
r
=
self
.
points
[
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
.
points
[
self
.
_dragging_idx
].
set_pos
(
x_clamped
,
y_clamped
)
return
else
:
# If movement > threshold => treat as pan
ning
# If movement > threshold => treat as pan
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
.
viewport
().
setCursor
(
Qt
.
ArrowCursor
)
# 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
:
#
N
ot dragg
ing =>
maybe add a new point
#
If n
ot dragg
ed,
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.
"""
zoom_in_factor
=
1.25
zoom_out_factor
=
1
/
zoom_in_factor
...
...
@@ -218,28 +270,26 @@ class ImageGraphicsView(QGraphicsView):
self
.
scale
(
zoom_in_factor
,
zoom_in_factor
)
else
:
self
.
scale
(
zoom_out_factor
,
zoom_out_factor
)
event
.
accept
()
# ----------- Points -----------
# ---------- Points ----------
def
_add_point
(
self
,
view_pos
):
"""
Add a removable red dot at the clicked location.
"""
"""
Add a removable red dot at the clicked location
, clamped inside the image
.
"""
scene_pos
=
self
.
mapToScene
(
view_pos
)
x
,
y
=
scene_pos
.
x
(),
scene_pos
.
y
()
dot
=
LabeledPointItem
(
x
,
y
,
label
=
""
,
# no label
radius
=
self
.
dot_radius
,
color
=
Qt
.
red
,
removable
=
True
)
self
.
points
.
append
(
dot
)
dot
=
self
.
_create_point
(
x
,
y
,
label
=
""
,
radius
=
self
.
dot_radius
,
color
=
Qt
.
red
,
removable
=
True
)
# Insert between S and E if they exist
if
len
(
self
.
points
)
>=
2
:
self
.
points
.
insert
(
len
(
self
.
points
)
-
1
,
dot
)
else
:
self
.
points
.
append
(
dot
)
self
.
scene
.
addItem
(
dot
)
def
_remove_point
(
self
,
view_pos
):
"""
Right-click => remove nearest dot if
within threshold, if
it
'
s removable.
"""
"""
Right-click => remove nearest dot if it
'
s removable.
"""
scene_pos
=
self
.
mapToScene
(
view_pos
)
x_click
,
y_click
=
scene_pos
.
x
(),
scene_pos
.
y
()
...
...
@@ -247,20 +297,18 @@ class ImageGraphicsView(QGraphicsView):
closest_idx
=
None
min_dist
=
float
(
'
inf
'
)
for
i
,
p
oint_item
in
enumerate
(
self
.
points
):
dist
=
p
oint_item
.
distance_to
(
x_click
,
y_click
)
for
i
,
p
in
enumerate
(
self
.
points
):
dist
=
p
.
distance_to
(
x_click
,
y_click
)
if
dist
<
min_dist
:
min_dist
=
dist
closest_idx
=
i
# 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
_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
()
...
...
@@ -278,17 +326,12 @@ class ImageGraphicsView(QGraphicsView):
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.
"""
"""
Remove all points if remove_all=True; else just removable 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
():
...
...
@@ -299,13 +342,6 @@ class ImageGraphicsView(QGraphicsView):
class
MainWindow
(
QMainWindow
):
"""
Main window with:
- Load Image
- Editor mode toggle
- Export points
- Clear Points
"""
def
__init__
(
self
):
super
().
__init__
()
self
.
setWindowTitle
(
"
Test GUI
"
)
...
...
@@ -356,7 +392,6 @@ class MainWindow(QMainWindow):
self
.
image_view
.
load_image
(
file_path
)
def
toggle_editor_mode
(
self
):
"""
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
)
...
...
@@ -368,10 +403,6 @@ class MainWindow(QMainWindow):
self
.
btn_editor_mode
.
setStyleSheet
(
"
background-color: lightgray;
"
)
def
export_points
(
self
):
"""
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
...
...
@@ -389,12 +420,9 @@ class MainWindow(QMainWindow):
print
(
f
"
Exported
{
len
(
points_array
)
}
points to
{
file_path
}
"
)
def
clear_points
(
self
):
"""
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
)
window
=
MainWindow
()
...
...
@@ -402,5 +430,5 @@ def main():
sys
.
exit
(
app
.
exec_
())
if
__name__
==
'
__main__
'
:
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