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,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()
|