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,207 @@
1
+ from PyImageLabeling.model.Core import Core
2
+ import numpy as np
3
+ from PyQt6.QtWidgets import QGraphicsEllipseItem, QGraphicsRectItem, QGraphicsPathItem, QGraphicsItemGroup, QGraphicsScene, QGraphicsItem
4
+ from PyQt6.QtGui import QPainterPath, QPen, QBrush, QImage, QPainter, QPixmap, QColor
5
+ from PyQt6.QtCore import QPointF, Qt, QRectF, QRect
6
+
7
+ from PyImageLabeling.model.Utils import Utils
8
+
9
+ class PaintBrushItemOld(QGraphicsItem):
10
+
11
+ def __init__(self, core, x, y, color, size):
12
+ super().__init__()
13
+ self.x = x
14
+ self.y = y
15
+ self.color = color
16
+ self.size = size
17
+ self.labeling_overlay_painter = core.get_labeling_overlay().get_painter()
18
+ #self.image_pixmap = core.image_pixmap
19
+
20
+ self.qrectf = QRectF(int(self.x)-(self.size/2)-5, int(self.y)-(self.size/2)-5, self.size+10, self.size+10)
21
+ self.qrectf = self.qrectf.intersected(core.image_qrectf)
22
+ alpha_color = Utils.load_parameters()["load_image"]["alpha_color"]
23
+
24
+ self.texture = QPixmap(self.size, self.size)
25
+ #self.image_pixmap.fill(Qt.GlobalColor.transparent)
26
+ self.texture.fill(Qt.GlobalColor.transparent)
27
+
28
+
29
+
30
+ painter = QPainter(self.texture)
31
+ self.pen = QPen(color, self.size)
32
+ self.pen.setCapStyle(Qt.PenCapStyle.RoundCap)
33
+ #painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Xor)
34
+ painter.setPen(self.pen)
35
+ painter.drawPoint(int(self.size/2), int(self.size/2))
36
+ painter.end()
37
+
38
+
39
+ def boundingRect(self):
40
+ return self.qrectf
41
+
42
+ def paint(self, painter, option, widget):
43
+ #painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_DestinationAtop)
44
+ #self.pen = QPen(self.color, self.size)
45
+ #self.pen.setCapStyle(Qt.PenCapStyle.RoundCap)
46
+ #painter.setPen(self.pen)
47
+ #painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_DestinationOver)
48
+ #painter.drawPoint(int(self.x), int(self.y))
49
+
50
+
51
+ #print("pp:", painter.device(), self)
52
+ painter.drawPixmap(int(self.x-(self.size/2)), int(self.y-(self.size/2)), self.texture)
53
+ #painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
54
+ #painter.end()
55
+
56
+ self.labeling_overlay_painter.drawPixmap(int(self.x-(self.size/2)), int(self.y-(self.size/2)), self.texture)
57
+
58
+ class PaintBrushItem(QGraphicsItem):
59
+
60
+ def __init__(self, core, x, y, color, size):
61
+ super().__init__()
62
+
63
+ # Initialize the variable of the first point
64
+ self.core = core
65
+ self.x = x
66
+ self.y = y
67
+ self.color = color
68
+ self.size = size
69
+ self.labeling_overlay_painter = self.core.get_current_image_item().get_labeling_overlay().get_painter()
70
+ #self.image_pixmap = self.core.image_pixmap
71
+ self.position_x = int(self.x-(self.size/2))
72
+ self.position_y = int(self.y-(self.size/2))
73
+ self.bounding_rect = QRectF(self.position_x, self.position_y, self.size, self.size)
74
+ self.bounding_rect = self.bounding_rect.intersected(core.get_current_image_item().image_qrectf)
75
+
76
+ # Create the image of the first point
77
+ self.texture = QPixmap(self.size, self.size)
78
+ self.texture.fill(Qt.GlobalColor.transparent)
79
+
80
+ painter = QPainter(self.texture)
81
+ self.pen = QPen(color, self.size)
82
+ self.pen.setCapStyle(Qt.PenCapStyle.RoundCap)
83
+
84
+ painter.setPen(self.pen)
85
+ painter.drawPoint(int(self.size/2), int(self.size/2))
86
+
87
+ # Remove the existing pixel label already colored
88
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_DestinationOut)
89
+ painter.drawPixmap(QRect(0, 0, self.size, self.size), self.core.get_current_image_item().get_labeling_overlay().labeling_overlay_pixmap, self.bounding_rect.toRect())
90
+
91
+ painter.end()
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+ def add_point(self, new_x, new_y):
100
+ # Compute the bounding rect of the new point
101
+ new_position_x = int(new_x-(self.size/2))
102
+ new_position_y = int(new_y-(self.size/2))
103
+ new_bounding_rect = QRectF(new_position_x, new_position_y, self.size, self.size)
104
+ new_bounding_rect = new_bounding_rect.intersected(self.core.get_current_image_item().image_qrectf)
105
+
106
+ # Do the union of the two bounding rects
107
+ self.united_bounding_rect = self.bounding_rect.united(new_bounding_rect)
108
+
109
+ # Create a new texture
110
+ new_texture = QPixmap(int(self.united_bounding_rect.width()), int(self.united_bounding_rect.height()))
111
+ new_texture.fill(Qt.GlobalColor.transparent)
112
+
113
+ # Add the new point in the texture
114
+ painter = QPainter(new_texture)
115
+ self.pen = QPen(self.color, self.size)
116
+ self.pen.setCapStyle(Qt.PenCapStyle.RoundCap)
117
+ painter.setPen(self.pen)
118
+ #painter.setOpacity(1)
119
+
120
+
121
+ painter.drawPoint(int(new_position_x-self.united_bounding_rect.x()+(self.size/2)), int(new_position_y-self.united_bounding_rect.y()+(self.size/2)))
122
+ #painter.setOpacity(0)
123
+
124
+ # Copy the old texture in the new texture
125
+ #painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Xor)
126
+ painter.drawPixmap(int(self.bounding_rect.x()-self.united_bounding_rect.x()), int(self.bounding_rect.y()-self.united_bounding_rect.y()), self.texture)
127
+
128
+ # Remove the existing pixel label already colored
129
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_DestinationOut)
130
+ painter.drawPixmap(QRect(0, 0, int(self.united_bounding_rect.width()), int(self.united_bounding_rect.height())), self.core.get_current_image_item().get_labeling_overlay().labeling_overlay_pixmap, self.united_bounding_rect.toRect())
131
+
132
+
133
+ painter.end()
134
+
135
+ # Update the good variable for the painter
136
+ self.texture = new_texture
137
+ self.bounding_rect = self.united_bounding_rect
138
+ self.position_x = int(self.bounding_rect.x())
139
+ self.position_y = int(self.bounding_rect.y())
140
+
141
+ def boundingRect(self):
142
+ return self.bounding_rect
143
+
144
+ def paint(self, painter, option, widget):
145
+ painter.setOpacity(self.core.get_current_image_item().get_labeling_overlay().get_opacity())
146
+ painter.drawPixmap(self.position_x, self.position_y, self.texture)
147
+
148
+
149
+ def labeling_overlay_paint(self):
150
+ #self.labeling_overlay_painter.setOpacity(0.7)
151
+ self.labeling_overlay_painter.drawPixmap(self.position_x, self.position_y, self.texture)
152
+
153
+
154
+ class PaintBrush(Core):
155
+ def __init__(self):
156
+ super().__init__()
157
+ self.last_position_x, self.last_position_y = None, None
158
+ self.point_spacing = 2
159
+ self.paint_brush_items = []
160
+ self.previous_pixmap = None
161
+
162
+ def paint_brush(self):
163
+ self.checked_button = self.paint_brush.__name__
164
+
165
+ def start_paint_brush(self, current_position):
166
+ self.view.zoomable_graphics_view.change_cursor("paint")
167
+
168
+ self.current_position_x = int(current_position.x())
169
+ self.current_position_y = int(current_position.y())
170
+
171
+ self.size_paint_brush = Utils.load_parameters()["paint_brush"]["size"]
172
+ self.color = self.get_current_label_item().get_color()
173
+
174
+ self.paint_brush_item = PaintBrushItem(self, self.current_position_x, self.current_position_y, self.color, self.size_paint_brush)
175
+ self.paint_brush_item.setZValue(2) # To place in the top of the item
176
+ self.zoomable_graphics_view.scene.addItem(self.paint_brush_item) # update is already call in this method
177
+
178
+ self.last_position_x, self.last_position_y = self.current_position_x, self.current_position_y
179
+
180
+ def move_paint_brush(self, current_position):
181
+ self.current_position_x = int(current_position.x())
182
+ self.current_position_y = int(current_position.y())
183
+
184
+ if Utils.compute_diagonal(self.current_position_x, self.current_position_y, self.last_position_x, self.last_position_y) < self.point_spacing:
185
+ return
186
+
187
+ self.paint_brush_item.add_point(self.current_position_x, self.current_position_y)
188
+ self.paint_brush_item.update()
189
+
190
+ #paint_brush_item = PaintBrushItem(self, self.current_position_x, self.current_position_y, self.color, self.size_paint_brush)
191
+ #paint_brush_item.setZValue(3) # To place in the top of the item
192
+ #self.zoomable_graphics_view.scene.addItem(paint_brush_item) # update is already call in this method
193
+ #self.paint_brush_items.append(paint_brush_item)
194
+
195
+ self.last_position_x, self.last_position_y = self.current_position_x, self.current_position_y
196
+
197
+ def end_paint_brush(self):
198
+ # Paint the good pixmap
199
+ self.paint_brush_item.labeling_overlay_paint()
200
+
201
+ # Display it :)
202
+ self.get_current_image_item().update_labeling_overlay()
203
+
204
+ # Romeve the fake item
205
+ self.zoomable_graphics_view.scene.removeItem(self.paint_brush_item)
206
+
207
+
@@ -0,0 +1,279 @@
1
+ from PyImageLabeling.model.Core import Core
2
+ from PyQt6.QtWidgets import QGraphicsPolygonItem, QGraphicsEllipseItem, QGraphicsLineItem
3
+ from PyQt6.QtGui import QPen, QBrush, QPolygonF
4
+ from PyQt6.QtCore import Qt, QPointF, QRectF, QSizeF
5
+ import math
6
+
7
+ HANDLE_SIZE = 8
8
+ HANDLE_DETECTION_DISTANCE = 15
9
+ CLOSE_DISTANCE = 20
10
+
11
+
12
+ class PolygonItem(QGraphicsPolygonItem):
13
+ HANDLE_TYPES = {
14
+ 'rotation': Qt.CursorShape.OpenHandCursor,
15
+ # all vertex handles use SizeAll
16
+ }
17
+
18
+ def __init__(self, polygon, color=Qt.GlobalColor.red):
19
+ super().__init__(polygon)
20
+
21
+ self.setPen(QPen(color, 2))
22
+ self.setFlags(
23
+ QGraphicsPolygonItem.GraphicsItemFlag.ItemIsSelectable
24
+ | QGraphicsPolygonItem.GraphicsItemFlag.ItemIsMovable
25
+ | QGraphicsPolygonItem.GraphicsItemFlag.ItemSendsGeometryChanges
26
+ )
27
+ self.setAcceptHoverEvents(True)
28
+
29
+ self.handles = {}
30
+ self.handle_selected = None
31
+ self.handles_visible = False
32
+ self.initial_rotation = 0
33
+ self.initial_angle = 0
34
+
35
+ self.update_handles()
36
+
37
+ def update_handles(self):
38
+ polygon = self.polygon()
39
+ self.handles = {
40
+ f"vertex_{i}": QRectF(
41
+ point - QPointF(HANDLE_SIZE / 2, HANDLE_SIZE / 2),
42
+ QSizeF(HANDLE_SIZE, HANDLE_SIZE),
43
+ )
44
+ for i, point in enumerate(polygon)
45
+ }
46
+ if not polygon.isEmpty():
47
+ center = polygon.boundingRect().center()
48
+ self.handles["rotation"] = QRectF(
49
+ center - QPointF(HANDLE_SIZE / 2, HANDLE_SIZE / 2),
50
+ QSizeF(HANDLE_SIZE, HANDLE_SIZE),
51
+ )
52
+
53
+ def check_handle_proximity(self, pos):
54
+ near_handle = any(
55
+ self.distance_to_rect(pos, rect) < HANDLE_DETECTION_DISTANCE
56
+ for rect in self.handles.values()
57
+ )
58
+ if self.isSelected():
59
+ near_handle = True
60
+ if near_handle != self.handles_visible:
61
+ self.handles_visible = near_handle
62
+ self.update()
63
+
64
+ @staticmethod
65
+ def distance_to_rect(point, rect):
66
+ center = rect.center()
67
+ return math.hypot(point.x() - center.x(), point.y() - center.y())
68
+
69
+ def update_cursor(self, pos):
70
+ if not self.handles_visible:
71
+ return
72
+ for name, rect in self.handles.items():
73
+ if rect.contains(pos):
74
+ self.setCursor(self.HANDLE_TYPES.get(name, Qt.CursorShape.SizeAllCursor))
75
+ return
76
+ self.setCursor(Qt.CursorShape.SizeAllCursor)
77
+
78
+ def hoverEnterEvent(self, event):
79
+ self.check_handle_proximity(event.pos())
80
+ super().hoverEnterEvent(event)
81
+
82
+ def hoverMoveEvent(self, event):
83
+ self.check_handle_proximity(event.pos())
84
+ self.update_cursor(event.pos())
85
+ super().hoverMoveEvent(event)
86
+
87
+ def hoverLeaveEvent(self, event):
88
+ self.handles_visible = False
89
+ self.setCursor(Qt.CursorShape.ArrowCursor)
90
+ self.update()
91
+ super().hoverLeaveEvent(event)
92
+
93
+ def paint(self, painter, option, widget=None):
94
+ super().paint(painter, option, widget)
95
+ if not self.handles_visible:
96
+ return
97
+
98
+ # Draw vertex handles
99
+ painter.setPen(QPen(Qt.GlobalColor.black, 1))
100
+ painter.setBrush(QBrush(Qt.GlobalColor.white))
101
+ for name, rect in self.handles.items():
102
+ if name != "rotation":
103
+ painter.drawRect(rect)
104
+
105
+ # Draw rotation handle
106
+ if "rotation" in self.handles:
107
+ painter.setPen(QPen(Qt.GlobalColor.blue, 2))
108
+ painter.setBrush(QBrush(Qt.GlobalColor.blue))
109
+ painter.drawEllipse(self.handles["rotation"])
110
+
111
+ def mousePressEvent(self, event):
112
+ self.handle_selected = None
113
+ for name, rect in self.handles.items():
114
+ if rect.contains(event.pos()):
115
+ self.handle_selected = name
116
+ if name == "rotation":
117
+ center = self.polygon().boundingRect().center()
118
+ self.setTransformOriginPoint(center)
119
+ center_scene = self.mapToScene(center)
120
+ mouse_scene = self.mapToScene(event.pos())
121
+ self.initial_rotation = math.atan2(
122
+ mouse_scene.y() - center_scene.y(),
123
+ mouse_scene.x() - center_scene.x(),
124
+ )
125
+ self.initial_angle = self.rotation()
126
+ break
127
+ super().mousePressEvent(event)
128
+
129
+ def mouseMoveEvent(self, event):
130
+ if self.handle_selected == "rotation":
131
+ self.rotate_item(event)
132
+ elif self.handle_selected and self.handle_selected.startswith("vertex_"):
133
+ self.move_vertex(event)
134
+ else:
135
+ super().mouseMoveEvent(event)
136
+
137
+ def mouseReleaseEvent(self, event):
138
+ if self.handle_selected == "rotation":
139
+ self.setCursor(Qt.CursorShape.OpenHandCursor)
140
+ event.accept()
141
+ self.handle_selected = None
142
+ self.handles_visible = True
143
+ self.update()
144
+ if not event.isAccepted():
145
+ super().mouseReleaseEvent(event)
146
+
147
+ def rotate_item(self, event):
148
+ self.setCursor(Qt.CursorShape.ClosedHandCursor)
149
+ center = self.polygon().boundingRect().center()
150
+ center_scene = self.mapToScene(center)
151
+ mouse_scene = self.mapToScene(event.pos())
152
+ current_angle = math.atan2(
153
+ mouse_scene.y() - center_scene.y(),
154
+ mouse_scene.x() - center_scene.x(),
155
+ )
156
+ angle_diff = math.degrees(current_angle - self.initial_rotation)
157
+ self.setRotation(self.initial_angle + angle_diff)
158
+ self.update_handles()
159
+ self.update()
160
+
161
+ def move_vertex(self, event):
162
+ index = int(self.handle_selected.split("_")[1])
163
+ polygon = self.polygon()
164
+ if 0 <= index < polygon.size():
165
+ polygon[index] = event.pos()
166
+ self.setPolygon(polygon)
167
+ self.update_handles()
168
+ self.update()
169
+ self.handles_visible = False
170
+
171
+
172
+ class Polygon(Core):
173
+ def __init__(self):
174
+ super().__init__()
175
+ self.polygon_points = []
176
+ self.is_drawing = False
177
+ self.preview_lines = []
178
+ self.first_point_indicator = None
179
+ self.preview_line = None
180
+ self.selected_polygon = None
181
+
182
+ def polygon(self):
183
+ self.checked_button = self.polygon.__name__
184
+ self.zoomable_graphics_view.scene.selectionChanged.connect(
185
+ self.update_selected_polygon
186
+ )
187
+
188
+ def cleanup_preview(self):
189
+ for line in self.preview_lines:
190
+ if line in self.zoomable_graphics_view.scene.items():
191
+ self.zoomable_graphics_view.scene.removeItem(line)
192
+ self.preview_lines.clear()
193
+ if self.first_point_indicator and self.first_point_indicator in self.zoomable_graphics_view.scene.items():
194
+ self.zoomable_graphics_view.scene.removeItem(self.first_point_indicator)
195
+ self.first_point_indicator = None
196
+ if self.preview_line and self.preview_line in self.zoomable_graphics_view.scene.items():
197
+ self.zoomable_graphics_view.scene.removeItem(self.preview_line)
198
+ self.preview_line = None
199
+
200
+ def start_polygon_tool(self, current_position):
201
+ self.zoomable_graphics_view.change_cursor("polygon")
202
+ pos = QPointF(current_position)
203
+ if not self.is_drawing:
204
+ # Start new polygon
205
+ self.cleanup_preview()
206
+ self.polygon_points = [pos]
207
+ self.is_drawing = True
208
+ self.color = self.get_labeling_overlay().get_color()
209
+ self.first_point_indicator = QGraphicsEllipseItem(
210
+ pos.x() - 5, pos.y() - 5, 10, 10
211
+ )
212
+ self.first_point_indicator.setPen(QPen(self.color, 2))
213
+ self.first_point_indicator.setBrush(QBrush(self.color))
214
+ self.first_point_indicator.setZValue(3)
215
+ self.zoomable_graphics_view.scene.addItem(self.first_point_indicator)
216
+ else:
217
+ # Close polygon if close enough
218
+ first_point = self.polygon_points[0]
219
+ if (
220
+ math.hypot(pos.x() - first_point.x(), pos.y() - first_point.y())
221
+ <= CLOSE_DISTANCE
222
+ and len(self.polygon_points) >= 3
223
+ ):
224
+ self.finalize_polygon()
225
+ return
226
+ # Add point + preview line
227
+ prev_point = self.polygon_points[-1]
228
+ self.polygon_points.append(pos)
229
+ line = QGraphicsLineItem(prev_point.x(), prev_point.y(), pos.x(), pos.y())
230
+ pen = QPen(self.color, 2, Qt.PenStyle.DashLine)
231
+ line.setPen(pen)
232
+ line.setZValue(2)
233
+ self.zoomable_graphics_view.scene.addItem(line)
234
+ self.preview_lines.append(line)
235
+
236
+ def move_polygon_tool(self, current_position):
237
+ if not (self.is_drawing and self.polygon_points):
238
+ return
239
+ pos = QPointF(current_position)
240
+ if self.preview_line and self.preview_line in self.zoomable_graphics_view.scene.items():
241
+ self.zoomable_graphics_view.scene.removeItem(self.preview_line)
242
+ last_point = self.polygon_points[-1]
243
+ self.preview_line = QGraphicsLineItem(last_point.x(), last_point.y(), pos.x(), pos.y())
244
+ pen = QPen(self.color, 1, Qt.PenStyle.DotLine)
245
+ self.preview_line.setPen(pen)
246
+ self.preview_line.setZValue(1)
247
+ self.zoomable_graphics_view.scene.addItem(self.preview_line)
248
+
249
+ def finalize_polygon(self):
250
+ if len(self.polygon_points) < 3:
251
+ return
252
+ self.cleanup_preview()
253
+ polygon = QPolygonF(self.polygon_points)
254
+ final = PolygonItem(polygon, self.color)
255
+ final.setZValue(2)
256
+ final.setFlag(QGraphicsPolygonItem.GraphicsItemFlag.ItemIsSelectable, True)
257
+ self.zoomable_graphics_view.scene.addItem(final)
258
+ self.selected_polygon = final
259
+ self.polygon_points.clear()
260
+ self.is_drawing = False
261
+
262
+ def cancel_polygon(self):
263
+ if self.is_drawing:
264
+ self.cleanup_preview()
265
+ self.polygon_points.clear()
266
+ self.is_drawing = False
267
+
268
+ def end_polygon_tool(self):
269
+ pass
270
+
271
+ def update_selected_polygon(self):
272
+ items = self.zoomable_graphics_view.scene.selectedItems()
273
+ self.selected_polygon = next((i for i in reversed(items) if isinstance(i, PolygonItem)), None)
274
+
275
+ def clear_polygon(self):
276
+ if self.selected_polygon and self.selected_polygon in self.zoomable_graphics_view.scene.items():
277
+ self.zoomable_graphics_view.scene.removeItem(self.selected_polygon)
278
+ self.selected_polygon = None
279
+ self.cancel_polygon()