PyImageLabeling 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyImageLabeling/__init__.py +22 -0
- PyImageLabeling/config.json +289 -0
- PyImageLabeling/controller/Controller.py +25 -0
- PyImageLabeling/controller/Events.py +147 -0
- PyImageLabeling/controller/FileEvents.py +69 -0
- PyImageLabeling/controller/ImageEvents.py +32 -0
- PyImageLabeling/controller/LabelEvents.py +219 -0
- PyImageLabeling/controller/LabelingEvents.py +123 -0
- PyImageLabeling/controller/settings/ContourFillinSetting.py +93 -0
- PyImageLabeling/controller/settings/CoutourFillingApplyCancel.py +37 -0
- PyImageLabeling/controller/settings/EraserSetting.py +73 -0
- PyImageLabeling/controller/settings/LabelSetting.py +91 -0
- PyImageLabeling/controller/settings/MagicPenSetting.py +125 -0
- PyImageLabeling/controller/settings/OpacitySetting.py +66 -0
- PyImageLabeling/controller/settings/PaintBrushSetting.py +66 -0
- PyImageLabeling/icons/apply.png +0 -0
- PyImageLabeling/icons/asterisk-green.png +0 -0
- PyImageLabeling/icons/asterisk-red.png +0 -0
- PyImageLabeling/icons/back.png +0 -0
- PyImageLabeling/icons/border.png +0 -0
- PyImageLabeling/icons/cancel.png +0 -0
- PyImageLabeling/icons/cleaner.png +0 -0
- PyImageLabeling/icons/close.png +0 -0
- PyImageLabeling/icons/down.png +0 -0
- PyImageLabeling/icons/ellipse.png +0 -0
- PyImageLabeling/icons/eraser.png +0 -0
- PyImageLabeling/icons/filling.png +0 -0
- PyImageLabeling/icons/logoMAIA.png +0 -0
- PyImageLabeling/icons/magic.png +0 -0
- PyImageLabeling/icons/maia.png +0 -0
- PyImageLabeling/icons/maia1.png +0 -0
- PyImageLabeling/icons/maia3.ico +0 -0
- PyImageLabeling/icons/maia_icon.png +0 -0
- PyImageLabeling/icons/move.png +0 -0
- PyImageLabeling/icons/opacity.png +0 -0
- PyImageLabeling/icons/open_image.png +0 -0
- PyImageLabeling/icons/open_layer.png +0 -0
- PyImageLabeling/icons/paint.png +0 -0
- PyImageLabeling/icons/plus.png +0 -0
- PyImageLabeling/icons/polygon.png +0 -0
- PyImageLabeling/icons/rectangle.png +0 -0
- PyImageLabeling/icons/reset.png +0 -0
- PyImageLabeling/icons/save.png +0 -0
- PyImageLabeling/icons/setting.png +0 -0
- PyImageLabeling/icons/transparency.png:Zone.Identifier +4 -0
- PyImageLabeling/icons/up.png +0 -0
- PyImageLabeling/icons/visibility.png +0 -0
- PyImageLabeling/icons/zoom_minus.png +0 -0
- PyImageLabeling/icons/zoom_plus.png +0 -0
- PyImageLabeling/model/Core.py +795 -0
- PyImageLabeling/model/File/Files.py +166 -0
- PyImageLabeling/model/File/NextImage.py +36 -0
- PyImageLabeling/model/File/PreviousImage.py +19 -0
- PyImageLabeling/model/Image/MoveImage.py +32 -0
- PyImageLabeling/model/Image/ResetMoveZoomImage.py +16 -0
- PyImageLabeling/model/Image/ZoomMinus.py +25 -0
- PyImageLabeling/model/Image/ZoomPlus.py +16 -0
- PyImageLabeling/model/Labeling/ClearAll.py +22 -0
- PyImageLabeling/model/Labeling/ContourFilling.py +135 -0
- PyImageLabeling/model/Labeling/Ellipse.py +350 -0
- PyImageLabeling/model/Labeling/Eraser.py +131 -0
- PyImageLabeling/model/Labeling/MagicPen.py +131 -0
- PyImageLabeling/model/Labeling/PaintBrush.py +207 -0
- PyImageLabeling/model/Labeling/Polygon.py +279 -0
- PyImageLabeling/model/Labeling/Rectangle.py +248 -0
- PyImageLabeling/model/Labeling/Undo.py +12 -0
- PyImageLabeling/model/Model.py +40 -0
- PyImageLabeling/model/Utils.py +40 -0
- PyImageLabeling/old_version/label_rectangle_properties.json +6 -0
- PyImageLabeling/old_version/main.py +2073 -0
- PyImageLabeling/old_version/models/EraseSettingsDialog.py +51 -0
- PyImageLabeling/old_version/models/LabeledRectangle.py +80 -0
- PyImageLabeling/old_version/models/MagicSettingsDialog.py +119 -0
- PyImageLabeling/old_version/models/OverlayOpacityDialog.py +63 -0
- PyImageLabeling/old_version/models/PaintSettingsDialog.py +289 -0
- PyImageLabeling/old_version/models/PointItem.py +66 -0
- PyImageLabeling/old_version/models/ProcessWorker.py +52 -0
- PyImageLabeling/old_version/models/ZoomableGraphicsView.py +1214 -0
- PyImageLabeling/old_version/models/tools/ContourTool.py +279 -0
- PyImageLabeling/old_version/models/tools/EraserTool.py +290 -0
- PyImageLabeling/old_version/models/tools/MagicPenTool.py +199 -0
- PyImageLabeling/old_version/models/tools/OverlayTool.py +179 -0
- PyImageLabeling/old_version/models/tools/PaintTool.py +68 -0
- PyImageLabeling/old_version/models/tools/PolygonTool.py +786 -0
- PyImageLabeling/old_version/models/tools/RectangleTool.py +1036 -0
- PyImageLabeling/parameters.json +1 -0
- PyImageLabeling/style.css +611 -0
- PyImageLabeling/view/Builder.py +333 -0
- PyImageLabeling/view/QBackgroundItem.py +30 -0
- PyImageLabeling/view/QWidgets.py +10 -0
- PyImageLabeling/view/View.py +226 -0
- PyImageLabeling/view/ZoomableGraphicsView.py +91 -0
- PyImageLabeling/view/__init__.py +0 -0
- pyimagelabeling-1.0.0.dist-info/METADATA +55 -0
- pyimagelabeling-1.0.0.dist-info/RECORD +99 -0
- pyimagelabeling-1.0.0.dist-info/WHEEL +5 -0
- pyimagelabeling-1.0.0.dist-info/licenses/LICENCE +22 -0
- pyimagelabeling-1.0.0.dist-info/top_level.txt +2 -0
- pypi/publish_pypi.py +18 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from PyImageLabeling.model.Core import Core
|
|
2
|
+
from PyQt6.QtWidgets import QGraphicsEllipseItem
|
|
3
|
+
from PyQt6.QtGui import QPen, QCursor, QBrush
|
|
4
|
+
from PyQt6.QtCore import Qt, QPointF, QRectF, QSizeF
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
HANDLE_SIZE = 8 # Size of handles for resizing
|
|
8
|
+
HANDLE_DETECTION_DISTANCE = 15 # Distance for auto-showing handles
|
|
9
|
+
|
|
10
|
+
class EllipseItem(QGraphicsEllipseItem):
|
|
11
|
+
def __init__(self, x, y, width, height, color=Qt.GlobalColor.red):
|
|
12
|
+
super().__init__(x, y, width, height)
|
|
13
|
+
|
|
14
|
+
self.pen = QPen(color, 2)
|
|
15
|
+
self.pen.setStyle(Qt.PenStyle.SolidLine)
|
|
16
|
+
self.setPen(self.pen)
|
|
17
|
+
|
|
18
|
+
self.setFlags(
|
|
19
|
+
QGraphicsEllipseItem.GraphicsItemFlag.ItemIsSelectable |
|
|
20
|
+
QGraphicsEllipseItem.GraphicsItemFlag.ItemIsMovable |
|
|
21
|
+
QGraphicsEllipseItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
self.handles = {} # Dictionary to track handles
|
|
25
|
+
self.rotation_handle = None # Rotation handle
|
|
26
|
+
self.handle_selected = None
|
|
27
|
+
self.mouse_press_pos = None
|
|
28
|
+
self.handles_visible = False
|
|
29
|
+
self.initial_rotation = 0
|
|
30
|
+
self.initial_angle = 0
|
|
31
|
+
|
|
32
|
+
# Accept hover events to detect mouse proximity
|
|
33
|
+
self.setAcceptHoverEvents(True)
|
|
34
|
+
|
|
35
|
+
self.update_handles()
|
|
36
|
+
|
|
37
|
+
def get_ellipse_point(self, angle_degrees):
|
|
38
|
+
"""Get a point on the ellipse perimeter at the given angle"""
|
|
39
|
+
rect = self.rect()
|
|
40
|
+
center_x = rect.center().x()
|
|
41
|
+
center_y = rect.center().y()
|
|
42
|
+
|
|
43
|
+
# Semi-major and semi-minor axes
|
|
44
|
+
a = rect.width() / 2 # horizontal radius
|
|
45
|
+
b = rect.height() / 2 # vertical radius
|
|
46
|
+
|
|
47
|
+
# Convert angle to radians
|
|
48
|
+
angle_rad = math.radians(angle_degrees)
|
|
49
|
+
|
|
50
|
+
# Parametric equations for ellipse
|
|
51
|
+
x = center_x + a * math.cos(angle_rad)
|
|
52
|
+
y = center_y + b * math.sin(angle_rad)
|
|
53
|
+
|
|
54
|
+
return QPointF(x, y)
|
|
55
|
+
|
|
56
|
+
def update_handles(self):
|
|
57
|
+
"""Update handle positions on the ellipse perimeter"""
|
|
58
|
+
rect = self.rect()
|
|
59
|
+
|
|
60
|
+
# Place handles at 0°, 90°, 180°, 270° on the ellipse perimeter
|
|
61
|
+
right_point = self.get_ellipse_point(0) # Right (0°)
|
|
62
|
+
bottom_point = self.get_ellipse_point(90) # Bottom (90°)
|
|
63
|
+
left_point = self.get_ellipse_point(180) # Left (180°)
|
|
64
|
+
top_point = self.get_ellipse_point(270) # Top (270°)
|
|
65
|
+
|
|
66
|
+
# Create handle rectangles centered on these points
|
|
67
|
+
self.handles = {
|
|
68
|
+
'right': QRectF(right_point - QPointF(HANDLE_SIZE/2, HANDLE_SIZE/2), QSizeF(HANDLE_SIZE, HANDLE_SIZE)),
|
|
69
|
+
'bottom': QRectF(bottom_point - QPointF(HANDLE_SIZE/2, HANDLE_SIZE/2), QSizeF(HANDLE_SIZE, HANDLE_SIZE)),
|
|
70
|
+
'left': QRectF(left_point - QPointF(HANDLE_SIZE/2, HANDLE_SIZE/2), QSizeF(HANDLE_SIZE, HANDLE_SIZE)),
|
|
71
|
+
'top': QRectF(top_point - QPointF(HANDLE_SIZE/2, HANDLE_SIZE/2), QSizeF(HANDLE_SIZE, HANDLE_SIZE)),
|
|
72
|
+
'rotation': QRectF(QPointF(rect.center()) - QPointF(HANDLE_SIZE/2, HANDLE_SIZE/2), QSizeF(HANDLE_SIZE, HANDLE_SIZE))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def hoverEnterEvent(self, event):
|
|
76
|
+
"""Mouse entered the item area"""
|
|
77
|
+
self.check_handle_proximity(event.pos())
|
|
78
|
+
super().hoverEnterEvent(event)
|
|
79
|
+
|
|
80
|
+
def hoverMoveEvent(self, event):
|
|
81
|
+
"""Mouse moved within the item area"""
|
|
82
|
+
self.check_handle_proximity(event.pos())
|
|
83
|
+
self.update_cursor(event.pos())
|
|
84
|
+
super().hoverMoveEvent(event)
|
|
85
|
+
|
|
86
|
+
def hoverLeaveEvent(self, event):
|
|
87
|
+
"""Mouse left the item area"""
|
|
88
|
+
self.handles_visible = False
|
|
89
|
+
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
90
|
+
self.update()
|
|
91
|
+
super().hoverLeaveEvent(event)
|
|
92
|
+
|
|
93
|
+
def check_handle_proximity(self, pos):
|
|
94
|
+
"""Check if mouse is near any handle and make them visible"""
|
|
95
|
+
near_handle = False
|
|
96
|
+
|
|
97
|
+
# Check handles
|
|
98
|
+
for handle_rect in self.handles.values():
|
|
99
|
+
if self.distance_to_rect(pos, handle_rect) < HANDLE_DETECTION_DISTANCE:
|
|
100
|
+
near_handle = True
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
# Also show handles when selected
|
|
104
|
+
if self.isSelected():
|
|
105
|
+
near_handle = True
|
|
106
|
+
|
|
107
|
+
if near_handle != self.handles_visible:
|
|
108
|
+
self.handles_visible = near_handle
|
|
109
|
+
self.update()
|
|
110
|
+
|
|
111
|
+
def distance_to_rect(self, point, rect):
|
|
112
|
+
"""Calculate distance from point to rectangle"""
|
|
113
|
+
center = rect.center()
|
|
114
|
+
dx = abs(point.x() - center.x())
|
|
115
|
+
dy = abs(point.y() - center.y())
|
|
116
|
+
return math.sqrt(dx*dx + dy*dy)
|
|
117
|
+
|
|
118
|
+
def update_cursor(self, pos):
|
|
119
|
+
"""Update cursor based on which handle is under mouse"""
|
|
120
|
+
if not self.handles_visible:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Check resize handles
|
|
124
|
+
for name, rect in self.handles.items():
|
|
125
|
+
if rect.contains(pos):
|
|
126
|
+
if name in ['rotation']:
|
|
127
|
+
self.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
128
|
+
elif name in ['right', 'left']:
|
|
129
|
+
self.setCursor(Qt.CursorShape.SizeHorCursor)
|
|
130
|
+
elif name in ['top', 'bottom']:
|
|
131
|
+
self.setCursor(Qt.CursorShape.SizeVerCursor)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
self.setCursor(Qt.CursorShape.SizeAllCursor)
|
|
135
|
+
|
|
136
|
+
def paint(self, painter, option, widget=None):
|
|
137
|
+
super().paint(painter, option, widget)
|
|
138
|
+
|
|
139
|
+
if self.handles_visible:
|
|
140
|
+
# Draw resize handles
|
|
141
|
+
painter.setPen(QPen(Qt.GlobalColor.black, 1, Qt.PenStyle.SolidLine))
|
|
142
|
+
painter.setBrush(QBrush(Qt.GlobalColor.white, Qt.BrushStyle.SolidPattern))
|
|
143
|
+
|
|
144
|
+
# Draw square handles for resize
|
|
145
|
+
for name, handle_rect in self.handles.items():
|
|
146
|
+
if name != 'rotation':
|
|
147
|
+
painter.drawRect(handle_rect)
|
|
148
|
+
|
|
149
|
+
# Draw rotation handle (circular) at center
|
|
150
|
+
if self.handles['rotation']:
|
|
151
|
+
painter.setPen(QPen(Qt.GlobalColor.blue, 2, Qt.PenStyle.SolidLine))
|
|
152
|
+
painter.setBrush(QBrush(Qt.GlobalColor.blue, Qt.BrushStyle.SolidPattern))
|
|
153
|
+
painter.drawEllipse(self.handles['rotation'])
|
|
154
|
+
|
|
155
|
+
def mousePressEvent(self, event):
|
|
156
|
+
self.handle_selected = None
|
|
157
|
+
self.mouse_press_pos = event.pos()
|
|
158
|
+
|
|
159
|
+
if not self.handles_visible:
|
|
160
|
+
super().mousePressEvent(event)
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# Check handles
|
|
164
|
+
for name, rect in self.handles.items():
|
|
165
|
+
if rect.contains(event.pos()):
|
|
166
|
+
self.handle_selected = name
|
|
167
|
+
if name == 'rotation':
|
|
168
|
+
# Store initial rotation data for rotation handle
|
|
169
|
+
rect_center = self.rect().center()
|
|
170
|
+
rect_center_scene = self.mapToScene(rect_center)
|
|
171
|
+
mouse_scene_pos = self.mapToScene(event.pos())
|
|
172
|
+
self.initial_rotation = math.atan2(
|
|
173
|
+
mouse_scene_pos.y() - rect_center_scene.y(),
|
|
174
|
+
mouse_scene_pos.x() - rect_center_scene.x()
|
|
175
|
+
)
|
|
176
|
+
self.initial_angle = self.rotation()
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
super().mousePressEvent(event)
|
|
180
|
+
|
|
181
|
+
def mouseMoveEvent(self, event):
|
|
182
|
+
if self.handle_selected == 'rotation':
|
|
183
|
+
# Handle rotation
|
|
184
|
+
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
185
|
+
self.handles_visible = False
|
|
186
|
+
rect_center = self.rect().center()
|
|
187
|
+
self.setTransformOriginPoint(rect_center)
|
|
188
|
+
|
|
189
|
+
# Map center to scene coordinates
|
|
190
|
+
rect_center_scene = self.mapToScene(rect_center)
|
|
191
|
+
|
|
192
|
+
# Calculate current mouse angle relative to ellipse center
|
|
193
|
+
mouse_scene_pos = self.mapToScene(event.pos())
|
|
194
|
+
current_mouse_angle = math.atan2(
|
|
195
|
+
mouse_scene_pos.y() - rect_center_scene.y(),
|
|
196
|
+
mouse_scene_pos.x() - rect_center_scene.x()
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Calculate angle difference and apply rotation
|
|
200
|
+
angle_diff = math.degrees(current_mouse_angle - self.initial_rotation)
|
|
201
|
+
new_rotation = self.initial_angle + angle_diff
|
|
202
|
+
self.setRotation(new_rotation)
|
|
203
|
+
|
|
204
|
+
self.update_handles()
|
|
205
|
+
self.update()
|
|
206
|
+
|
|
207
|
+
elif self.handle_selected and self.handle_selected != 'rotation':
|
|
208
|
+
# Handle resizing
|
|
209
|
+
pos = event.pos()
|
|
210
|
+
rect = self.rect()
|
|
211
|
+
self.handles_visible = False
|
|
212
|
+
center = rect.center()
|
|
213
|
+
|
|
214
|
+
if self.handle_selected == 'right':
|
|
215
|
+
# Resize horizontally from right edge
|
|
216
|
+
new_width = 2 * abs(pos.x() - center.x())
|
|
217
|
+
new_width = max(10, new_width) # Minimum width
|
|
218
|
+
rect.setWidth(new_width)
|
|
219
|
+
rect.moveCenter(center)
|
|
220
|
+
|
|
221
|
+
elif self.handle_selected == 'left':
|
|
222
|
+
# Resize horizontally from left edge
|
|
223
|
+
new_width = 2 * abs(pos.x() - center.x())
|
|
224
|
+
new_width = max(10, new_width) # Minimum width
|
|
225
|
+
rect.setWidth(new_width)
|
|
226
|
+
rect.moveCenter(center)
|
|
227
|
+
|
|
228
|
+
elif self.handle_selected == 'bottom':
|
|
229
|
+
# Resize vertically from bottom edge
|
|
230
|
+
new_height = 2 * abs(pos.y() - center.y())
|
|
231
|
+
new_height = max(10, new_height) # Minimum height
|
|
232
|
+
rect.setHeight(new_height)
|
|
233
|
+
rect.moveCenter(center)
|
|
234
|
+
|
|
235
|
+
elif self.handle_selected == 'top':
|
|
236
|
+
# Resize vertically from top edge
|
|
237
|
+
new_height = 2 * abs(pos.y() - center.y())
|
|
238
|
+
new_height = max(10, new_height) # Minimum height
|
|
239
|
+
rect.setHeight(new_height)
|
|
240
|
+
rect.moveCenter(center)
|
|
241
|
+
|
|
242
|
+
self.setRect(rect)
|
|
243
|
+
self.update_handles()
|
|
244
|
+
self.update() # Force repaint to hide handles during resizing
|
|
245
|
+
else:
|
|
246
|
+
super().mouseMoveEvent(event)
|
|
247
|
+
|
|
248
|
+
def mouseReleaseEvent(self, event):
|
|
249
|
+
if self.handle_selected == 'rotation':
|
|
250
|
+
self.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
251
|
+
event.accept()
|
|
252
|
+
self.handle_selected = None
|
|
253
|
+
self.handles_visible = True
|
|
254
|
+
self.update()
|
|
255
|
+
|
|
256
|
+
# Only call super if we didn't handle rotation
|
|
257
|
+
if not (self.handle_selected == 'rotation' or event.isAccepted()):
|
|
258
|
+
super().mouseReleaseEvent(event)
|
|
259
|
+
|
|
260
|
+
class Ellipse(Core):
|
|
261
|
+
def __init__(self):
|
|
262
|
+
super().__init__()
|
|
263
|
+
self.first_click_pos = None
|
|
264
|
+
self.current_ellipse = None
|
|
265
|
+
self.is_drawing = False
|
|
266
|
+
self.selected_ellipse = None
|
|
267
|
+
|
|
268
|
+
def ellipse(self):
|
|
269
|
+
self.checked_button = self.ellipse.__name__
|
|
270
|
+
self.zoomable_graphics_view.scene.selectionChanged.connect(self.update_selected_ellipse)
|
|
271
|
+
|
|
272
|
+
def cleanup_temporary_ellipses(self):
|
|
273
|
+
"""Remove preview ellipses"""
|
|
274
|
+
if self.current_ellipse:
|
|
275
|
+
if self.current_ellipse in self.zoomable_graphics_view.scene.items():
|
|
276
|
+
self.zoomable_graphics_view.scene.removeItem(self.current_ellipse)
|
|
277
|
+
self.current_ellipse = None
|
|
278
|
+
|
|
279
|
+
def start_ellipse_tool(self, current_position):
|
|
280
|
+
"""Mouse press → start drawing"""
|
|
281
|
+
self.zoomable_graphics_view.change_cursor("ellipse")
|
|
282
|
+
self.cleanup_temporary_ellipses()
|
|
283
|
+
|
|
284
|
+
self.first_click_pos = QPointF(current_position.x(), current_position.y())
|
|
285
|
+
self.color = self.get_labeling_overlay().get_color()
|
|
286
|
+
self.is_drawing = True
|
|
287
|
+
|
|
288
|
+
# Preview ellipse
|
|
289
|
+
self.current_ellipse = QGraphicsEllipseItem(
|
|
290
|
+
self.first_click_pos.x(),
|
|
291
|
+
self.first_click_pos.y(),
|
|
292
|
+
1, 1
|
|
293
|
+
)
|
|
294
|
+
pen = QPen(self.color, 2)
|
|
295
|
+
pen.setStyle(Qt.PenStyle.DashLine)
|
|
296
|
+
self.current_ellipse.setPen(pen)
|
|
297
|
+
self.current_ellipse.setZValue(2)
|
|
298
|
+
self.zoomable_graphics_view.scene.addItem(self.current_ellipse)
|
|
299
|
+
|
|
300
|
+
def move_ellipse_tool(self, current_position):
|
|
301
|
+
"""Mouse move → resize preview ellipse"""
|
|
302
|
+
if not self.is_drawing or not self.current_ellipse:
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
current_pos = QPointF(current_position.x(), current_position.y())
|
|
306
|
+
x = min(self.first_click_pos.x(), current_pos.x())
|
|
307
|
+
y = min(self.first_click_pos.y(), current_pos.y())
|
|
308
|
+
w = abs(current_pos.x() - self.first_click_pos.x())
|
|
309
|
+
h = abs(current_pos.y() - self.first_click_pos.y())
|
|
310
|
+
|
|
311
|
+
self.current_ellipse.setRect(x, y, w, h)
|
|
312
|
+
|
|
313
|
+
def end_ellipse_tool(self):
|
|
314
|
+
"""Mouse release → finalize ellipse"""
|
|
315
|
+
if not self.is_drawing or not self.current_ellipse:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
rect = self.current_ellipse.rect()
|
|
319
|
+
self.cleanup_temporary_ellipses()
|
|
320
|
+
|
|
321
|
+
if rect.width() > 5 and rect.height() > 5:
|
|
322
|
+
final_ellipse = EllipseItem(
|
|
323
|
+
rect.x(), rect.y(),
|
|
324
|
+
rect.width(), rect.height(),
|
|
325
|
+
self.color
|
|
326
|
+
)
|
|
327
|
+
final_ellipse.setZValue(2)
|
|
328
|
+
final_ellipse.setFlag(QGraphicsEllipseItem.GraphicsItemFlag.ItemIsSelectable, True)
|
|
329
|
+
self.zoomable_graphics_view.scene.addItem(final_ellipse)
|
|
330
|
+
self.selected_ellipse = final_ellipse
|
|
331
|
+
|
|
332
|
+
self.first_click_pos = None
|
|
333
|
+
self.is_drawing = False
|
|
334
|
+
|
|
335
|
+
def update_selected_ellipse(self):
|
|
336
|
+
"""Update selected_ellipse when user clicks on an ellipse"""
|
|
337
|
+
selected_items = self.zoomable_graphics_view.scene.selectedItems()
|
|
338
|
+
if selected_items:
|
|
339
|
+
item = selected_items[-1] # last selected item
|
|
340
|
+
if isinstance(item, EllipseItem):
|
|
341
|
+
self.selected_ellipse = item
|
|
342
|
+
else:
|
|
343
|
+
self.selected_ellipse = None
|
|
344
|
+
|
|
345
|
+
def clear_ellipse(self):
|
|
346
|
+
"""Remove the currently selected ellipse from the scene"""
|
|
347
|
+
if self.selected_ellipse:
|
|
348
|
+
if self.selected_ellipse in self.zoomable_graphics_view.scene.items():
|
|
349
|
+
self.view.zoomable_graphics_view.scene.removeItem(self.selected_ellipse)
|
|
350
|
+
self.selected_ellipse = None
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from PyQt6.QtCore import Qt, QPointF, QRectF, QPoint, QRect
|
|
2
|
+
from PyQt6.QtGui import QPixmap, QPainter, QBrush, QColor, QPainterPath, QPen
|
|
3
|
+
from PyQt6.QtWidgets import QGraphicsEllipseItem, QGraphicsPathItem, QGraphicsItem
|
|
4
|
+
from PyImageLabeling.model.Core import Core
|
|
5
|
+
import math
|
|
6
|
+
from PyImageLabeling.model.Utils import Utils
|
|
7
|
+
|
|
8
|
+
class EraserBrushItem(QGraphicsItem):
|
|
9
|
+
|
|
10
|
+
def __init__(self, core, x, y, color, size, absolute_mode):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.core = core
|
|
13
|
+
self.x = x
|
|
14
|
+
self.y = y
|
|
15
|
+
self.color = color
|
|
16
|
+
self.size = size
|
|
17
|
+
self.absolute_mode = absolute_mode
|
|
18
|
+
self.labeling_overlay_painter = self.core.get_current_image_item().get_labeling_overlay().get_painter()
|
|
19
|
+
self.image_pixmap = self.core.get_current_image_item().get_image_pixmap()
|
|
20
|
+
|
|
21
|
+
# Compute the good qrect to avoid going beyond the painting area
|
|
22
|
+
self.qrectf = QRectF(int(self.x)-(self.size/2)-5, int(self.y)-(self.size/2)-5, self.size+10, self.size+10)
|
|
23
|
+
self.qrectf = self.qrectf.intersected(core.get_current_image_item().get_qrectf())
|
|
24
|
+
alpha_color = Utils.load_parameters()["load_image"]["alpha_color"]
|
|
25
|
+
|
|
26
|
+
# Create a fake texture with the good image inside
|
|
27
|
+
self.eraser_texture = QPixmap(self.size, self.size)
|
|
28
|
+
self.eraser_texture.fill(QColor(*alpha_color))
|
|
29
|
+
|
|
30
|
+
painter = QPainter(self.eraser_texture)
|
|
31
|
+
|
|
32
|
+
painter.drawPixmap(QRect(0, 0, self.size, self.size), self.image_pixmap, QRect(int(self.x-(self.size/2)), int(self.y-(self.size/2)), self.size, self.size))
|
|
33
|
+
painter.setOpacity(self.core.get_current_image_item().get_labeling_overlay().get_opacity())
|
|
34
|
+
for other_labeling_overlay_pixmap in self.core.get_current_image_item().get_labeling_overlay_pixmaps():
|
|
35
|
+
painter.drawPixmap(QRect(0, 0, self.size, self.size), other_labeling_overlay_pixmap, QRect(int(self.x-(self.size/2)), int(self.y-(self.size/2)), self.size, self.size))
|
|
36
|
+
|
|
37
|
+
painter.end()
|
|
38
|
+
|
|
39
|
+
# Use the fake texture as a QBrush texture of a draw point
|
|
40
|
+
self.eraser_pixmap = QPixmap(self.size, self.size)
|
|
41
|
+
self.eraser_pixmap.fill(Qt.GlobalColor.transparent)
|
|
42
|
+
|
|
43
|
+
painter = QPainter(self.eraser_pixmap)
|
|
44
|
+
self.qbrush = QBrush()
|
|
45
|
+
self.qbrush.setTexture(self.eraser_texture)
|
|
46
|
+
self.pen = QPen(self.qbrush, self.size)
|
|
47
|
+
self.pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
48
|
+
painter.setPen(self.pen)
|
|
49
|
+
painter.drawPoint(int(self.size/2), int(self.size/2))
|
|
50
|
+
painter.end()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def boundingRect(self):
|
|
55
|
+
return self.qrectf
|
|
56
|
+
|
|
57
|
+
def paint(self, painter, option, widget):
|
|
58
|
+
painter.drawPixmap(int(self.x-(self.size/2)), int(self.y-(self.size/2)), self.eraser_pixmap)
|
|
59
|
+
|
|
60
|
+
pen = QPen(Qt.GlobalColor.black, self.size)
|
|
61
|
+
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
62
|
+
|
|
63
|
+
if self.absolute_mode == 1:
|
|
64
|
+
# Apply eraser to all labeling overlays
|
|
65
|
+
for labeling_overlay in self.core.get_current_image_item().get_labeling_overlays():
|
|
66
|
+
overlay_painter = labeling_overlay.get_painter()
|
|
67
|
+
overlay_painter.setPen(pen)
|
|
68
|
+
overlay_painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
|
|
69
|
+
overlay_painter.drawPoint(int(self.x), int(self.y))
|
|
70
|
+
else:
|
|
71
|
+
# Apply eraser only to current labeling overlay
|
|
72
|
+
self.labeling_overlay_painter.setPen(pen)
|
|
73
|
+
self.labeling_overlay_painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
|
|
74
|
+
self.labeling_overlay_painter.drawPoint(int(self.x), int(self.y))
|
|
75
|
+
|
|
76
|
+
class Eraser(Core):
|
|
77
|
+
def __init__(self):
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.last_position_x, self.last_position_y = None, None
|
|
80
|
+
self.point_spacing = 2
|
|
81
|
+
self.eraser_brush_items = []
|
|
82
|
+
|
|
83
|
+
def eraser(self):
|
|
84
|
+
self.checked_button = self.eraser.__name__
|
|
85
|
+
|
|
86
|
+
def start_eraser(self, current_position):
|
|
87
|
+
self.view.zoomable_graphics_view.change_cursor("eraser")
|
|
88
|
+
|
|
89
|
+
self.current_position_x = int(current_position.x())
|
|
90
|
+
self.current_position_y = int(current_position.y())
|
|
91
|
+
|
|
92
|
+
self.size_eraser_brush = Utils.load_parameters()["eraser"]["size"]
|
|
93
|
+
self.absolute_mode = Utils.load_parameters()["eraser"]["absolute_mode"]
|
|
94
|
+
self.color = self.get_current_label_item().get_color()
|
|
95
|
+
|
|
96
|
+
eraser_brush_item = EraserBrushItem(self, self.current_position_x, self.current_position_y, self.color, self.size_eraser_brush, self.absolute_mode)
|
|
97
|
+
eraser_brush_item.setZValue(4) # To place in the top of the item
|
|
98
|
+
self.zoomable_graphics_view.scene.addItem(eraser_brush_item) # update is already call in this method
|
|
99
|
+
self.eraser_brush_items.append(eraser_brush_item)
|
|
100
|
+
|
|
101
|
+
self.last_position_x, self.last_position_y = self.current_position_x, self.current_position_y
|
|
102
|
+
|
|
103
|
+
def move_eraser(self, current_position):
|
|
104
|
+
self.current_position_x = int(current_position.x())
|
|
105
|
+
self.current_position_y = int(current_position.y())
|
|
106
|
+
|
|
107
|
+
if Utils.compute_diagonal(self.current_position_x, self.current_position_y, self.last_position_x, self.last_position_y) < self.point_spacing:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
eraser_brush_item = EraserBrushItem(self, self.current_position_x, self.current_position_y, self.color, self.size_eraser_brush, self.absolute_mode)
|
|
111
|
+
eraser_brush_item.setZValue(4) # To place in the top of the item
|
|
112
|
+
self.zoomable_graphics_view.scene.addItem(eraser_brush_item) # update is already call in this method
|
|
113
|
+
self.eraser_brush_items.append(eraser_brush_item)
|
|
114
|
+
|
|
115
|
+
self.last_position_x, self.last_position_y = self.current_position_x, self.current_position_y
|
|
116
|
+
|
|
117
|
+
def end_eraser(self):
|
|
118
|
+
# Remove the dislay of all these item
|
|
119
|
+
for item in self.eraser_brush_items:
|
|
120
|
+
self.zoomable_graphics_view.scene.removeItem(item)
|
|
121
|
+
self.eraser_brush_items.clear()
|
|
122
|
+
|
|
123
|
+
if self.absolute_mode == 1:
|
|
124
|
+
# Update all labeling overlays
|
|
125
|
+
for labeling_overlay in self.get_current_image_item().get_labeling_overlays():
|
|
126
|
+
labeling_overlay.update()
|
|
127
|
+
labeling_overlay.reset_pen()
|
|
128
|
+
else:
|
|
129
|
+
# Update only current labeling overlay
|
|
130
|
+
self.get_current_image_item().update_labeling_overlay()
|
|
131
|
+
self.get_current_image_item().get_labeling_overlay().reset_pen()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from PyQt6.QtCore import Qt, QRectF
|
|
2
|
+
from PyImageLabeling.model.Core import Core
|
|
3
|
+
from PyQt6.QtGui import QColor, QPixmap, QBrush, QPainter, QBitmap, QColorConstants, QImage
|
|
4
|
+
from PyQt6.QtWidgets import QProgressDialog, QApplication, QMessageBox
|
|
5
|
+
from collections import deque
|
|
6
|
+
|
|
7
|
+
import numpy
|
|
8
|
+
import matplotlib
|
|
9
|
+
|
|
10
|
+
from PyImageLabeling.model.Utils import Utils
|
|
11
|
+
#DIRECTIONS = ((1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, -1), (1, -1), (-1, 1))
|
|
12
|
+
|
|
13
|
+
DIRECTIONS = ((1, 0), (-1, 0), (0, 1), (0, -1))
|
|
14
|
+
|
|
15
|
+
class MagicPen(Core):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__()
|
|
18
|
+
|
|
19
|
+
def magic_pen(self):
|
|
20
|
+
self.checked_button = self.magic_pen.__name__
|
|
21
|
+
print("magic")
|
|
22
|
+
|
|
23
|
+
def start_magic_pen(self, scene_pos):
|
|
24
|
+
"""Fill area with points using magic pen"""
|
|
25
|
+
self.view.zoomable_graphics_view.change_cursor("magic")
|
|
26
|
+
self.fill_shape(scene_pos)
|
|
27
|
+
|
|
28
|
+
def fill_shape(self, scene_pos):
|
|
29
|
+
# Create progress dialog
|
|
30
|
+
self.view.progressBar.reset()
|
|
31
|
+
|
|
32
|
+
self._fill_shape_worker(scene_pos)
|
|
33
|
+
self.get_current_image_item().update_labeling_overlay()
|
|
34
|
+
|
|
35
|
+
def _fill_shape_worker(self, scene_pos):
|
|
36
|
+
#Create some variables
|
|
37
|
+
initial_position_x, initial_position_y = int(scene_pos.x()), int(scene_pos.y())
|
|
38
|
+
width, height = self.get_current_image_item().get_width(), self.get_current_image_item().get_height()
|
|
39
|
+
if not (0 <= initial_position_x < width and 0 <= initial_position_y < height): return None
|
|
40
|
+
|
|
41
|
+
self.numpy_pixels_rgb = self.get_current_image_item().get_image_numpy_pixels_rgb()
|
|
42
|
+
|
|
43
|
+
#Get parameters
|
|
44
|
+
tolerance = Utils.load_parameters()["magic_pen"]["tolerance"]
|
|
45
|
+
max_pixels = Utils.load_parameters()["magic_pen"]["max_pixels"]
|
|
46
|
+
method = Utils.load_parameters()["magic_pen"]["method"]
|
|
47
|
+
|
|
48
|
+
#Initialize some data
|
|
49
|
+
visited = numpy.full((width, height), False)
|
|
50
|
+
|
|
51
|
+
#Call the good method
|
|
52
|
+
if method == "HSV":
|
|
53
|
+
return self._fill_shape_hsv(visited, initial_position_x, initial_position_y, width, height, tolerance, max_pixels)
|
|
54
|
+
elif method == "RGB":
|
|
55
|
+
return self._fill_shape_rgb(visited, initial_position_x, initial_position_y, width, height, tolerance, max_pixels)
|
|
56
|
+
else:
|
|
57
|
+
raise NotImplementedError("Mathod not implmented: "+str(method))
|
|
58
|
+
print("MagicPen: image created")
|
|
59
|
+
|
|
60
|
+
def _fill_shape_rgb(self, visited, initial_position_x, initial_position_y, width, height, tolerance, max_pixels):
|
|
61
|
+
#target_color = QColor(self.raw_image.pixel(initial_position_x, initial_position_y))
|
|
62
|
+
target_rgb = self.numpy_pixels_rgb[initial_position_y, initial_position_x].astype(int)
|
|
63
|
+
queue = deque()
|
|
64
|
+
|
|
65
|
+
if (0 <= initial_position_x < width and 0 <= initial_position_y < height):
|
|
66
|
+
queue.append((initial_position_x, initial_position_y))
|
|
67
|
+
|
|
68
|
+
n_pixels = 0
|
|
69
|
+
while queue and n_pixels <= max_pixels:
|
|
70
|
+
x, y = queue.popleft()
|
|
71
|
+
if visited[x][y] == True: continue
|
|
72
|
+
visited[x][y] = True
|
|
73
|
+
current_rgb = self.numpy_pixels_rgb[y, x].astype(int)
|
|
74
|
+
dist = numpy.mean(100-numpy.divide(numpy.multiply(numpy.abs(target_rgb-current_rgb), 100), 255))
|
|
75
|
+
|
|
76
|
+
if dist < tolerance: continue
|
|
77
|
+
|
|
78
|
+
#Color the new_overlay
|
|
79
|
+
#self.labeling_overlay.setPixel(x, y, 1)
|
|
80
|
+
self.get_current_image_item().get_labeling_overlay().get_painter().drawPoint(x, y)
|
|
81
|
+
n_pixels += 1
|
|
82
|
+
|
|
83
|
+
# Add neighbors
|
|
84
|
+
for dx, dy in DIRECTIONS:
|
|
85
|
+
new_x, new_y = x + dx, y + dy
|
|
86
|
+
if (0 <= new_x < width and 0 <= new_y < height):
|
|
87
|
+
queue.append((new_x, new_y))
|
|
88
|
+
|
|
89
|
+
print("MagicPen: end n_pixels:", n_pixels)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _fill_shape_hsv(self, visited, initial_position_x, initial_position_y, width, height, tolerance, max_pixels):
|
|
93
|
+
#Convertion HSV is to slow: an optimization to do is to use openCv2 to store an HSV matrix in LoadImage.
|
|
94
|
+
|
|
95
|
+
#target_color = QColor(self.raw_image.pixel(initial_position_x, initial_position_y))
|
|
96
|
+
target_hsv = matplotlib.colors.rgb_to_hsv(numpy.divide(self.numpy_pixels_rgb[initial_position_y, initial_position_x].astype(float), 255))
|
|
97
|
+
queue = deque()
|
|
98
|
+
|
|
99
|
+
if (0 <= initial_position_x < width and 0 <= initial_position_y < height):
|
|
100
|
+
queue.append((initial_position_x, initial_position_y))
|
|
101
|
+
|
|
102
|
+
n_pixels = 0
|
|
103
|
+
while queue and n_pixels <= max_pixels:
|
|
104
|
+
x, y = queue.popleft()
|
|
105
|
+
if visited[x][y] == True: continue
|
|
106
|
+
visited[x][y] = True
|
|
107
|
+
current_hsv = matplotlib.colors.rgb_to_hsv(numpy.divide(self.numpy_pixels_rgb[y, x].astype(float), 255))
|
|
108
|
+
#print("target_hsv:", target_hsv)
|
|
109
|
+
#print("current_hsv:", current_hsv)
|
|
110
|
+
|
|
111
|
+
dist = numpy.mean(100-numpy.multiply(numpy.abs(target_hsv-current_hsv), 100))
|
|
112
|
+
#print("dist:", dist)
|
|
113
|
+
if dist < tolerance: continue
|
|
114
|
+
|
|
115
|
+
#Color the new_overlay
|
|
116
|
+
self.get_current_image_item().get_labeling_overlay().get_painter().drawPoint(x, y)
|
|
117
|
+
n_pixels += 1
|
|
118
|
+
|
|
119
|
+
# Add neighbors
|
|
120
|
+
for dx, dy in DIRECTIONS:
|
|
121
|
+
new_x, new_y = x + dx, y + dy
|
|
122
|
+
if (0 <= new_x < width and 0 <= new_y < height):
|
|
123
|
+
queue.append((new_x, new_y))
|
|
124
|
+
|
|
125
|
+
print("MagicPen: end n_pixels:", n_pixels)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|