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.
Files changed (99) hide show
  1. PyImageLabeling/__init__.py +22 -0
  2. PyImageLabeling/config.json +289 -0
  3. PyImageLabeling/controller/Controller.py +25 -0
  4. PyImageLabeling/controller/Events.py +147 -0
  5. PyImageLabeling/controller/FileEvents.py +69 -0
  6. PyImageLabeling/controller/ImageEvents.py +32 -0
  7. PyImageLabeling/controller/LabelEvents.py +219 -0
  8. PyImageLabeling/controller/LabelingEvents.py +123 -0
  9. PyImageLabeling/controller/settings/ContourFillinSetting.py +93 -0
  10. PyImageLabeling/controller/settings/CoutourFillingApplyCancel.py +37 -0
  11. PyImageLabeling/controller/settings/EraserSetting.py +73 -0
  12. PyImageLabeling/controller/settings/LabelSetting.py +91 -0
  13. PyImageLabeling/controller/settings/MagicPenSetting.py +125 -0
  14. PyImageLabeling/controller/settings/OpacitySetting.py +66 -0
  15. PyImageLabeling/controller/settings/PaintBrushSetting.py +66 -0
  16. PyImageLabeling/icons/apply.png +0 -0
  17. PyImageLabeling/icons/asterisk-green.png +0 -0
  18. PyImageLabeling/icons/asterisk-red.png +0 -0
  19. PyImageLabeling/icons/back.png +0 -0
  20. PyImageLabeling/icons/border.png +0 -0
  21. PyImageLabeling/icons/cancel.png +0 -0
  22. PyImageLabeling/icons/cleaner.png +0 -0
  23. PyImageLabeling/icons/close.png +0 -0
  24. PyImageLabeling/icons/down.png +0 -0
  25. PyImageLabeling/icons/ellipse.png +0 -0
  26. PyImageLabeling/icons/eraser.png +0 -0
  27. PyImageLabeling/icons/filling.png +0 -0
  28. PyImageLabeling/icons/logoMAIA.png +0 -0
  29. PyImageLabeling/icons/magic.png +0 -0
  30. PyImageLabeling/icons/maia.png +0 -0
  31. PyImageLabeling/icons/maia1.png +0 -0
  32. PyImageLabeling/icons/maia3.ico +0 -0
  33. PyImageLabeling/icons/maia_icon.png +0 -0
  34. PyImageLabeling/icons/move.png +0 -0
  35. PyImageLabeling/icons/opacity.png +0 -0
  36. PyImageLabeling/icons/open_image.png +0 -0
  37. PyImageLabeling/icons/open_layer.png +0 -0
  38. PyImageLabeling/icons/paint.png +0 -0
  39. PyImageLabeling/icons/plus.png +0 -0
  40. PyImageLabeling/icons/polygon.png +0 -0
  41. PyImageLabeling/icons/rectangle.png +0 -0
  42. PyImageLabeling/icons/reset.png +0 -0
  43. PyImageLabeling/icons/save.png +0 -0
  44. PyImageLabeling/icons/setting.png +0 -0
  45. PyImageLabeling/icons/transparency.png:Zone.Identifier +4 -0
  46. PyImageLabeling/icons/up.png +0 -0
  47. PyImageLabeling/icons/visibility.png +0 -0
  48. PyImageLabeling/icons/zoom_minus.png +0 -0
  49. PyImageLabeling/icons/zoom_plus.png +0 -0
  50. PyImageLabeling/model/Core.py +795 -0
  51. PyImageLabeling/model/File/Files.py +166 -0
  52. PyImageLabeling/model/File/NextImage.py +36 -0
  53. PyImageLabeling/model/File/PreviousImage.py +19 -0
  54. PyImageLabeling/model/Image/MoveImage.py +32 -0
  55. PyImageLabeling/model/Image/ResetMoveZoomImage.py +16 -0
  56. PyImageLabeling/model/Image/ZoomMinus.py +25 -0
  57. PyImageLabeling/model/Image/ZoomPlus.py +16 -0
  58. PyImageLabeling/model/Labeling/ClearAll.py +22 -0
  59. PyImageLabeling/model/Labeling/ContourFilling.py +135 -0
  60. PyImageLabeling/model/Labeling/Ellipse.py +350 -0
  61. PyImageLabeling/model/Labeling/Eraser.py +131 -0
  62. PyImageLabeling/model/Labeling/MagicPen.py +131 -0
  63. PyImageLabeling/model/Labeling/PaintBrush.py +207 -0
  64. PyImageLabeling/model/Labeling/Polygon.py +279 -0
  65. PyImageLabeling/model/Labeling/Rectangle.py +248 -0
  66. PyImageLabeling/model/Labeling/Undo.py +12 -0
  67. PyImageLabeling/model/Model.py +40 -0
  68. PyImageLabeling/model/Utils.py +40 -0
  69. PyImageLabeling/old_version/label_rectangle_properties.json +6 -0
  70. PyImageLabeling/old_version/main.py +2073 -0
  71. PyImageLabeling/old_version/models/EraseSettingsDialog.py +51 -0
  72. PyImageLabeling/old_version/models/LabeledRectangle.py +80 -0
  73. PyImageLabeling/old_version/models/MagicSettingsDialog.py +119 -0
  74. PyImageLabeling/old_version/models/OverlayOpacityDialog.py +63 -0
  75. PyImageLabeling/old_version/models/PaintSettingsDialog.py +289 -0
  76. PyImageLabeling/old_version/models/PointItem.py +66 -0
  77. PyImageLabeling/old_version/models/ProcessWorker.py +52 -0
  78. PyImageLabeling/old_version/models/ZoomableGraphicsView.py +1214 -0
  79. PyImageLabeling/old_version/models/tools/ContourTool.py +279 -0
  80. PyImageLabeling/old_version/models/tools/EraserTool.py +290 -0
  81. PyImageLabeling/old_version/models/tools/MagicPenTool.py +199 -0
  82. PyImageLabeling/old_version/models/tools/OverlayTool.py +179 -0
  83. PyImageLabeling/old_version/models/tools/PaintTool.py +68 -0
  84. PyImageLabeling/old_version/models/tools/PolygonTool.py +786 -0
  85. PyImageLabeling/old_version/models/tools/RectangleTool.py +1036 -0
  86. PyImageLabeling/parameters.json +1 -0
  87. PyImageLabeling/style.css +611 -0
  88. PyImageLabeling/view/Builder.py +333 -0
  89. PyImageLabeling/view/QBackgroundItem.py +30 -0
  90. PyImageLabeling/view/QWidgets.py +10 -0
  91. PyImageLabeling/view/View.py +226 -0
  92. PyImageLabeling/view/ZoomableGraphicsView.py +91 -0
  93. PyImageLabeling/view/__init__.py +0 -0
  94. pyimagelabeling-1.0.0.dist-info/METADATA +55 -0
  95. pyimagelabeling-1.0.0.dist-info/RECORD +99 -0
  96. pyimagelabeling-1.0.0.dist-info/WHEEL +5 -0
  97. pyimagelabeling-1.0.0.dist-info/licenses/LICENCE +22 -0
  98. pyimagelabeling-1.0.0.dist-info/top_level.txt +2 -0
  99. 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
+