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,786 @@
1
+ from PyQt6.QtCore import Qt, QRectF, QPointF, QLineF
2
+ from PyQt6.QtGui import QPen, QColor, QCursor, QImage, QPainter, QPolygonF
3
+ from PyQt6.QtWidgets import (QInputDialog, QDialog, QVBoxLayout, QComboBox, QLabel,
4
+ QDialogButtonBox, QMenu, QColorDialog, QSpinBox, QHBoxLayout,
5
+ QPushButton, QGroupBox, QFormLayout, QGraphicsItem, QGraphicsEllipseItem, QLineEdit)
6
+ import os
7
+ import time
8
+ import math
9
+
10
+ class LabelPolygonPropertiesDialog(QDialog):
11
+ def __init__(self, parent=None):
12
+ super().__init__(parent)
13
+ self.setWindowTitle("Label Properties")
14
+ self.setMinimumWidth(300)
15
+ self.setStyleSheet("""
16
+ QDialog {
17
+ background-color: #000000;
18
+ color: white;
19
+ border: 1px solid #444444;
20
+ }
21
+ QLabel {
22
+ color: white;
23
+ font-size: 14px;
24
+ background-color: #000000;
25
+ border: none;
26
+ }
27
+ """)
28
+
29
+ layout = QVBoxLayout()
30
+
31
+ self.label_name = QLabel("Label: ")
32
+ self.label_color = QLabel("Color: ")
33
+ self.label_thickness= QLabel("thickness: ")
34
+
35
+ layout.addWidget(self.label_name)
36
+ layout.addWidget(self.label_color)
37
+ layout.addWidget(self.label_thickness)
38
+
39
+ self.setLayout(layout)
40
+
41
+ def update_properties(self, label, color, thickness):
42
+ self.label_name.setText(f"Label: {label}")
43
+ self.label_color.setText(f"Color: {color.name()}")
44
+ self.label_thickness.setText(f"thickness: {thickness}")
45
+
46
+ class DraggableVertex(QGraphicsEllipseItem):
47
+ """Custom draggable vertex point for polygon editing"""
48
+ def __init__(self, x, y, vertex_index, polygon_tool, polygon_item):
49
+ super().__init__(x - 4, y - 4, 8, 8)
50
+ self.vertex_index = vertex_index
51
+ self.polygon_tool = polygon_tool
52
+ self.polygon_item = polygon_item
53
+
54
+ # Visual appearance
55
+ self.setPen(QPen(QColor(255, 255, 0), 2)) # Yellow outline
56
+ self.setBrush(QColor(255, 0, 0)) # Red fill
57
+
58
+ # Make it draggable and send position changes
59
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
60
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
61
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsScenePositionChanges, True)
62
+ self.setZValue(1000) # Ensure points are on top
63
+
64
+ def itemChange(self, change, value):
65
+ """Handle item changes, particularly position changes"""
66
+ if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
67
+ # Update the polygon when this vertex moves
68
+ if self.polygon_tool and hasattr(self.polygon_tool, 'update_polygon_from_vertex'):
69
+ # Get the center of the item in scene coordinates
70
+ new_pos = self.mapToScene(value)
71
+ self.polygon_tool.update_polygon_from_vertex(self.vertex_index, new_pos)
72
+ return value # Return the original value to ensure it is set correctly
73
+
74
+ return super().itemChange(change, value)
75
+
76
+
77
+ class CustomizePolygonDialog(QDialog):
78
+ """Dialog for customizing polygon appearance"""
79
+ def __init__(self, current_color=None, current_thickness=2, properties_manager=None, parent=None):
80
+ super().__init__(parent)
81
+ self.properties_manager = properties_manager
82
+ self.setWindowTitle("Customize Polygon")
83
+ self.setModal(True)
84
+ self.resize(300, 300) # Increased height for label selection
85
+
86
+ # CONSISTENT DARK THEME STYLESHEET
87
+ self.setStyleSheet("""
88
+ QDialog {
89
+ background-color: #000000;
90
+ color: white;
91
+ font-size: 14px;
92
+ border: 1px solid #444444;
93
+ }
94
+ QLabel {
95
+ color: white;
96
+ background-color: transparent;
97
+ font-size: 12px;
98
+ }
99
+ QSpinBox {
100
+ background-color: #111111;
101
+ color: white;
102
+ border: 1px solid #555555;
103
+ padding: 5px;
104
+ }
105
+ QSpinBox:focus {
106
+ border: 1px solid #666666;
107
+ }
108
+ QPushButton {
109
+ background-color: #111111;
110
+ color: white;
111
+ border: 1px solid #666666;
112
+ border-radius: 5px;
113
+ padding: 6px 12px;
114
+ }
115
+ QPushButton:hover {
116
+ background-color: #222222;
117
+ }
118
+ QPushButton:pressed {
119
+ background-color: #333333;
120
+ }
121
+ QGroupBox {
122
+ color: white;
123
+ font-weight: bold;
124
+ border: 1px solid #444444;
125
+ margin-top: 10px;
126
+ padding-top: 10px;
127
+ background-color: transparent;
128
+ }
129
+ QGroupBox::title {
130
+ subcontrol-origin: margin;
131
+ left: 10px;
132
+ padding: 0 5px 0 5px;
133
+ color: white;
134
+ }
135
+ QDialogButtonBox QPushButton {
136
+ background-color: #111111;
137
+ color: white;
138
+ border: 1px solid #666666;
139
+ min-width: 80px;
140
+ padding: 6px 12px;
141
+ }
142
+ QDialogButtonBox QPushButton:hover {
143
+ background-color: #222222;
144
+ }
145
+ QComboBox {
146
+ background-color: #111111;
147
+ color: white;
148
+ border: 1px solid #555555;
149
+ padding: 5px;
150
+ }
151
+ QComboBox QAbstractItemView {
152
+ background-color: #000000;
153
+ color: white;
154
+ selection-background-color: #222222;
155
+ }
156
+ """)
157
+
158
+ layout = QVBoxLayout(self)
159
+
160
+ # Label selection group
161
+ if self.properties_manager:
162
+ label_group = QGroupBox("Label")
163
+ label_layout = QFormLayout()
164
+
165
+ self.label_combo = QComboBox()
166
+ self.label_combo.addItem("") # Empty option
167
+ self.label_combo.addItems(self.properties_manager.get_all_labels())
168
+ self.label_combo.setEditable(True)
169
+ self.label_combo.currentTextChanged.connect(self.update_properties_from_label)
170
+
171
+ label_layout.addRow("Label:", self.label_combo)
172
+ label_group.setLayout(label_layout)
173
+ layout.addWidget(label_group)
174
+
175
+ # Color selection group
176
+ color_group = QGroupBox("Polygon Color")
177
+ color_layout = QFormLayout()
178
+
179
+ # Color selection
180
+ color_selection_layout = QHBoxLayout()
181
+ self.color_button = QPushButton("Choose Color")
182
+ self.color_button.clicked.connect(self.choose_color)
183
+
184
+ # Set initial color
185
+ self.selected_color = current_color if current_color else QColor(0, 255, 0) # Default green
186
+ self.update_color_button()
187
+
188
+ color_selection_layout.addWidget(self.color_button)
189
+ color_selection_layout.addStretch()
190
+
191
+ color_layout.addRow("Color:", color_selection_layout)
192
+ color_group.setLayout(color_layout)
193
+ layout.addWidget(color_group)
194
+
195
+ # Thickness selection group
196
+ thickness_group = QGroupBox("Polygon Thickness")
197
+ thickness_layout = QFormLayout()
198
+
199
+ self.thickness_spinbox = QSpinBox()
200
+ self.thickness_spinbox.setMinimum(1)
201
+ self.thickness_spinbox.setMaximum(10)
202
+ self.thickness_spinbox.setValue(current_thickness)
203
+ self.thickness_spinbox.setSuffix(" px")
204
+
205
+ thickness_layout.addRow("Thickness:", self.thickness_spinbox)
206
+ thickness_group.setLayout(thickness_layout)
207
+ layout.addWidget(thickness_group)
208
+
209
+ # Dialog buttons
210
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
211
+ button_box.accepted.connect(self.accept)
212
+ button_box.rejected.connect(self.reject)
213
+
214
+ layout.addWidget(button_box)
215
+
216
+ def update_properties_from_label(self, label):
217
+ """Update color and thickness based on the selected label"""
218
+ if not label or not self.properties_manager:
219
+ return
220
+
221
+ props = self.properties_manager.get_label_property(label)
222
+ if props:
223
+ self.selected_color = props['color']
224
+ self.update_color_button()
225
+ self.thickness_spinbox.setValue(props['thickness'])
226
+
227
+ # UPDATE PARENT'S DEFAULT VALUES IF PARENT IS POLYGON TOOL
228
+ if hasattr(self.parent(), 'default_polygon_color'):
229
+ self.parent().default_polygon_color = props['color']
230
+ self.parent().default_polygon_thickness = props['thickness']
231
+
232
+ def get_settings(self):
233
+ """Return the selected color, thickness, and label"""
234
+ color = self.selected_color
235
+ thickness = self.thickness_spinbox.value()
236
+ label = self.label_combo.currentText().strip() if hasattr(self, 'label_combo') else None
237
+ return color, thickness, label
238
+
239
+ def choose_color(self):
240
+ """Open color dialog to choose polygon color"""
241
+ color_dialog = QColorDialog(self.selected_color, self)
242
+ color_dialog.setStyleSheet("""
243
+ QColorDialog {
244
+ background-color: #000000;
245
+ color: white;
246
+ }
247
+ QColorDialog QLabel {
248
+ color: white;
249
+ background-color: transparent;
250
+ }
251
+ QColorDialog QPushButton {
252
+ background-color: #111111;
253
+ color: white;
254
+ border: 1px solid #666666;
255
+ border-radius: 5px;
256
+ padding: 6px 12px;
257
+ }
258
+ QColorDialog QPushButton:hover {
259
+ background-color: #222222;
260
+ }
261
+ QColorDialog QSpinBox {
262
+ background-color: #111111;
263
+ color: white;
264
+ border: 1px solid #555555;
265
+ padding: 5px;
266
+ }
267
+ QColorDialog QLineEdit {
268
+ background-color: #222222;
269
+ color: white;
270
+ border: 1px solid #555555;
271
+ padding: 5px;
272
+ }
273
+ """)
274
+
275
+ if color_dialog.exec() == QDialog.DialogCode.Accepted:
276
+ self.selected_color = color_dialog.currentColor()
277
+ self.update_color_button()
278
+
279
+ def update_color_button(self):
280
+ """Update the color button to show the selected color"""
281
+ color_name = self.selected_color.name()
282
+ self.color_button.setText(f"Color: {color_name}")
283
+ text_color = 'white' if self.selected_color.lightness() < 128 else 'black'
284
+ self.color_button.setStyleSheet(f"""
285
+ QPushButton {{
286
+ background-color: {color_name};
287
+ color: {text_color};
288
+ border: 1px solid #555555;
289
+ padding: 6px 12px;
290
+ border-radius: 5px;
291
+ font-weight: bold;
292
+ }}
293
+ QPushButton:hover {{
294
+ border: 2px solid #777777;
295
+ background-color: {color_name};
296
+ }}
297
+ """)
298
+
299
+ def get_settings(self):
300
+ """Return the selected color, thickness, and label"""
301
+ color = self.selected_color
302
+ thickness = self.thickness_spinbox.value()
303
+ label = self.label_combo.currentText().strip() if hasattr(self, 'label_combo') else None
304
+ return color, thickness, label
305
+
306
+
307
+ class PolygonTool:
308
+ last_used_label = None
309
+ def __init__(self):
310
+ # Default polygon appearance settings
311
+ self.default_polygon_color = QColor(0, 255, 0) # Green
312
+ self.default_polygon_thickness = 2
313
+ def add_polygon_point(self, point):
314
+ """Add a point to the current polygon being created"""
315
+ self.current_polygon_points.append(point)
316
+
317
+ # Draw a small circle at the point
318
+ circle_radius = 3
319
+ circle = self.scene.addEllipse(
320
+ point.x() - circle_radius, point.y() - circle_radius,
321
+ circle_radius * 2, circle_radius * 2,
322
+ QPen(self.default_polygon_color, 1)
323
+ )
324
+
325
+ # Connect to previous point with a line
326
+ if len(self.current_polygon_points) > 1:
327
+ prev_point = self.current_polygon_points[-2]
328
+ line = self.scene.addLine(
329
+ prev_point.x(), prev_point.y(),
330
+ point.x(), point.y(),
331
+ QPen(self.default_polygon_color, self.default_polygon_thickness)
332
+ )
333
+ self.current_polygon_lines.append(line)
334
+
335
+ self.current_polygon_lines.append(circle)
336
+
337
+ def update_polygon_preview(self, current_pos):
338
+ """Update the preview line showing where the next polygon edge will be"""
339
+ if not self.current_polygon_points:
340
+ return
341
+
342
+ # Remove existing preview line
343
+ if hasattr(self, 'preview_line') and self.preview_line:
344
+ self.scene.removeItem(self.preview_line)
345
+
346
+ # Create new preview line from last point to current mouse position
347
+ last_point = self.current_polygon_points[-1]
348
+ self.preview_line = self.scene.addLine(
349
+ last_point.x(), last_point.y(),
350
+ current_pos.x(), current_pos.y(),
351
+ QPen(self.default_polygon_color, 1, Qt.PenStyle.DashLine)
352
+ )
353
+
354
+ # If we have at least 3 points, also show a line to the first point
355
+ if len(self.current_polygon_points) >= 3:
356
+ if hasattr(self, 'close_preview_line') and self.close_preview_line:
357
+ self.scene.removeItem(self.close_preview_line)
358
+
359
+ first_point = self.current_polygon_points[0]
360
+ distance = math.sqrt((current_pos.x() - first_point.x())**2 +
361
+ (current_pos.y() - first_point.y())**2)
362
+
363
+ # Show close preview line if mouse is close to first point
364
+ if distance <= self.close_distance_threshold:
365
+ self.close_preview_line = self.scene.addLine(
366
+ current_pos.x(), current_pos.y(),
367
+ first_point.x(), first_point.y(),
368
+ QPen(QColor(255, 255, 0), 2) # Yellow line to indicate close
369
+ )
370
+
371
+ def close_polygon(self):
372
+ """Close the current polygon and create a polygon item"""
373
+ if len(self.current_polygon_points) < 3:
374
+ return
375
+
376
+ # Remove preview lines
377
+ if hasattr(self, 'preview_line') and self.preview_line:
378
+ self.scene.removeItem(self.preview_line)
379
+ self.preview_line = None
380
+ if hasattr(self, 'close_preview_line') and self.close_preview_line:
381
+ self.scene.removeItem(self.close_preview_line)
382
+ self.close_preview_line = None
383
+
384
+ # Remove temporary drawing elements
385
+ for item in self.current_polygon_lines:
386
+ self.scene.removeItem(item)
387
+
388
+ # Create the final polygon
389
+ polygon = QPolygonF(self.current_polygon_points)
390
+
391
+ # Use last used label's properties if available
392
+ if PolygonTool.last_used_label and hasattr(self, 'label_properties_manager'):
393
+ props = self.label_properties_manager.get_label_property(PolygonTool.last_used_label)
394
+ if props:
395
+ color = props['color']
396
+ thickness = props['thickness']
397
+ else:
398
+ color = self.default_polygon_color
399
+ thickness = self.default_polygon_thickness
400
+ else:
401
+ color = self.default_polygon_color
402
+ thickness = self.default_polygon_thickness
403
+
404
+ dialog = CustomizePolygonDialog(color, thickness, self.label_properties_manager if hasattr(self, 'label_properties_manager') else None, self)
405
+ dialog.setWindowTitle("Customize Polygon")
406
+
407
+ # Set the label combo box to the last used label if available
408
+ if PolygonTool.last_used_label:
409
+ dialog.label_combo.setCurrentText(PolygonTool.last_used_label)
410
+
411
+ if dialog.exec() == QDialog.DialogCode.Accepted:
412
+ new_color, new_thickness, new_label = dialog.get_settings()
413
+
414
+ # UPDATE DEFAULT VALUES TO MATCH SELECTED VALUES
415
+ self.default_polygon_color = new_color
416
+ self.default_polygon_thickness = new_thickness
417
+
418
+ pen = QPen(new_color, new_thickness)
419
+ polygon_item = self.scene.addPolygon(polygon, pen)
420
+
421
+ # Store custom settings on the polygon
422
+ polygon_item.custom_color = new_color
423
+ polygon_item.custom_thickness = new_thickness
424
+ polygon_item.label_name = new_label # Store the label name
425
+ polygon_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
426
+ polygon_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
427
+
428
+ # Save the label properties if a new label was created
429
+ if new_label and hasattr(self, 'label_properties_manager'):
430
+ self.label_properties_manager.add_label_property(new_label, new_color, new_thickness)
431
+
432
+ # Add to polygon items list
433
+ self.polygon_items.append(polygon_item)
434
+
435
+ # Update the last used label
436
+ if new_label:
437
+ PolygonTool.last_used_label = new_label
438
+
439
+ # Reset polygon creation state
440
+ self.current_polygon_points = []
441
+ self.current_polygon_lines = []
442
+
443
+ return polygon_item if 'polygon_item' in locals() else None
444
+
445
+ def cancel_polygon_creation(self):
446
+ """Cancel the current polygon creation"""
447
+ # Remove preview lines
448
+ if hasattr(self, 'preview_line') and self.preview_line:
449
+ self.scene.removeItem(self.preview_line)
450
+ self.preview_line = None
451
+ if hasattr(self, 'close_preview_line') and self.close_preview_line:
452
+ self.scene.removeItem(self.close_preview_line)
453
+ self.close_preview_line = None
454
+
455
+ # Remove temporary drawing elements
456
+ for item in self.current_polygon_lines:
457
+ self.scene.removeItem(item)
458
+
459
+ # Reset state
460
+ self.current_polygon_points = []
461
+ self.current_polygon_lines = []
462
+
463
+ print("Polygon creation cancelled")
464
+
465
+ def show_polygon_context_menu(self, labeled_polygon, global_pos):
466
+ """Show context menu for polygon manipulation"""
467
+ context_menu = QMenu(self)
468
+
469
+ # CONSISTENT DARK THEME FOR CONTEXT MENU
470
+ context_menu.setStyleSheet("""
471
+ QMenu {
472
+ background-color: #000000;
473
+ color: white;
474
+ border: 1px solid #444444;
475
+ font-size: 14px;
476
+ }
477
+ QMenu::item {
478
+ padding: 8px 20px;
479
+ background-color: transparent;
480
+ }
481
+ QMenu::item:selected {
482
+ background-color: #222222;
483
+ }
484
+ QMenu::separator {
485
+ height: 1px;
486
+ background-color: #444444;
487
+ margin: 2px 0px;
488
+ }
489
+ """)
490
+
491
+ custom_action = context_menu.addAction("Custom")
492
+ modify_action = context_menu.addAction("Modify")
493
+ context_menu.addSeparator()
494
+ delete_action = context_menu.addAction("Delete")
495
+
496
+ # Execute the menu and handle the selected action
497
+ action = context_menu.exec(global_pos)
498
+
499
+ if action == custom_action:
500
+ self.customize_polygon(labeled_polygon)
501
+ elif action == modify_action:
502
+ self.start_polygon_edit_mode(labeled_polygon)
503
+ elif action == delete_action:
504
+ self.delete_polygon(labeled_polygon)
505
+
506
+ def customize_polygon(self, labeled_polygon):
507
+ """Show dialog to customize polygon appearance"""
508
+ current_pen = labeled_polygon.pen()
509
+ current_color = current_pen.color()
510
+ current_thickness = current_pen.width()
511
+ current_label = getattr(labeled_polygon, 'label_name', '')
512
+
513
+ dialog = CustomizePolygonDialog(
514
+ current_color,
515
+ current_thickness,
516
+ self.label_properties_manager if hasattr(self, 'label_properties_manager') else None,
517
+ self
518
+ )
519
+
520
+ if hasattr(dialog, 'label_combo'):
521
+ dialog.label_combo.setCurrentText(current_label)
522
+
523
+ if dialog.exec() == QDialog.DialogCode.Accepted:
524
+ new_color, new_thickness, new_label = dialog.get_settings()
525
+ new_pen = QPen(new_color, new_thickness)
526
+
527
+ self.default_polygon_color = new_color
528
+ self.default_polygon_thickness = new_thickness
529
+
530
+ if not hasattr(labeled_polygon, 'original_pen'):
531
+ labeled_polygon.original_pen = current_pen
532
+
533
+ labeled_polygon.setPen(new_pen)
534
+ labeled_polygon.custom_color = new_color
535
+ labeled_polygon.custom_thickness = new_thickness
536
+
537
+ # Update label if changed
538
+ if new_label:
539
+ labeled_polygon.label_name = new_label
540
+ PolygonTool.last_used_label = new_label
541
+
542
+ # Update label properties if this is a new label
543
+ if hasattr(self, 'label_properties_manager') and new_label:
544
+ self.label_properties_manager.add_label_property(
545
+ new_label, new_color, new_thickness
546
+ )
547
+
548
+ self.scene.update()
549
+
550
+
551
+ def delete_polygon(self, labeled_polygon):
552
+ """Delete the selected polygon"""
553
+ if labeled_polygon in self.polygon_items:
554
+ self.polygon_items.remove(labeled_polygon)
555
+ if labeled_polygon in self.labeled_polygons:
556
+ self.labeled_polygons.remove(labeled_polygon)
557
+ self.scene.removeItem(labeled_polygon)
558
+
559
+ def save_polygon_to_jpeg(self, labeled_polygon):
560
+ """Save the polygon selection area to a JPEG file"""
561
+ # Get the bounding rectangle of the polygon
562
+ bounding_rect = labeled_polygon.boundingRect()
563
+
564
+ # Create a QImage with the size of the bounding rectangle
565
+ image = QImage(int(bounding_rect.width()), int(bounding_rect.height()), QImage.Format.Format_RGB888)
566
+ image.fill(Qt.GlobalColor.white)
567
+
568
+ # Create a painter for the image
569
+ painter = QPainter(image)
570
+
571
+ # Render the scene portion
572
+ source_rect = bounding_rect
573
+ target_rect = QRectF(0, 0, bounding_rect.width(), bounding_rect.height())
574
+ self.scene.render(painter, target_rect, source_rect)
575
+
576
+ # Draw the polygon on the image
577
+ pen = QPen(labeled_polygon.pen().color(), labeled_polygon.pen().width())
578
+ painter.setPen(pen)
579
+ painter.setBrush(Qt.BrushStyle.NoBrush)
580
+ polygon = labeled_polygon.polygon()
581
+ polygon_translated = QPolygonF()
582
+ for point in polygon:
583
+ polygon_translated.append(QPointF(point.x() - bounding_rect.x(), point.y() - bounding_rect.y()))
584
+ painter.drawPolygon(polygon_translated)
585
+
586
+ painter.end()
587
+
588
+ # Save the image
589
+ save_dir = os.path.join(os.getcwd(), 'save', 'polygons')
590
+ if not os.path.exists(save_dir):
591
+ os.makedirs(save_dir)
592
+
593
+ file_name = f"polygon_{int(bounding_rect.x())}_{int(bounding_rect.y())}.jpeg"
594
+ file_path = os.path.join(save_dir, file_name)
595
+
596
+ if image.save(file_path, "JPEG"):
597
+ print(f"Polygon selection saved successfully at {file_path}")
598
+ else:
599
+ print("Failed to save the polygon selection.")
600
+
601
+ def toggle_polygon_mode(self, enabled):
602
+ """Toggle polygon selection mode on/off"""
603
+ self.polygon_mode = enabled
604
+
605
+ if not enabled:
606
+ # Cancel any current polygon creation
607
+ self.cancel_polygon_creation()
608
+
609
+ def clear_polygons(self):
610
+ """Remove all polygon selections from the scene"""
611
+ if hasattr(self, 'polygon_items') and self.polygon_items:
612
+ for item in self.polygon_items:
613
+ if item in self.scene.items():
614
+ self.scene.removeItem(item)
615
+ self.polygon_items = []
616
+
617
+ if hasattr(self, 'labeled_polygons') and self.labeled_polygons:
618
+ for item in self.labeled_polygons:
619
+ if item in self.scene.items():
620
+ self.scene.removeItem(item)
621
+ self.labeled_polygons = []
622
+
623
+ # Cancel any current polygon creation
624
+ self.cancel_polygon_creation()
625
+
626
+ self.scene.update()
627
+
628
+ def set_default_polygon_style(self):
629
+ """Show dialog to set default polygon style"""
630
+ dialog = CustomizePolygonDialog(self.default_polygon_color,
631
+ self.default_polygon_thickness, self)
632
+ dialog.setWindowTitle("Set Default Polygon Style")
633
+
634
+ if dialog.exec() == QDialog.DialogCode.Accepted:
635
+ self.default_polygon_color, self.default_polygon_thickness = dialog.get_settings()
636
+ print(f"Default polygon style updated: Color={self.default_polygon_color.name()}, Thickness={self.default_polygon_thickness}px")
637
+
638
+ def save_entire_image_with_polygons(self):
639
+ """Save the entire image with rectangles and polygons drawn on it."""
640
+ if not hasattr(self, 'base_pixmap') or self.base_pixmap is None:
641
+ print("No base image to save.")
642
+ return False
643
+
644
+ # Create a QImage with the same size as the base pixmap
645
+ image = QImage(self.base_pixmap.size(), QImage.Format.Format_ARGB32)
646
+ image.fill(Qt.GlobalColor.transparent) # Start with a transparent background
647
+
648
+ # Create a painter for the image
649
+ painter = QPainter(image)
650
+
651
+ # Draw the base image
652
+ painter.drawPixmap(0, 0, self.base_pixmap)
653
+
654
+ # Draw all polygons with their custom colors and thickness
655
+ if hasattr(self, 'polygon_items'):
656
+ for polygon_item in self.polygon_items:
657
+ polygon = polygon_item.polygon()
658
+ pen = polygon_item.pen()
659
+ painter.setPen(pen) # Use the polygon's custom pen
660
+ painter.drawPolygon(polygon)
661
+
662
+ if hasattr(self, 'labeled_rectangles'):
663
+ for rect_item in self.labeled_rectangles:
664
+ rect = rect_item.rect()
665
+ pen = rect_item.pen()
666
+ painter.setPen(pen) # Use the rectangle's custom pen
667
+ painter.drawRect(rect)
668
+
669
+ if hasattr(self, 'rectangle_items'):
670
+ for rect_item in self.rectangle_items:
671
+ rect = rect_item.rect()
672
+ pen = rect_item.pen()
673
+ painter.setPen(pen) # Use the rectangle's custom pen
674
+ painter.drawRect(rect)
675
+
676
+ painter.end()
677
+
678
+ # Create the folder to save the image
679
+ save_dir = os.path.join(os.getcwd(), 'save')
680
+ if not os.path.exists(save_dir):
681
+ os.makedirs(save_dir)
682
+
683
+ # Generate a unique file name
684
+ file_name = f"entire_image_with_shapes_{int(time.time())}.png"
685
+ file_path = os.path.join(save_dir, file_name)
686
+
687
+ # Save the image as PNG
688
+ if image.save(file_path, "PNG"):
689
+ print(f"Image saved successfully at {file_path}")
690
+ return True
691
+ else:
692
+ print("Failed to save the image.")
693
+ return False
694
+
695
+ def start_polygon_edit_mode(self, polygon_item):
696
+ """Start editing mode for the polygon with draggable vertices"""
697
+ if hasattr(self, 'polygon_edit_mode') and self.polygon_edit_mode:
698
+ self.end_polygon_edit_mode()
699
+
700
+ self.polygon_edit_mode = True
701
+ self.editing_polygon = polygon_item
702
+ self.polygon_point_items = []
703
+
704
+ # Create draggable vertex points for each vertex
705
+ polygon = polygon_item.polygon()
706
+ for i, point in enumerate(polygon):
707
+ vertex_item = DraggableVertex(point.x(), point.y(), i, self, polygon_item)
708
+ self.scene.addItem(vertex_item)
709
+ self.polygon_point_items.append(vertex_item)
710
+
711
+ # Make the polygon itself non-movable during edit
712
+ polygon_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
713
+
714
+ def update_polygon_from_vertex(self, vertex_index, new_position):
715
+ """Update the polygon shape when a vertex is moved"""
716
+ if not self.polygon_edit_mode or not self.editing_polygon:
717
+ return
718
+
719
+ # Get current polygon
720
+ current_polygon = self.editing_polygon.polygon()
721
+
722
+ # Update the specific vertex
723
+ if 0 <= vertex_index < len(current_polygon):
724
+ # Create new polygon with updated vertex
725
+ new_points = []
726
+ for i, point in enumerate(current_polygon):
727
+ if i == vertex_index:
728
+ new_points.append(new_position)
729
+ else:
730
+ new_points.append(point)
731
+
732
+ # Update the polygon
733
+ new_polygon = QPolygonF(new_points)
734
+ self.editing_polygon.setPolygon(new_polygon)
735
+
736
+ def update_polygon_from_points(self):
737
+ """Update the polygon shape based on current point positions"""
738
+ if not hasattr(self, 'polygon_edit_mode') or not self.polygon_edit_mode or not self.editing_polygon:
739
+ return
740
+
741
+ # Get current positions of all points
742
+ new_points = []
743
+ for point_item in self.polygon_point_items:
744
+ # Get the center of the point item in scene coordinates
745
+ point_center = point_item.boundingRect().center()
746
+ scene_center = point_item.mapToScene(point_center)
747
+ new_points.append(QPointF(scene_center.x(), scene_center.y()))
748
+
749
+ # Update the polygon
750
+ new_polygon = QPolygonF(new_points)
751
+ self.editing_polygon.setPolygon(new_polygon)
752
+
753
+ def end_polygon_edit_mode(self):
754
+ """End polygon editing mode"""
755
+ if not hasattr(self, 'polygon_edit_mode') or not self.polygon_edit_mode:
756
+ return
757
+
758
+ # Remove all point items
759
+ for point_item in self.polygon_point_items:
760
+ self.scene.removeItem(point_item)
761
+ self.polygon_point_items = []
762
+
763
+ # Make the polygon movable again
764
+ if self.editing_polygon:
765
+ self.editing_polygon.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
766
+
767
+ self.polygon_edit_mode = False
768
+ self.editing_polygon = None
769
+ self.dragging_polygon_point = False
770
+ self.dragged_point_item = None
771
+
772
+ print("Polygon edit mode ended.")
773
+
774
+ # Add this method to handle mouse events during polygon editing:
775
+ def handle_polygon_edit_mouse_move(self, event):
776
+ """Handle mouse move events during polygon editing"""
777
+ if self.polygon_edit_mode:
778
+ self.update_polygon_from_points()
779
+
780
+ def handle_polygon_edit_mouse_press(self, event):
781
+ """Handle mouse press events during polygon editing"""
782
+ if self.polygon_edit_mode and event.button() == Qt.MouseButton.RightButton:
783
+ self.end_polygon_edit_mode()
784
+ return True # Event handled
785
+ return False # Event not handled
786
+