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,1036 @@
1
+ from PyQt6.QtCore import Qt, QRectF, QPointF, QLineF
2
+ from PyQt6.QtGui import QPen, QColor, QCursor, QImage, QPainter
3
+ from PyQt6.QtWidgets import (QInputDialog, QDialog, QVBoxLayout, QComboBox, QLabel,
4
+ QDialogButtonBox, QMenu, QColorDialog, QSpinBox, QHBoxLayout,
5
+ QPushButton, QGroupBox, QFormLayout, QMessageBox, QListWidget, QListWidgetItem)
6
+ import os
7
+ import time
8
+ import json
9
+
10
+ class LabelRectanglePropertiesDialog(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 LabelPropertiesManager:
47
+ """Manages saving and loading of label properties"""
48
+
49
+ def __init__(self, properties_file="label_rectangle_properties.json"):
50
+ self.properties_file = properties_file
51
+ self.label_properties = {}
52
+ self.load_properties()
53
+
54
+ def save_properties(self):
55
+ """Save label properties to JSON file"""
56
+ try:
57
+ # Convert QColor objects to serializable format
58
+ serializable_props = {}
59
+ for label, props in self.label_properties.items():
60
+ color = props['color']
61
+ if isinstance(color, str):
62
+ color = QColor(color) # Convert string to QColor if needed
63
+ serializable_props[label] = {
64
+ 'color': color.name(), # Safely get hex color
65
+ 'thickness': props['thickness']
66
+ }
67
+
68
+ with open(self.properties_file, 'w') as f:
69
+ json.dump(serializable_props, f, indent=2)
70
+ return True
71
+ except Exception as e:
72
+ print(f"Error saving label properties: {e}")
73
+ return False
74
+
75
+ def load_properties(self):
76
+ """Load label properties from JSON file"""
77
+ try:
78
+ if os.path.exists(self.properties_file):
79
+ with open(self.properties_file, 'r') as f:
80
+ data = json.load(f)
81
+
82
+ # Convert hex strings back to QColor objects
83
+ for label, props in data.items():
84
+ self.label_properties[label] = {
85
+ 'color': QColor(props['color']),
86
+ 'thickness': props['thickness']
87
+ }
88
+ return True
89
+ except Exception as e:
90
+ print(f"Error loading label properties: {e}")
91
+ return False
92
+
93
+ def add_label_property(self, label, color, thickness):
94
+ """Add or update a label property"""
95
+ self.label_properties[label] = {
96
+ 'color': color,
97
+ 'thickness': thickness
98
+ }
99
+ self.save_properties()
100
+
101
+ def get_label_property(self, label):
102
+ """Get properties for a specific label"""
103
+ return self.label_properties.get(label, None)
104
+
105
+ def get_all_labels(self):
106
+ """Get all saved label names"""
107
+ return list(self.label_properties.keys())
108
+
109
+ def remove_label_property(self, label):
110
+ """Remove a label property"""
111
+ if label in self.label_properties:
112
+ del self.label_properties[label]
113
+ self.save_properties()
114
+ return True
115
+ return False
116
+
117
+
118
+ class ManageLabelPropertiesDialog(QDialog):
119
+ """Dialog for managing saved label properties"""
120
+
121
+ def __init__(self, properties_manager, parent=None):
122
+ super().__init__(parent)
123
+ self.properties_manager = properties_manager
124
+ self.parent_widget = parent # Store reference to parent widget
125
+ self.setWindowTitle("Manage Label Properties")
126
+ self.setModal(True)
127
+ self.resize(400, 300)
128
+
129
+ # Apply dark theme
130
+ self.setStyleSheet("""
131
+ QDialog {
132
+ background-color: #000000;
133
+ color: white;
134
+ font-size: 14px;
135
+ border: 1px solid #444444;
136
+ }
137
+ QLabel {
138
+ color: white;
139
+ background-color: transparent;
140
+ font-size: 12px;
141
+ }
142
+ QListWidget {
143
+ background-color: #111111;
144
+ color: white;
145
+ border: 1px solid #555555;
146
+ selection-background-color: #333333;
147
+ }
148
+ QPushButton {
149
+ background-color: #111111;
150
+ color: white;
151
+ border: 1px solid #666666;
152
+ border-radius: 5px;
153
+ padding: 6px 12px;
154
+ }
155
+ QPushButton:hover {
156
+ background-color: #222222;
157
+ }
158
+ QPushButton:pressed {
159
+ background-color: #333333;
160
+ }
161
+ QDialogButtonBox QPushButton {
162
+ background-color: #111111;
163
+ color: white;
164
+ border: 1px solid #666666;
165
+ min-width: 80px;
166
+ padding: 6px 12px;
167
+ }
168
+ QDialogButtonBox QPushButton:hover {
169
+ background-color: #222222;
170
+ }
171
+ """)
172
+
173
+ layout = QVBoxLayout(self)
174
+
175
+ # Instructions
176
+ instructions = QLabel("Manage your saved label properties:")
177
+ layout.addWidget(instructions)
178
+
179
+ # List widget to show saved labels
180
+ self.labels_list = QListWidget()
181
+ self.populate_labels_list()
182
+ layout.addWidget(self.labels_list)
183
+
184
+ # Buttons for managing labels
185
+ buttons_layout = QHBoxLayout()
186
+
187
+ self.edit_button = QPushButton("Edit Selected")
188
+ self.edit_button.clicked.connect(self.edit_selected_label)
189
+ buttons_layout.addWidget(self.edit_button)
190
+
191
+ self.delete_button = QPushButton("Delete Selected")
192
+ self.delete_button.clicked.connect(self.delete_selected_label)
193
+ buttons_layout.addWidget(self.delete_button)
194
+
195
+ self.refresh_button = QPushButton("Refresh")
196
+ self.refresh_button.clicked.connect(self.populate_labels_list)
197
+ buttons_layout.addWidget(self.refresh_button)
198
+
199
+ layout.addLayout(buttons_layout)
200
+
201
+ # Dialog buttons
202
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
203
+ button_box.rejected.connect(self.accept)
204
+ layout.addWidget(button_box)
205
+
206
+ def populate_labels_list(self):
207
+ """Populate the list with saved labels and their properties"""
208
+ self.labels_list.clear()
209
+
210
+ for label in self.properties_manager.get_all_labels():
211
+ props = self.properties_manager.get_label_property(label)
212
+ if props:
213
+ color_name = props['color'].name()
214
+ thickness = props['thickness']
215
+
216
+ item_text = f"{label} (Color: {color_name}, Thickness: {thickness}px)"
217
+ item = QListWidgetItem(item_text)
218
+
219
+ # Set the item's background color to match the label color
220
+ item.setBackground(props['color'])
221
+
222
+ # Set text color based on background brightness
223
+ text_color = QColor('white' if props['color'].lightness() < 128 else 'black')
224
+ item.setForeground(text_color)
225
+
226
+ # Store the label name for easy access
227
+ item.setData(Qt.ItemDataRole.UserRole, label)
228
+
229
+ self.labels_list.addItem(item)
230
+
231
+ def edit_selected_label(self):
232
+ """Edit the selected label's properties"""
233
+ current_item = self.labels_list.currentItem()
234
+ if not current_item:
235
+ QMessageBox.warning(self, "No Selection", "Please select a label to edit.")
236
+ return
237
+
238
+ label = current_item.data(Qt.ItemDataRole.UserRole)
239
+ props = self.properties_manager.get_label_property(label)
240
+
241
+ if props:
242
+ dialog = CustomizeRectangleDialog(props['color'], props['thickness'], self)
243
+ dialog.setWindowTitle(f"Edit Properties for '{label}'")
244
+
245
+ if dialog.exec() == QDialog.DialogCode.Accepted:
246
+ new_color, new_thickness = dialog.get_settings()
247
+ self.properties_manager.add_label_property(label, new_color, new_thickness)
248
+
249
+ # Update all existing rectangles with this label
250
+ self.update_rectangles_with_label(label, new_color, new_thickness)
251
+
252
+ self.populate_labels_list()
253
+ QMessageBox.information(self, "Success", f"Properties for '{label}' updated successfully!")
254
+
255
+ def update_rectangles_with_label(self, label, new_color, new_thickness):
256
+ """Update all existing rectangles that have the specified label"""
257
+ if hasattr(self.parent_widget, 'labeled_rectangles'):
258
+ for rect_item in self.parent_widget.labeled_rectangles:
259
+ if hasattr(rect_item, 'get_label') and rect_item.get_label() == label:
260
+ # Update the rectangle's appearance
261
+ new_pen = QPen(new_color, new_thickness)
262
+ rect_item.setPen(new_pen)
263
+
264
+ # Update stored properties on the rectangle
265
+ rect_item.set_color(new_color)
266
+ rect_item.set_thickness(new_thickness)
267
+
268
+ # Update the scene to reflect changes
269
+ self.parent_widget.scene.update()
270
+
271
+ def delete_selected_label(self):
272
+ """Delete the selected label's properties"""
273
+ current_item = self.labels_list.currentItem()
274
+ if not current_item:
275
+ QMessageBox.warning(self, "No Selection", "Please select a label to delete.")
276
+ return
277
+
278
+ label = current_item.data(Qt.ItemDataRole.UserRole)
279
+
280
+ reply = QMessageBox.question(self, "Confirm Deletion",
281
+ f"Are you sure you want to delete the properties for '{label}'?",
282
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
283
+ QMessageBox.StandardButton.No)
284
+
285
+ if reply == QMessageBox.StandardButton.Yes:
286
+ if self.properties_manager.remove_label_property(label):
287
+ self.populate_labels_list()
288
+ QMessageBox.information(self, "Success", f"Properties for '{label}' deleted successfully!")
289
+ else:
290
+ QMessageBox.warning(self, "Error", f"Failed to delete properties for '{label}'.")
291
+
292
+ class CustomizeRectangleDialog(QDialog):
293
+ """Dialog for customizing rectangle appearance"""
294
+ def __init__(self, current_color=None, current_thickness=2, parent=None):
295
+ super().__init__(parent)
296
+ self.setWindowTitle("Customize Rectangle")
297
+ self.setModal(True)
298
+ self.resize(300, 200)
299
+
300
+ # CONSISTENT DARK THEME STYLESHEET
301
+ self.setStyleSheet("""
302
+ QDialog {
303
+ background-color: #000000;
304
+ color: white;
305
+ font-size: 14px;
306
+ border: 1px solid #444444;
307
+ }
308
+ QLabel {
309
+ color: white;
310
+ background-color: transparent;
311
+ font-size: 12px;
312
+ }
313
+ QSpinBox {
314
+ background-color: #111111;
315
+ color: white;
316
+ border: 1px solid #555555;
317
+ padding: 5px;
318
+ }
319
+ QSpinBox:focus {
320
+ border: 1px solid #666666;
321
+ }
322
+ QPushButton {
323
+ background-color: #111111;
324
+ color: white;
325
+ border: 1px solid #666666;
326
+ border-radius: 5px;
327
+ padding: 6px 12px;
328
+ }
329
+ QPushButton:hover {
330
+ background-color: #222222;
331
+ }
332
+ QPushButton:pressed {
333
+ background-color: #333333;
334
+ }
335
+ QGroupBox {
336
+ color: white;
337
+ font-weight: bold;
338
+ border: 1px solid #444444;
339
+ margin-top: 10px;
340
+ padding-top: 10px;
341
+ background-color: transparent;
342
+ }
343
+ QGroupBox::title {
344
+ subcontrol-origin: margin;
345
+ left: 10px;
346
+ padding: 0 5px 0 5px;
347
+ color: white;
348
+ }
349
+ QDialogButtonBox QPushButton {
350
+ background-color: #111111;
351
+ color: white;
352
+ border: 1px solid #666666;
353
+ min-width: 80px;
354
+ padding: 6px 12px;
355
+ }
356
+ QDialogButtonBox QPushButton:hover {
357
+ background-color: #222222;
358
+ }
359
+ """)
360
+
361
+ layout = QVBoxLayout(self)
362
+
363
+ # Color selection group
364
+ color_group = QGroupBox("Rectangle Color")
365
+ color_layout = QFormLayout()
366
+
367
+ # Color selection
368
+ color_selection_layout = QHBoxLayout()
369
+ self.color_button = QPushButton("Choose Color")
370
+ self.color_button.clicked.connect(self.choose_color)
371
+
372
+ # Set initial color
373
+ self.selected_color = current_color if current_color else QColor(255, 0, 0) # Default red
374
+ self.update_color_button()
375
+
376
+ color_selection_layout.addWidget(self.color_button)
377
+ color_selection_layout.addStretch()
378
+
379
+ color_layout.addRow("Color:", color_selection_layout)
380
+ color_group.setLayout(color_layout)
381
+ layout.addWidget(color_group)
382
+
383
+ # Thickness selection group
384
+ thickness_group = QGroupBox("Rectangle Thickness")
385
+ thickness_layout = QFormLayout()
386
+
387
+ self.thickness_spinbox = QSpinBox()
388
+ self.thickness_spinbox.setMinimum(1)
389
+ self.thickness_spinbox.setMaximum(10)
390
+ self.thickness_spinbox.setValue(current_thickness)
391
+ self.thickness_spinbox.setSuffix(" px")
392
+
393
+ thickness_layout.addRow("Thickness:", self.thickness_spinbox)
394
+ thickness_group.setLayout(thickness_layout)
395
+ layout.addWidget(thickness_group)
396
+
397
+ # Dialog buttons
398
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
399
+ QDialogButtonBox.StandardButton.Cancel)
400
+ button_box.accepted.connect(self.accept)
401
+ button_box.rejected.connect(self.reject)
402
+
403
+ layout.addWidget(button_box)
404
+
405
+ def choose_color(self):
406
+ """Open color dialog to choose rectangle color"""
407
+ # Apply dark theme to color dialog
408
+ color_dialog = QColorDialog(self.selected_color, self)
409
+ color_dialog.setStyleSheet("""
410
+ QColorDialog {
411
+ background-color: #000000;
412
+ color: white;
413
+ }
414
+ QColorDialog QLabel {
415
+ color: white;
416
+ background-color: transparent;
417
+ }
418
+ QColorDialog QPushButton {
419
+ background-color: #111111;
420
+ color: white;
421
+ border: 1px solid #666666;
422
+ border-radius: 5px;
423
+ padding: 6px 12px;
424
+ }
425
+ QColorDialog QPushButton:hover {
426
+ background-color: #222222;
427
+ }
428
+ QColorDialog QSpinBox {
429
+ background-color: #111111;
430
+ color: white;
431
+ border: 1px solid #555555;
432
+ padding: 5px;
433
+ }
434
+ QColorDialog QLineEdit {
435
+ background-color: #222222;
436
+ color: white;
437
+ border: 1px solid #555555;
438
+ padding: 5px;
439
+ }
440
+ """)
441
+
442
+ if color_dialog.exec() == QDialog.DialogCode.Accepted:
443
+ self.selected_color = color_dialog.currentColor()
444
+ self.update_color_button()
445
+
446
+ def update_color_button(self):
447
+ """Update the color button to show the selected color"""
448
+ color_name = self.selected_color.name()
449
+ self.color_button.setText(f"Color: {color_name}")
450
+ # Use inline style for color button to override the general button style
451
+ text_color = 'white' if self.selected_color.lightness() < 128 else 'black'
452
+ self.color_button.setStyleSheet(f"""
453
+ QPushButton {{
454
+ background-color: {color_name};
455
+ color: {text_color};
456
+ border: 1px solid #555555;
457
+ padding: 6px 12px;
458
+ border-radius: 5px;
459
+ font-weight: bold;
460
+ }}
461
+ QPushButton:hover {{
462
+ border: 2px solid #777777;
463
+ background-color: {color_name};
464
+ }}
465
+ """)
466
+
467
+ def get_settings(self):
468
+ """Return the selected color and thickness"""
469
+ return self.selected_color, self.thickness_spinbox.value()
470
+
471
+ class RectangleTool:
472
+ def __init__(self):
473
+ # Default rectangle appearance settings
474
+ self.default_rect_color = QColor(255, 0, 0) # Red
475
+ self.default_rect_thickness = 2
476
+
477
+ def load_label_properties_to_dicts(self):
478
+ """Load saved label properties into the label_colors and label_thickness dictionaries"""
479
+ for label in self.label_properties_manager.get_all_labels():
480
+ props = self.label_properties_manager.get_label_property(label)
481
+ if props:
482
+ self.label_colors[label] = props['color']
483
+ self.label_thickness[label] = props['thickness']
484
+
485
+ def mouseDoubleClickEvent(self, event):
486
+ """Handle double click to exit movement, rotation, or modification mode"""
487
+ if hasattr(self, 'movement_mode') and self.movement_mode:
488
+ self.movement_mode = False
489
+ self.setCursor(Qt.CursorShape.ArrowCursor)
490
+
491
+ # Restore original pen
492
+ if hasattr(self, 'moving_rect') and hasattr(self.moving_rect, 'original_pen'):
493
+ self.moving_rect.setPen(self.moving_rect.original_pen)
494
+ self.moving_rect = None
495
+
496
+ elif hasattr(self, 'rotation_mode') and self.rotation_mode:
497
+ self.rotation_mode = False
498
+ self.setCursor(Qt.CursorShape.ArrowCursor)
499
+
500
+ # Restore original pen
501
+ if hasattr(self, 'rotating_rect') and hasattr(self.rotating_rect, 'original_pen'):
502
+ self.rotating_rect.setPen(self.rotating_rect.original_pen)
503
+ self.rotating_rect = None
504
+
505
+ elif hasattr(self, 'modification_mode') and self.modification_mode:
506
+ # Exit modification mode on double click
507
+ self.finish_modification()
508
+
509
+ def show_rectangle_context_menu(self, labeled_rect, global_pos):
510
+ """Show context menu with Move, Rotate, Delete, Custom, and Change Label options for a rectangle"""
511
+ context_menu = QMenu(self)
512
+
513
+ # CONSISTENT DARK THEME FOR CONTEXT MENU
514
+ context_menu.setStyleSheet("""
515
+ QMenu {
516
+ background-color: #000000;
517
+ color: white;
518
+ border: 1px solid #444444;
519
+ font-size: 14px;
520
+ }
521
+ QMenu::item {
522
+ padding: 8px 20px;
523
+ background-color: transparent;
524
+ }
525
+ QMenu::item:selected {
526
+ background-color: #222222;
527
+ }
528
+ QMenu::separator {
529
+ height: 1px;
530
+ background-color: #444444;
531
+ margin: 2px 0px;
532
+ }
533
+ """)
534
+
535
+ # Create actions
536
+ move_action = context_menu.addAction("Move")
537
+ rotate_action = context_menu.addAction("Rotate")
538
+ modify_action = context_menu.addAction("Modify")
539
+ context_menu.addSeparator()
540
+ custom_action = context_menu.addAction("Custom")
541
+ if self.rectangle_mode_type == 'classification':
542
+ change_label_action = context_menu.addAction("Change Label")
543
+ context_menu.addSeparator()
544
+ manage_labels_action = context_menu.addAction("Manage Label Properties")
545
+ context_menu.addSeparator()
546
+ delete_action = context_menu.addAction("Delete")
547
+
548
+ # Execute the menu and handle the selected action
549
+ action = context_menu.exec(global_pos)
550
+
551
+ if action == move_action:
552
+ self.enable_rectangle_movement(labeled_rect)
553
+ elif action == rotate_action:
554
+ self.enable_rectangle_rotation(labeled_rect)
555
+ elif action == modify_action:
556
+ self.enable_rectangle_modification(labeled_rect)
557
+ elif action == custom_action:
558
+ self.customize_rectangle(labeled_rect)
559
+ elif action == delete_action:
560
+ self.delete_rectangle(labeled_rect)
561
+ elif self.rectangle_mode_type == 'classification':
562
+ if action == manage_labels_action:
563
+ self.manage_label_properties()
564
+ if action == change_label_action:
565
+ self.change_rectangle_label(labeled_rect)
566
+
567
+ def manage_label_properties(self):
568
+ """Show dialog to manage saved label properties"""
569
+ dialog = ManageLabelPropertiesDialog(self.label_properties_manager, self)
570
+ dialog.exec()
571
+
572
+ # Reload properties into dictionaries after management
573
+ self.load_label_properties_to_dicts()
574
+
575
+ def change_rectangle_label(self, labeled_rect):
576
+ """Show dialog to change the label of the rectangle and update its color"""
577
+ # Get the current label
578
+ current_label = labeled_rect.get_label() if hasattr(labeled_rect, 'get_label') else ""
579
+
580
+ # Create a dialog to select a new label from existing labels
581
+ dialog = QDialog(self)
582
+ dialog.setWindowTitle("Change Label")
583
+ dialog.setObjectName("CustomDialog")
584
+
585
+ layout = QVBoxLayout()
586
+
587
+ # Add a combo box with existing labels
588
+ combo = QComboBox()
589
+ if hasattr(self, 'recent_labels') and self.recent_labels:
590
+ combo.addItems(self.recent_labels)
591
+ combo.setEditable(True)
592
+ combo.setCurrentText(current_label)
593
+
594
+ label = QLabel("Select an existing label or type a new one:")
595
+ layout.addWidget(label)
596
+ layout.addWidget(combo)
597
+
598
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
599
+ button_box.accepted.connect(dialog.accept)
600
+ button_box.rejected.connect(dialog.reject)
601
+ layout.addWidget(button_box)
602
+
603
+ dialog.setLayout(layout)
604
+
605
+ # Apply black theme stylesheet
606
+ dialog.setStyleSheet("""
607
+ QDialog#CustomDialog {
608
+ background-color: #000000;
609
+ color: white;
610
+ font-size: 14px;
611
+ border: 1px solid #444444;
612
+ }
613
+ QLabel {
614
+ color: white;
615
+ background-color: transparent;
616
+ }
617
+ QComboBox {
618
+ background-color: #111111;
619
+ color: white;
620
+ border: 1px solid #555555;
621
+ padding: 5px;
622
+ }
623
+ QComboBox QAbstractItemView {
624
+ background-color: #000000;
625
+ color: white;
626
+ selection-background-color: #222222;
627
+ }
628
+ QDialogButtonBox QPushButton {
629
+ background-color: #111111;
630
+ color: white;
631
+ border: 1px solid #666666;
632
+ min-width: 80px;
633
+ padding: 6px 12px;
634
+ }
635
+ QDialogButtonBox QPushButton:hover {
636
+ background-color: #222222;
637
+ }
638
+ """)
639
+
640
+ # Execute the dialog
641
+ if dialog.exec() == QDialog.DialogCode.Accepted:
642
+ new_label = combo.currentText().strip()
643
+ if new_label:
644
+ # Update the label
645
+ labeled_rect.set_label(new_label)
646
+
647
+ # Define color mapping logic
648
+ new_color = self.label_colors.get(new_label, labeled_rect.get_color())
649
+ labeled_rect.set_color(new_color)
650
+
651
+ new_thickness = self.label_thickness.get(new_label)
652
+ labeled_rect.set_thickness(new_thickness)
653
+
654
+ self.label_properties_manager.add_label_property(new_label, new_color, new_thickness)
655
+
656
+ self.scene.update()
657
+
658
+ self.save_rectangle_to_jpeg(labeled_rect)
659
+
660
+ def customize_rectangle(self, labeled_rect):
661
+ """Show dialog to customize rectangle color and thickness"""
662
+ # Get current rectangle settings
663
+ current_pen = labeled_rect.pen()
664
+ current_color = current_pen.color()
665
+ current_thickness = current_pen.width()
666
+
667
+ # Show customization dialog
668
+ dialog = CustomizeRectangleDialog(current_color, current_thickness, self)
669
+
670
+ if dialog.exec() == QDialog.DialogCode.Accepted:
671
+ # Apply the new settings
672
+ new_color, new_thickness = dialog.get_settings()
673
+ new_pen = QPen(new_color, new_thickness)
674
+
675
+ # Store the original pen if not already stored
676
+ if not hasattr(labeled_rect, 'original_pen'):
677
+ labeled_rect.original_pen = current_pen
678
+
679
+ # Apply the new pen
680
+ labeled_rect.setPen(new_pen)
681
+
682
+ # Store the custom settings on the rectangle for future reference
683
+ labeled_rect.custom_color = new_color
684
+ labeled_rect.custom_thickness = new_thickness
685
+
686
+ # Update the scene
687
+ self.scene.update()
688
+
689
+ def set_default_rectangle_style(self):
690
+ """Show dialog to set default rectangle color and thickness for new rectangles"""
691
+ dialog = CustomizeRectangleDialog(self.default_rect_color, self.default_rect_thickness, self)
692
+ dialog.setWindowTitle("Set Default Rectangle Style")
693
+
694
+ if dialog.exec() == QDialog.DialogCode.Accepted:
695
+ self.default_rect_color, self.default_rect_thickness = dialog.get_settings()
696
+ print(f"Default rectangle style updated: Color={self.default_rect_color.name()}, Thickness={self.default_rect_thickness}px")
697
+
698
+ def create_rectangle_with_default_style(self, rect):
699
+ """Create a rectangle with the current default style"""
700
+ pen = QPen(self.default_rect_color, self.default_rect_thickness)
701
+ rectangle_item = self.scene.addRect(rect, pen)
702
+
703
+ # Store the default settings on the rectangle
704
+ rectangle_item.custom_color = self.default_rect_color
705
+ rectangle_item.custom_thickness = self.default_rect_thickness
706
+
707
+ return rectangle_item
708
+
709
+ def enable_rectangle_movement(self, labeled_rect):
710
+ """Enable movement mode for the selected rectangle"""
711
+ # Store the rectangle being moved
712
+ self.moving_rect = labeled_rect
713
+ self.movement_mode = True
714
+
715
+ # Change cursor to indicate movement mode
716
+ self.setCursor(Qt.CursorShape.SizeAllCursor)
717
+
718
+ # Store original pen to restore later
719
+ if not hasattr(labeled_rect, 'original_pen'):
720
+ labeled_rect.original_pen = labeled_rect.pen()
721
+
722
+ # Highlight the rectangle being moved
723
+ highlight_pen = QPen(QColor(255, 255, 0), 2) # Yellow highlight
724
+ labeled_rect.setPen(highlight_pen)
725
+
726
+ # Store the rectangle's current center in scene coordinates
727
+ # This accounts for any rotation or transformation
728
+ rect_center_local = labeled_rect.rect().center()
729
+ self.rect_center_scene = labeled_rect.mapToScene(rect_center_local)
730
+
731
+ # Store current mouse position
732
+ current_mouse_pos = self.mapToScene(self.mapFromGlobal(QCursor.pos()))
733
+ self.mouse_offset = QPointF(
734
+ current_mouse_pos.x() - self.rect_center_scene.x(),
735
+ current_mouse_pos.y() - self.rect_center_scene.y()
736
+ )
737
+
738
+ def enable_rectangle_modification(self, labeled_rect):
739
+ """Enable modification mode for the selected rectangle (resize)"""
740
+ # Store the rectangle being modified
741
+ self.modifying_rect = labeled_rect
742
+ self.modification_mode = True
743
+
744
+ # Change cursor to indicate modification mode
745
+ self.setCursor(Qt.CursorShape.SizeFDiagCursor)
746
+
747
+ # Store original pen to restore later
748
+ if not hasattr(labeled_rect, 'original_pen'):
749
+ labeled_rect.original_pen = labeled_rect.pen()
750
+
751
+ # Highlight the rectangle being modified with a different color
752
+ highlight_pen = QPen(QColor(0, 255, 255), 2) # Cyan highlight for modify mode
753
+ labeled_rect.setPen(highlight_pen)
754
+
755
+ # Store the original rectangle dimensions and position
756
+ self.original_rect = labeled_rect.rect()
757
+ self.original_scene_rect = labeled_rect.sceneBoundingRect()
758
+
759
+ # Store the initial mouse position
760
+ mouse_pos = self.mapToScene(self.mapFromGlobal(QCursor.pos()))
761
+ self.initial_mouse_pos = mouse_pos
762
+
763
+ # Determine which corner/edge is closest to the mouse for resizing
764
+ rect_scene = labeled_rect.sceneBoundingRect()
765
+ self.resize_handle = self.get_resize_handle(mouse_pos, rect_scene)
766
+
767
+ def get_resize_handle(self, mouse_pos, rect_scene):
768
+ """Determine which resize handle (corner/edge) is closest to the mouse"""
769
+ # Define resize handles (corners and edges)
770
+ handles = {
771
+ 'top_left': rect_scene.topLeft(),
772
+ 'top_right': rect_scene.topRight(),
773
+ 'bottom_left': rect_scene.bottomLeft(),
774
+ 'bottom_right': rect_scene.bottomRight(),
775
+ 'top': QPointF(rect_scene.center().x(), rect_scene.top()),
776
+ 'bottom': QPointF(rect_scene.center().x(), rect_scene.bottom()),
777
+ 'left': QPointF(rect_scene.left(), rect_scene.center().y()),
778
+ 'right': QPointF(rect_scene.right(), rect_scene.center().y())
779
+ }
780
+
781
+ # Find the closest handle
782
+ min_distance = float('inf')
783
+ closest_handle = 'bottom_right' # Default to bottom-right corner
784
+
785
+ for handle_name, handle_pos in handles.items():
786
+ distance = QLineF(mouse_pos, handle_pos).length()
787
+ if distance < min_distance:
788
+ min_distance = distance
789
+ closest_handle = handle_name
790
+
791
+ return closest_handle
792
+
793
+ def enable_rectangle_movement(self, labeled_rect):
794
+ """Enable movement mode for the selected rectangle"""
795
+ # Store the rectangle being moved
796
+ self.moving_rect = labeled_rect
797
+ self.movement_mode = True
798
+
799
+ # Change cursor to indicate movement mode
800
+ self.setCursor(Qt.CursorShape.SizeAllCursor)
801
+
802
+ # Store original pen to restore later
803
+ if not hasattr(labeled_rect, 'original_pen'):
804
+ labeled_rect.original_pen = labeled_rect.pen()
805
+
806
+ # Highlight the rectangle being moved
807
+ highlight_pen = QPen(QColor(255, 255, 0), 2) # Yellow highlight
808
+ labeled_rect.setPen(highlight_pen)
809
+
810
+ # Store the rectangle's current center in scene coordinates
811
+ # This accounts for any rotation or transformation
812
+ rect_center_local = labeled_rect.rect().center()
813
+ self.rect_center_scene = labeled_rect.mapToScene(rect_center_local)
814
+
815
+ # Store current mouse position
816
+ current_mouse_pos = self.mapToScene(self.mapFromGlobal(QCursor.pos()))
817
+ self.mouse_offset = QPointF(
818
+ current_mouse_pos.x() - self.rect_center_scene.x(),
819
+ current_mouse_pos.y() - self.rect_center_scene.y()
820
+ )
821
+
822
+ def enable_rectangle_rotation(self, labeled_rect):
823
+ """Enable rotation mode for the selected rectangle"""
824
+ # Store the rectangle being rotated
825
+ self.rotating_rect = labeled_rect
826
+ self.rotation_mode = True
827
+
828
+ # Change cursor to indicate rotation mode
829
+ self.setCursor(Qt.CursorShape.CrossCursor)
830
+
831
+ # Store original pen to restore later
832
+ if not hasattr(labeled_rect, 'original_pen'):
833
+ labeled_rect.original_pen = labeled_rect.pen()
834
+
835
+ # Highlight the rectangle being rotated
836
+ highlight_pen = QPen(QColor(255, 255, 0), 2) # Yellow highlight
837
+ labeled_rect.setPen(highlight_pen)
838
+
839
+ # Calculate the center of the rectangle (intersection of diagonals)
840
+ rect = labeled_rect.rect()
841
+ center_local = rect.center()
842
+
843
+ # Set the transformation origin to the center of the rectangle
844
+ # This is crucial for proper rotation around the center
845
+ labeled_rect.setTransformOriginPoint(center_local)
846
+
847
+ # Convert center to scene coordinates for mouse angle calculations
848
+ self.rect_center = labeled_rect.mapToScene(center_local)
849
+
850
+ # Store the initial angle of the rectangle
851
+ self.initial_angle = labeled_rect.rotation()
852
+
853
+ # Store the initial angle between center and mouse
854
+ mouse_pos = self.mapToScene(self.mapFromGlobal(QCursor.pos()))
855
+ line = QLineF(self.rect_center, mouse_pos)
856
+ self.initial_mouse_angle = line.angle()
857
+
858
+ def delete_rectangle(self, labeled_rect):
859
+ """Delete the selected rectangle"""
860
+ if labeled_rect in self.labeled_rectangles:
861
+ self.labeled_rectangles.remove(labeled_rect)
862
+ self.scene.removeItem(labeled_rect)
863
+
864
+ def finish_modification(self):
865
+ """Finish the modification operation"""
866
+ if self.modifying_rect:
867
+ # Restore original pen
868
+ if hasattr(self.modifying_rect, 'original_pen'):
869
+ self.modifying_rect.setPen(self.modifying_rect.original_pen)
870
+
871
+ # Clear modification state
872
+ self.modifying_rect = None
873
+ self.modification_mode = False
874
+ self.resize_handle = None
875
+
876
+ # Restore normal cursor
877
+ self.setCursor(Qt.CursorShape.ArrowCursor)
878
+
879
+ def save_rectangle_to_jpeg(self, labeled_rect):
880
+ """Save the selected rectangle portion of the image to a JPEG file"""
881
+ # Get the bounding rectangle
882
+ rect = labeled_rect.rect()
883
+
884
+ # Convert scene coordinates to pixmap coordinates
885
+ scene_rect = QRectF(rect)
886
+
887
+ # Create a QImage with the exact size of the rectangle
888
+ image = QImage(int(rect.width()), int(rect.height()), QImage.Format.Format_RGB888)
889
+ image.fill(Qt.GlobalColor.white) # Set background color
890
+
891
+ # Create a painter for the image
892
+ painter = QPainter(image)
893
+
894
+ # Set up the rendering to extract just the portion we want
895
+ source_rect = scene_rect
896
+ target_rect = QRectF(0, 0, rect.width(), rect.height())
897
+
898
+ # Render only the portion of the scene we want
899
+ self.scene.render(painter, target_rect, source_rect)
900
+ painter.end()
901
+
902
+ # Check rectangle mode type for different saving behavior
903
+ if hasattr(self, 'rectangle_mode_type'):
904
+ if self.rectangle_mode_type == "classification":
905
+ # Classification mode - use label for folder structure
906
+ save_dir = os.path.join(os.getcwd(), 'save', labeled_rect.label)
907
+ if not os.path.exists(save_dir):
908
+ os.makedirs(save_dir)
909
+
910
+ # Generate a unique file name with label
911
+ file_name = f"{labeled_rect.label}_{int(rect.x())}_{int(rect.y())}.jpeg"
912
+ file_path = os.path.join(save_dir, file_name)
913
+
914
+ # Store the label for reuse
915
+ if hasattr(self, 'recent_labels'):
916
+ if labeled_rect.label not in self.recent_labels:
917
+ self.recent_labels.append(labeled_rect.label)
918
+ else:
919
+ self.recent_labels = [labeled_rect.label]
920
+
921
+ elif self.rectangle_mode_type == "yolo":
922
+ # YOLO mode - save in general yolo folder without label
923
+ save_dir = os.path.join(os.getcwd(), 'save', 'yolo')
924
+ if not os.path.exists(save_dir):
925
+ os.makedirs(save_dir)
926
+
927
+ # Generate a unique file name without label
928
+ file_name = f"yolo_{int(rect.x())}_{int(rect.y())}.jpeg"
929
+ file_path = os.path.join(save_dir, file_name)
930
+ else:
931
+ # Fallback to original behavior
932
+ save_dir = os.path.join(os.getcwd(), 'save', 'default')
933
+ if not os.path.exists(save_dir):
934
+ os.makedirs(save_dir)
935
+ file_name = f"rect_{int(rect.x())}_{int(rect.y())}.jpeg"
936
+ file_path = os.path.join(save_dir, file_name)
937
+
938
+ # Save the image as JPEG
939
+ if image.save(file_path, "JPEG"):
940
+ print(f"Image saved successfully at {file_path}")
941
+ else:
942
+ print("Failed to save the image.")
943
+
944
+ def save_entire_image_with_rectangles(self):
945
+ """Save the entire image with rectangles drawn on it."""
946
+ if not hasattr(self, 'base_pixmap') or self.base_pixmap is None:
947
+ print("No base image to save.")
948
+ return False
949
+
950
+ # Create a QImage with the same size as the base pixmap
951
+ image = QImage(self.base_pixmap.size(), QImage.Format.Format_ARGB32)
952
+ image.fill(Qt.GlobalColor.transparent) # Start with a transparent background
953
+
954
+ # Create a painter for the image
955
+ painter = QPainter(image)
956
+
957
+ # Draw the base image
958
+ painter.drawPixmap(0, 0, self.base_pixmap)
959
+
960
+ # Draw all rectangles with their custom colors and thickness
961
+ if hasattr(self, 'labeled_rectangles'):
962
+ for rect_item in self.labeled_rectangles:
963
+ rect = rect_item.rect()
964
+ pen = rect_item.pen()
965
+ painter.setPen(pen) # Use the rectangle's custom pen
966
+ painter.drawRect(rect)
967
+
968
+ if hasattr(self, 'rectangle_items'):
969
+ for rect_item in self.rectangle_items:
970
+ rect = rect_item.rect()
971
+ pen = rect_item.pen()
972
+ painter.setPen(pen) # Use the rectangle's custom pen
973
+ painter.drawRect(rect)
974
+
975
+ if hasattr(self, 'polygon_items'):
976
+ for polygon_item in self.polygon_items:
977
+ polygon = polygon_item.polygon()
978
+ pen = polygon_item.pen()
979
+ painter.setPen(pen) # Use the polygon's custom pen
980
+ painter.drawPolygon(polygon)
981
+
982
+ painter.end()
983
+
984
+ # Create the folder to save the image
985
+ save_dir = os.path.join(os.getcwd(), 'save')
986
+ if not os.path.exists(save_dir):
987
+ os.makedirs(save_dir)
988
+
989
+ # Generate a unique file name
990
+ file_name = f"entire_image_with_shapes_{int(time.time())}.png"
991
+ file_path = os.path.join(save_dir, file_name)
992
+
993
+ # Save the image as PNG
994
+ if image.save(file_path, "PNG"):
995
+ print(f"Image saved successfully at {file_path}")
996
+ return True
997
+ else:
998
+ print("Failed to save the image.")
999
+ return False
1000
+
1001
+ def toggle_rectangle_mode(self, enabled):
1002
+ """Toggle rectangle selection mode on/off and clear any active selection"""
1003
+ self.rectangle_mode = enabled
1004
+
1005
+ # Clear any active rectangle selection when toggling the mode
1006
+ if self.current_rect:
1007
+ self.scene.removeItem(self.current_rect)
1008
+ self.current_rect = None
1009
+
1010
+ # Reset the starting point
1011
+ self.rect_start = None
1012
+
1013
+ def clear_rectangles(self):
1014
+ """Remove all rectangle selections from the scene"""
1015
+ # Clear rectangle_items if it exists
1016
+ if hasattr(self, 'rectangle_items') and self.rectangle_items:
1017
+ for item in self.rectangle_items:
1018
+ if item in self.scene.items():
1019
+ self.scene.removeItem(item)
1020
+ self.rectangle_items = []
1021
+
1022
+ # Clear labeled_rectangles
1023
+ if hasattr(self, 'labeled_rectangles') and self.labeled_rectangles:
1024
+ for item in self.labeled_rectangles:
1025
+ if item in self.scene.items():
1026
+ self.scene.removeItem(item)
1027
+ self.labeled_rectangles = []
1028
+
1029
+ # Clear any temporary rectangle being drawn
1030
+ if hasattr(self, 'current_rect') and self.current_rect:
1031
+ if self.current_rect in self.scene.items():
1032
+ self.scene.removeItem(self.current_rect)
1033
+ self.current_rect = None
1034
+
1035
+ # Update the scene to reflect the changes
1036
+ self.scene.update()