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,1214 @@
1
+ import cv2
2
+ import numpy as np
3
+ import sys
4
+ import os
5
+ import time
6
+ from PyQt6.QtWidgets import (
7
+ QGraphicsEllipseItem, QComboBox, QGraphicsRectItem, QInputDialog, QGraphicsItem, QGraphicsItemGroup, QGraphicsPixmapItem, QGraphicsOpacityEffect, QGraphicsView, QGraphicsScene, QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
8
+ QFileDialog, QWidget, QMessageBox, QHBoxLayout, QColorDialog, QDialog, QSlider, QFormLayout, QDialogButtonBox, QGridLayout, QProgressDialog, QCheckBox, QSpinBox, QSplashScreen, QMenu, QLineEdit, QFrame
9
+ )
10
+ from PyQt6.QtGui import QPixmap, QMouseEvent, QImage, QPainter, QColor, QPen, QBrush, QCursor, QIcon, QPainterPath, QFont
11
+ from PyQt6.QtCore import Qt, QPoint, QPointF, QTimer, QThread, pyqtSignal, QSize, QRectF, QObject, QLineF
12
+ import gc
13
+ import math
14
+ import traceback
15
+
16
+ from models.LabeledRectangle import LabeledRectangle
17
+ from models.PointItem import PointItem
18
+ from models.ProcessWorker import ProcessWorker
19
+ from models.OverlayOpacityDialog import OverlayOpacityDialog
20
+ from models.tools.PaintTool import PaintTool
21
+ from models.tools.EraserTool import EraserTool
22
+ from models.tools.MagicPenTool import MagicPenTool
23
+ from models.tools.OverlayTool import OverlayTool
24
+ from models.tools.RectangleTool import RectangleTool, LabelPropertiesManager, LabelRectanglePropertiesDialog
25
+ from models.tools.ContourTool import ContourTool
26
+ from models.tools.PolygonTool import PolygonTool, LabelPolygonPropertiesDialog
27
+
28
+ class ZoomableGraphicsView(QGraphicsView, PaintTool, EraserTool, MagicPenTool, OverlayTool, RectangleTool, ContourTool, PolygonTool):
29
+ def __init__(self, parent=None):
30
+ super().__init__(parent)
31
+
32
+ # Create scene
33
+ self.scene = QGraphicsScene(self)
34
+ self.setScene(self.scene)
35
+
36
+ # Setup view properties for best performance
37
+ self.setRenderHint(QPainter.RenderHint.Antialiasing, True)
38
+ self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
39
+ self.setOptimizationFlag(QGraphicsView.OptimizationFlag.DontAdjustForAntialiasing, True)
40
+ self.setOptimizationFlag(QGraphicsView.OptimizationFlag.DontSavePainterState, True)
41
+ self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.MinimalViewportUpdate)
42
+ self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
43
+ self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
44
+ self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
45
+
46
+ # Basic properties
47
+ self.zoom_factor = 1.0
48
+ self.base_pixmap = None
49
+ self.pixmap_item = None
50
+ self.base_pixmap_item = None
51
+ self.overlay_pixmap_item = None
52
+ self.raw_image = None
53
+ self.is_moving = False
54
+ self.last_mouse_pos = None
55
+ self.setMouseTracking(True)
56
+ self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
57
+
58
+ # Drawing properties
59
+ self.points = [] # List of PointItem objects
60
+ self.paint_mode = False
61
+ self.erase_mode = False
62
+ self.magic_pen_mode = False
63
+ self.point_radius = 3
64
+ self.point_color = QColor(255, 0, 0)
65
+ self.point_opacity = 100
66
+ self.point_label = ""
67
+ self.eraser_size = 10
68
+ self.absolute_erase_mode = False
69
+ self.magic_pen_tolerance = 20
70
+ self.max_points_limite = 100000
71
+ self.shape_fill_mode = False
72
+
73
+ # Processing
74
+ self.process_timeout = 10 # 10 seconds default timeout
75
+ self.worker = None
76
+
77
+ # Display properties
78
+ self.overlay_opacity = 255
79
+ self.points_opacity = 255
80
+ self.opacity_dialog = None
81
+ self.drawn_points_visible = True
82
+
83
+ # History
84
+ self.points_history = []
85
+ self.current_stroke = []
86
+ self.erased_points_history = []
87
+ self.MAX_HISTORY_SIZE = 5
88
+
89
+ # Memory management
90
+ self.operation_count = 0
91
+ self.memory_threshold = 10
92
+
93
+ self.input_cooldown_timer = QTimer()
94
+ self.input_cooldown_timer.setInterval(50) # 50ms delay
95
+ self.input_cooldown_timer.setSingleShot(True)
96
+ self.input_cooldown_timer.timeout.connect(self.process_delayed_input)
97
+ self.last_input_event = None
98
+
99
+ self.gc_timer = QTimer()
100
+ self.gc_timer.setInterval(10000) # Every 10 seconds
101
+ self.gc_timer.timeout.connect(gc.collect)
102
+ self.gc_timer.start()
103
+
104
+ self.rect_start = None
105
+ self.current_rect = None
106
+ self.rectangle_mode = False
107
+ self.labeled_rectangles = []
108
+ self.last_used_label = None
109
+ self.label_thickness = {}
110
+ self.label_properties_manager = LabelPropertiesManager()
111
+
112
+ self.default_polygon_color = QColor(255, 0, 0) # Green
113
+ self.default_polygon_thickness = 2
114
+
115
+ # Polygon creation state
116
+ self.polygon_mode = False
117
+ self.current_polygon_points = []
118
+ self.current_polygon_lines = []
119
+ self.current_polygon = None
120
+ self.polygon_items = []
121
+ self.labeled_polygons = []
122
+ self.polygon_edit_mode = False
123
+ self.editing_polygon = None
124
+ self.polygon_point_items = []
125
+ self.dragging_polygon_point = False
126
+ self.dragged_point_item = None
127
+
128
+ # Polygon manipulation modes
129
+ self.close_distance_threshold = 10
130
+
131
+ self.erase_timer = QTimer()
132
+ self.erase_timer.setInterval(5) # Mise à jour toutes les 50ms
133
+ self.erase_timer.setSingleShot(True)
134
+ self.erase_timer.timeout.connect(self.scene.update)
135
+
136
+ def process_delayed_input(self):
137
+ """Process stored input event after cooldown"""
138
+ if isinstance(self.last_input_event, QMouseEvent):
139
+ # Handle mouse movement (panning, zooming, drawing)
140
+ pass
141
+
142
+ def setBasePixmap(self, pixmap):
143
+ """Set the base image pixmap"""
144
+ self.base_pixmap = pixmap
145
+ self.raw_image = pixmap.toImage()
146
+
147
+ # Clear existing scene
148
+ self.scene.clear()
149
+ self.points = []
150
+
151
+ # Create pixmap item
152
+ self.pixmap_item = self.scene.addPixmap(pixmap)
153
+ self.pixmap_item.setZValue(0) # Base layer
154
+
155
+ # Reset view
156
+ self.setSceneRect(self.pixmap_item.boundingRect())
157
+ self.fitInView(self.pixmap_item.boundingRect(), Qt.AspectRatioMode.KeepAspectRatio)
158
+ self.zoom_factor = 1.0
159
+
160
+ # Reset transformations
161
+ self.resetTransform()
162
+
163
+ def reset_view(self):
164
+ """Reset view to original state"""
165
+ if self.base_pixmap:
166
+ self.resetTransform()
167
+ self.zoom_factor = 1.0
168
+ self.setSceneRect(self.pixmap_item.boundingRect())
169
+ self.fitInView(self.pixmap_item.boundingRect(), Qt.AspectRatioMode.KeepAspectRatio)
170
+
171
+ def wheelEvent(self, event):
172
+ """Handle mouse wheel for zooming centered on cursor position"""
173
+ if not self.base_pixmap:
174
+ return
175
+
176
+ # Calculate zoom factor
177
+ zoom_in = event.angleDelta().y() > 0
178
+ factor = 1.1 if zoom_in else 0.9
179
+
180
+ # Apply zoom factor limit
181
+ new_zoom_factor = self.zoom_factor * factor
182
+ if 0.9 <= new_zoom_factor <= 40.0:
183
+ self.zoom_factor = new_zoom_factor
184
+
185
+ # Get the scene position under the mouse
186
+ mouse_pos = event.position().toPoint()
187
+ scene_pos = self.mapToScene(mouse_pos)
188
+
189
+ # First reset the transformation anchor
190
+ self.setTransformationAnchor(QGraphicsView.ViewportAnchor.NoAnchor)
191
+
192
+ # Apply the scale
193
+ self.scale(factor, factor)
194
+
195
+ # Get the new position in viewport coordinates where scene_pos would show
196
+ new_viewport_pos = self.mapFromScene(scene_pos)
197
+
198
+ # Calculate the viewport delta and adjust the view
199
+ delta = new_viewport_pos - mouse_pos
200
+ self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + delta.x())
201
+ self.verticalScrollBar().setValue(self.verticalScrollBar().value() + delta.y())
202
+
203
+ # Prevent standard event handling
204
+ event.accept()
205
+
206
+ def mousePressEvent(self, event):
207
+ """Handle mouse press events"""
208
+ if not self.base_pixmap:
209
+ return
210
+
211
+ self.last_mouse_pos = event.position()
212
+ scene_pos = self.mapToScene(int(event.position().x()), int(event.position().y()))
213
+
214
+ if not self.input_cooldown_timer.isActive():
215
+ self.input_cooldown_timer.start()
216
+
217
+ if self.paint_mode and event.button() == Qt.MouseButton.LeftButton:
218
+ self.add_point(scene_pos)
219
+ self.is_painting = True
220
+ elif self.magic_pen_mode and event.button() == Qt.MouseButton.LeftButton:
221
+ self.fill_shape(scene_pos)
222
+ elif self.erase_mode and event.button() == Qt.MouseButton.LeftButton:
223
+ self.erase_point(scene_pos)
224
+ self.is_erasing = True
225
+ elif self.polygon_mode and hasattr(self, 'polygon_edit_mode') and self.polygon_edit_mode:
226
+ if event.button() == Qt.MouseButton.RightButton:
227
+ self.end_polygon_edit_mode()
228
+ return
229
+ elif event.button() == Qt.MouseButton.LeftButton:
230
+ # Check if we clicked on a polygon point
231
+ item = self.scene.itemAt(scene_pos, self.transform())
232
+ if item and hasattr(item, 'vertex_index'):
233
+ self.dragging_polygon_point = True
234
+ self.dragged_point_item = item
235
+ return
236
+ elif self.polygon_mode and event.button() == Qt.MouseButton.LeftButton:
237
+ # Check if we're close to the first point to close the polygon
238
+ if hasattr(self, "current_polygon_points") and len(self.current_polygon_points) >= 3:
239
+ first_point = self.current_polygon_points[0]
240
+ distance = math.sqrt((scene_pos.x() - first_point.x())**2 +
241
+ (scene_pos.y() - first_point.y())**2)
242
+
243
+ if distance <= self.close_distance_threshold:
244
+ self.close_polygon()
245
+ label = PolygonTool.last_used_label if hasattr(PolygonTool, 'last_used_label') else None
246
+ color = self.default_polygon_color if hasattr(self, 'default_polygon_color') else QColor(0, 255, 0)
247
+ thickness = self.default_polygon_thickness if hasattr(self, 'default_polygon_thickness') else 2
248
+ self.show_polygon_label_properties(label, color, thickness)
249
+ return
250
+
251
+ # Add new point to current polygon
252
+ self.add_polygon_point(scene_pos)
253
+
254
+ elif self.polygon_mode and event.button() == Qt.MouseButton.RightButton:
255
+ # Right click to close polygon (if we have at least 3 points)
256
+ if len(self.current_polygon_points) >= 3:
257
+ self.close_polygon()
258
+ else:
259
+ # Cancel current polygon creation
260
+ self.cancel_polygon_creation()
261
+
262
+ elif self.rectangle_mode and event.button() == Qt.MouseButton.LeftButton:
263
+ self.rect_start = self.mapToScene(event.pos())
264
+ elif event.button() == Qt.MouseButton.LeftButton and not self.paint_mode and not self.erase_mode:
265
+ self.is_moving = True
266
+ self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
267
+ if hasattr(self, "shape_fill_mode") and self.shape_fill_mode:
268
+ self.fill_contour()
269
+ return
270
+
271
+ super().mousePressEvent(event)
272
+
273
+ def mouseMoveEvent(self, event):
274
+ """Handle mouse movement"""
275
+ if not self.base_pixmap:
276
+ return
277
+
278
+ scene_pos = self.mapToScene(int(event.position().x()), int(event.position().y()))
279
+
280
+ if self.paint_mode and event.buttons() & Qt.MouseButton.LeftButton:
281
+ if self.last_mouse_pos:
282
+ last_scene_pos = self.mapToScene(int(self.last_mouse_pos.x()),
283
+ int(self.last_mouse_pos.y()))
284
+ self.draw_continuous_line(last_scene_pos, scene_pos)
285
+ self.last_mouse_pos = event.position()
286
+ elif self.erase_mode and event.buttons() & Qt.MouseButton.LeftButton:
287
+ self.erase_point(scene_pos)
288
+ elif self.polygon_mode and hasattr(self, 'current_polygon_points ') and self.current_polygon_points:
289
+ self.update_polygon_preview(scene_pos)
290
+ elif self.rectangle_mode and self.rect_start:
291
+ scene_pos = self.mapToScene(event.pos())
292
+ rect = QRectF(self.rect_start, scene_pos).normalized()
293
+ if self.current_rect:
294
+ self.scene.removeItem(self.current_rect)
295
+ self.current_rect = QGraphicsRectItem(rect)
296
+ self.current_rect.setPen(QPen(QColor(0, 255, 0), 2)) # Green outline while drawing
297
+ self.scene.addItem(self.current_rect)
298
+ elif self.polygon_mode and hasattr(self, 'dragging_polygon_point') and self.dragging_polygon_point and self.dragged_point_item:
299
+ # Move the dragged point to the new position
300
+ new_pos = scene_pos - self.dragged_point_item.boundingRect().center()
301
+ self.dragged_point_item.setPos(new_pos)
302
+ self.update_polygon_from_points()
303
+ return
304
+
305
+ elif self.is_moving:
306
+ super().mouseMoveEvent(event)
307
+
308
+ elif hasattr(self, 'movement_mode') and self.movement_mode and hasattr(self, 'moving_rect'):
309
+ # Move the rectangle
310
+ scene_pos = self.mapToScene(event.pos())
311
+
312
+ # Calculate the desired center position (mouse position minus stored offset)
313
+ desired_center = QPointF(
314
+ scene_pos.x() - self.mouse_offset.x(),
315
+ scene_pos.y() - self.mouse_offset.y()
316
+ )
317
+
318
+ # For rotated rectangles, use setPos() instead of setRect()
319
+ # Get the current rect center in local coordinates
320
+ rect_center_local = self.moving_rect.rect().center()
321
+
322
+ # Calculate where the rectangle's origin should be to center it at desired_center
323
+ new_pos = QPointF(
324
+ desired_center.x() - rect_center_local.x(),
325
+ desired_center.y() - rect_center_local.y()
326
+ )
327
+
328
+ # Set the new position (this respects the rotation transformation)
329
+ self.moving_rect.setPos(new_pos)
330
+
331
+ elif hasattr(self, 'rotation_mode') and self.rotation_mode and hasattr(self, 'rotating_rect'):
332
+ scene_pos = self.mapToScene(event.pos())
333
+ line = QLineF(self.rect_center, scene_pos)
334
+ current_mouse_angle = line.angle()
335
+
336
+ # Calculate the delta from the initial mouse angle
337
+ angle_delta = current_mouse_angle - self.initial_mouse_angle
338
+
339
+ # Apply rotation relative to the initial rotation
340
+ new_angle = self.initial_angle + angle_delta
341
+ self.rotating_rect.setRotation(new_angle)
342
+
343
+ elif hasattr(self, 'modification_mode') and self.modification_mode and self.modifying_rect:
344
+ # Get current mouse position in scene coordinates
345
+ mouse_pos = self.mapToScene(event.pos())
346
+
347
+ # Calculate the movement delta
348
+ delta = mouse_pos - self.initial_mouse_pos
349
+
350
+ # Get the current rectangle
351
+ current_rect = self.original_rect
352
+ new_rect = QRectF(current_rect)
353
+
354
+ # Modify rectangle based on which handle is being dragged
355
+ if self.resize_handle == 'bottom_right':
356
+ new_rect.setBottomRight(current_rect.bottomRight() + delta)
357
+ elif self.resize_handle == 'top_left':
358
+ new_rect.setTopLeft(current_rect.topLeft() + delta)
359
+ elif self.resize_handle == 'top_right':
360
+ new_rect.setTopRight(current_rect.topRight() + delta)
361
+ elif self.resize_handle == 'bottom_left':
362
+ new_rect.setBottomLeft(current_rect.bottomLeft() + delta)
363
+ elif self.resize_handle == 'right':
364
+ new_rect.setRight(current_rect.right() + delta.x())
365
+ elif self.resize_handle == 'left':
366
+ new_rect.setLeft(current_rect.left() + delta.x())
367
+ elif self.resize_handle == 'bottom':
368
+ new_rect.setBottom(current_rect.bottom() + delta.y())
369
+ elif self.resize_handle == 'top':
370
+ new_rect.setTop(current_rect.top() + delta.y())
371
+
372
+ # Ensure minimum size (e.g., 10x10 pixels)
373
+ min_size = 10
374
+ if new_rect.width() < min_size:
375
+ if self.resize_handle in ['left', 'top_left', 'bottom_left']:
376
+ new_rect.setLeft(new_rect.right() - min_size)
377
+ else:
378
+ new_rect.setRight(new_rect.left() + min_size)
379
+
380
+ if new_rect.height() < min_size:
381
+ if self.resize_handle in ['top', 'top_left', 'top_right']:
382
+ new_rect.setTop(new_rect.bottom() - min_size)
383
+ else:
384
+ new_rect.setBottom(new_rect.top() + min_size)
385
+
386
+ # Apply the new rectangle size
387
+ self.modifying_rect.setRect(new_rect)
388
+
389
+ # Update the view
390
+ self.update()
391
+
392
+ def on_rectangle_properties_widget_clicked(self):
393
+ """Handle click on rectangle properties widget to activate rectangle tool"""
394
+ # Define the properties you want to set
395
+ label = self.current_rectangle_label # Use the current rectangle label
396
+ color = self.current_rectangle_color # Use the current rectangle color
397
+ thickness = self.current_rectangle_thickness # Use the current rectangle thickness
398
+
399
+ # Call the method from main class to activate the rectangle tool
400
+ if hasattr(self, 'main_window') and self.main_window:
401
+ # If this dialog has a reference to main window
402
+ self.main_window.activate_rectangle_tool_with_properties(label, color, thickness)
403
+ elif hasattr(self, 'parent') and hasattr(self.parent(), 'activate_rectangle_tool_with_properties'):
404
+ # If the parent has the method
405
+ self.parent().activate_rectangle_tool_with_properties(label, color, thickness)
406
+ else:
407
+ app = QApplication.instance()
408
+ if app:
409
+ for widget in app.topLevelWidgets():
410
+ if hasattr(widget, 'activate_rectangle_tool_with_properties'):
411
+ widget.activate_rectangle_tool_with_properties(label, color, thickness)
412
+ break
413
+
414
+ def on_polygon_properties_widget_clicked(self, widget):
415
+ """Handle click on polygon properties widget to activate polygon tool"""
416
+ # Define the properties you want to set from the widget
417
+ label = widget.label_name.text().replace("Label: ", "")
418
+ color = QColor(widget.label_color.text().replace("Color: ", ""))
419
+ thickness = int(widget.label_thickness.text().replace("thickness: ", ""))
420
+
421
+ # Call the method from main class to activate the polygon tool
422
+ if hasattr(self, 'main_window') and self.main_window:
423
+ # If this dialog has a reference to main window
424
+ self.main_window.activate_polygon_tool_with_properties(label, color, thickness)
425
+ elif hasattr(self, 'parent') and hasattr(self.parent(), 'activate_polygon_tool_with_properties'):
426
+ # If the parent has the method
427
+ self.parent().activate_polygon_tool_with_properties(label, color, thickness)
428
+ else:
429
+ app = QApplication.instance()
430
+ if app:
431
+ for widget in app.topLevelWidgets():
432
+ if hasattr(widget, 'activate_polygon_tool_with_properties'):
433
+ widget.activate_polygon_tool_with_properties(label, color, thickness)
434
+ break
435
+
436
+ def show_label_properties(self, label_text, current_color, current_thickness, shape_type='rectangle'):
437
+ # Choose the appropriate dictionary based on shape type
438
+ if shape_type == 'polygon':
439
+ if not hasattr(self, 'polygon_label_properties_dialogs_dict'):
440
+ self.polygon_label_properties_dialogs_dict = {}
441
+ dialogs_dict = self.polygon_label_properties_dialogs_dict
442
+ current_label_attr = 'current_polygon_label'
443
+ current_color_attr = 'current_polygon_color'
444
+ current_thickness_attr = 'current_polygon_thickness'
445
+ else:
446
+ if not hasattr(self, 'rectangle_label_properties_dialogs_dict'):
447
+ self.rectangle_label_properties_dialogs_dict = {}
448
+ dialogs_dict = self.rectangle_label_properties_dialogs_dict
449
+ current_label_attr = 'current_rectangle_label'
450
+ current_color_attr = 'current_rectangle_color'
451
+ current_thickness_attr = 'current_rectangle_thickness'
452
+
453
+ # Store current properties
454
+ setattr(self, current_label_attr, label_text)
455
+ setattr(self, current_color_attr, current_color)
456
+ setattr(self, current_thickness_attr, current_thickness)
457
+
458
+ # Check if a dialog for this label already exists
459
+ if label_text in dialogs_dict:
460
+ # Get the existing dialog
461
+ existing_dialog = dialogs_dict[label_text]
462
+
463
+ # Check if the dialog still exists and is valid
464
+ if existing_dialog and not existing_dialog.isHidden():
465
+ # Update the existing dialog with current properties
466
+ existing_dialog.update_properties(
467
+ label_text,
468
+ current_color,
469
+ current_thickness
470
+ )
471
+ # Bring the existing dialog to front
472
+ existing_dialog.raise_()
473
+ existing_dialog.activateWindow()
474
+ return
475
+ else:
476
+ # Remove the invalid dialog from the dictionary
477
+ del dialogs_dict[label_text]
478
+
479
+ total_dialogs = len(dialogs_dict)
480
+ if total_dialogs == 1 and hasattr(self.parent(), 'shortcut_button'):
481
+ self.parent().shortcut_button.setText("Hide Shortcuts")
482
+
483
+ if shape_type == 'polygon':
484
+ label_properties_dialog = LabelPolygonPropertiesDialog(self)
485
+ click_handler = lambda widget=label_properties_dialog: self.on_polygon_properties_widget_clicked(widget)
486
+ else:
487
+ label_properties_dialog = LabelRectanglePropertiesDialog(self)
488
+ click_handler = self.on_rectangle_properties_widget_clicked
489
+
490
+ # Update the properties
491
+ label_properties_dialog.update_properties(
492
+ label_text,
493
+ current_color,
494
+ current_thickness
495
+ )
496
+
497
+ # Store the dialog in the dictionary with the label as key
498
+ dialogs_dict[label_text] = label_properties_dialog
499
+
500
+ # Connect the dialog's close event to clean up the dictionary
501
+ def on_dialog_closed():
502
+ if label_text in dialogs_dict:
503
+ del dialogs_dict[label_text]
504
+
505
+ # Connect to the finished signal (emitted when dialog is closed)
506
+ label_properties_dialog.finished.connect(on_dialog_closed)
507
+
508
+ # Connect the click handler to the dialog
509
+ label_properties_dialog.mousePressEvent = lambda event: click_handler()
510
+
511
+ # Show the dialog
512
+ label_properties_dialog.show()
513
+
514
+ # Position the dialog at the top of the screen
515
+ screen_geometry = QApplication.primaryScreen().availableGeometry()
516
+ x = screen_geometry.width() - label_properties_dialog.width() - 10
517
+
518
+ # Calculate y position based on existing dialogs
519
+ existing_dialogs = [d for d in dialogs_dict.values() if d and d.isVisible()]
520
+ y = 10 + len(existing_dialogs) * (label_properties_dialog.height() + 10)
521
+
522
+ label_properties_dialog.move(x, y)
523
+
524
+ def show_rectangle_label_properties(self, label_text, current_color, current_thickness):
525
+ """Show rectangle label properties - wrapper for backward compatibility"""
526
+ self.show_label_properties(label_text, current_color, current_thickness, 'rectangle')
527
+
528
+ # Add new method for polygons
529
+ def show_polygon_label_properties(self, label_text, current_color, current_thickness):
530
+ """Show polygon label properties"""
531
+ self.show_label_properties(label_text, current_color, current_thickness, 'polygon')
532
+
533
+ def mouseReleaseEvent(self, event):
534
+ """Handle mouse release events"""
535
+ print(f"Mouse release event detected with button: {event.button()}") # Debug print
536
+
537
+ if event.button() == Qt.MouseButton.LeftButton:
538
+ print("Left mouse button released") # Debug print
539
+ self.is_moving = False
540
+ self.is_painting = False
541
+ self.is_erasing = False
542
+ if self.polygon_mode and hasattr(self, 'dragging_polygon_point') and self.dragging_polygon_point:
543
+ self.dragging_polygon_point = False
544
+ self.dragged_point_item = None
545
+ return
546
+ if not (self.paint_mode or self.erase_mode):
547
+ self.setDragMode(QGraphicsView.DragMode.NoDrag)
548
+ self.setCursor(Qt.CursorShape.ArrowCursor)
549
+
550
+ if self.current_stroke:
551
+ self.points_history.append(self.current_stroke)
552
+ if len(self.points_history) > self.MAX_HISTORY_SIZE:
553
+ self.points_history.pop(0)
554
+ self.current_stroke = []
555
+
556
+ if self.rectangle_mode and self.rect_start:
557
+ scene_pos = self.mapToScene(event.pos())
558
+ rect = QRectF(self.rect_start, scene_pos).normalized()
559
+
560
+ if rect.width() > 5 and rect.height() > 5 and self.rectangle_mode_type == 'yolo':
561
+ labeled_rect = LabeledRectangle(rect.x(), rect.y(), rect.width(), rect.height(), "")
562
+ self.scene.addItem(labeled_rect)
563
+ self.labeled_rectangles.append(labeled_rect)
564
+
565
+ elif rect.width() > 5 and rect.height() > 5 and self.rectangle_mode_type == 'classification':
566
+ # Initialize label colors and thickness dictionaries if not exists
567
+ label_text = ""
568
+ if not hasattr(self, 'label_colors'):
569
+ self.label_colors = {}
570
+ if not hasattr(self, 'label_thickness'):
571
+ self.label_thickness = {}
572
+
573
+ # Show dialog with previous labels for quick selection
574
+ self.label_properties_manager.load_properties()
575
+ if hasattr(self.label_properties_manager, 'label_properties') and self.label_properties_manager.label_properties:
576
+ # Initialize dictionaries if they don't exist
577
+ if not hasattr(self, 'label_colors'):
578
+ self.label_colors = {}
579
+ if not hasattr(self, 'label_thickness'):
580
+ self.label_thickness = {}
581
+ if not hasattr(self, 'recent_labels'):
582
+ self.recent_labels = []
583
+
584
+ # Update from loaded properties
585
+ for label_name, props in self.label_properties_manager.label_properties.items():
586
+ if label_name not in self.recent_labels:
587
+ self.recent_labels.append(label_name)
588
+ self.label_colors[label_name] = props['color'].name() # Convert QColor to hex string
589
+ self.label_thickness[label_name] = props['thickness']
590
+
591
+ if hasattr(self, 'recent_labels') and self.recent_labels:
592
+ # Create the dialog
593
+ dialog = QDialog(self)
594
+ dialog.setWindowTitle("Select or Create Label")
595
+ dialog.setObjectName("CustomDialog")
596
+
597
+ layout = QVBoxLayout()
598
+
599
+ combo = QComboBox()
600
+ combo.addItems(self.recent_labels)
601
+ combo.setEditable(True)
602
+
603
+ # Auto-select the last used label if it exists
604
+ if self.last_used_label and self.last_used_label in self.recent_labels:
605
+ combo.setCurrentText(self.last_used_label)
606
+ else:
607
+ combo.setCurrentIndex(-1)
608
+
609
+ label = QLabel("Select an existing label or type a new one:")
610
+ layout.addWidget(label)
611
+ layout.addWidget(combo)
612
+
613
+ # Add color selection button
614
+ color_frame = QFrame()
615
+ color_layout = QHBoxLayout(color_frame)
616
+ color_layout.setContentsMargins(0, 0, 0, 0)
617
+
618
+ color_label = QLabel("Color:")
619
+ color_button = QPushButton("Choose Color")
620
+ color_button.setObjectName("ColorButton")
621
+
622
+ # Set default color based on last used label or default red
623
+ if self.last_used_label and self.last_used_label in self.label_colors:
624
+ current_color = QColor(self.label_colors[self.last_used_label])
625
+ else:
626
+ current_color = QColor(255, 0, 0) # Default red
627
+
628
+ # Add thickness selection
629
+ thickness_frame = QFrame()
630
+ thickness_layout = QHBoxLayout(thickness_frame)
631
+ thickness_layout.setContentsMargins(0, 0, 0, 0)
632
+
633
+ thickness_label = QLabel("Thickness:")
634
+ thickness_spinbox = QSpinBox()
635
+ thickness_spinbox.setMinimum(1)
636
+ thickness_spinbox.setMaximum(10)
637
+ thickness_spinbox.setSuffix(" px")
638
+
639
+ # Set default thickness based on last used label or default 2
640
+ if self.last_used_label and self.last_used_label in self.label_thickness:
641
+ current_thickness = self.label_thickness[self.last_used_label]
642
+ else:
643
+ current_thickness = 2 # Default thickness
644
+ thickness_spinbox.setValue(current_thickness)
645
+
646
+ color_button.setStyleSheet(f"""
647
+ QPushButton#ColorButton {{
648
+ background-color: {current_color.name()};
649
+ color: white;
650
+ border: 2px solid #666666;
651
+ border-radius: 5px;
652
+ padding: 6px 12px;
653
+ font-weight: bold;
654
+ }}
655
+ QPushButton#ColorButton:hover {{
656
+ border: 2px solid #888888;
657
+ }}
658
+ """)
659
+
660
+ def choose_color():
661
+ nonlocal current_color
662
+ color = QColorDialog.getColor(current_color, dialog, "Choose Rectangle Color")
663
+ if color.isValid():
664
+ current_color = color
665
+ color_button.setStyleSheet(f"""
666
+ QPushButton#ColorButton {{
667
+ background-color: {current_color.name()};
668
+ color: white;
669
+ border: 2px solid #666666;
670
+ border-radius: 5px;
671
+ padding: 6px 12px;
672
+ font-weight: bold;
673
+ }}
674
+ QPushButton#ColorButton:hover {{
675
+ border: 2px solid #888888;
676
+ }}
677
+ """)
678
+
679
+ def on_combo_change():
680
+ nonlocal current_color, current_thickness
681
+ selected_label = combo.currentText().strip()
682
+ if selected_label in self.label_colors:
683
+ current_color = QColor(self.label_colors[selected_label])
684
+ color_button.setStyleSheet(f"""
685
+ QPushButton#ColorButton {{
686
+ background-color: {current_color.name()};
687
+ color: white;
688
+ border: 2px solid #666666;
689
+ border-radius: 5px;
690
+ padding: 6px 12px;
691
+ font-weight: bold;
692
+ }}
693
+ QPushButton#ColorButton:hover {{
694
+ border: 2px solid #888888;
695
+ }}
696
+ """)
697
+ if selected_label in self.label_thickness:
698
+ current_thickness = self.label_thickness[selected_label]
699
+ thickness_spinbox.setValue(current_thickness)
700
+
701
+ def on_thickness_change():
702
+ nonlocal current_thickness
703
+ current_thickness = thickness_spinbox.value()
704
+
705
+ color_button.clicked.connect(choose_color)
706
+ combo.currentTextChanged.connect(on_combo_change)
707
+ thickness_spinbox.valueChanged.connect(on_thickness_change)
708
+
709
+ color_layout.addWidget(color_label)
710
+ color_layout.addWidget(color_button)
711
+ layout.addWidget(color_frame)
712
+
713
+ thickness_layout.addWidget(thickness_label)
714
+ thickness_layout.addWidget(thickness_spinbox)
715
+ layout.addWidget(thickness_frame)
716
+
717
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
718
+ QDialogButtonBox.StandardButton.Cancel)
719
+ button_box.accepted.connect(dialog.accept)
720
+ button_box.rejected.connect(dialog.reject)
721
+ layout.addWidget(button_box)
722
+
723
+ dialog.setLayout(layout)
724
+ dialog.setStyleSheet("""
725
+ QDialog#CustomDialog {
726
+ background-color: #000000;
727
+ color: white;
728
+ font-size: 14px;
729
+ border: 1px solid #444444;
730
+ }
731
+ QLabel {
732
+ color: white;
733
+ background-color: transparent;
734
+ }
735
+ QFrame {
736
+ background-color: transparent;
737
+ }
738
+ QComboBox {
739
+ background-color: #111111;
740
+ color: white;
741
+ border: 1px solid #555555;
742
+ padding: 5px;
743
+ }
744
+ QComboBox QAbstractItemView {
745
+ background-color: #000000;
746
+ color: white;
747
+ selection-background-color: #222222;
748
+ }
749
+ QPushButton {
750
+ background-color: #111111;
751
+ color: white;
752
+ border: 1px solid #666666;
753
+ border-radius: 5px;
754
+ padding: 6px 12px;
755
+ }
756
+ QPushButton:hover {
757
+ background-color: #222222;
758
+ }
759
+ QDialogButtonBox QPushButton {
760
+ background-color: #111111;
761
+ color: white;
762
+ border: 1px solid #666666;
763
+ }
764
+ QDialog {
765
+ background-color: #000000;
766
+ color: white;
767
+ font-size: 14px;
768
+ border: 1px solid #444444;
769
+ }
770
+ QLabel {
771
+ color: white;
772
+ background-color: transparent;
773
+ font-size: 12px;
774
+ }
775
+ QSpinBox {
776
+ background-color: #111111;
777
+ color: white;
778
+ border: 1px solid #555555;
779
+ padding: 5px;
780
+ }
781
+ QSpinBox:focus {
782
+ border: 1px solid #666666;
783
+ }
784
+ QPushButton {
785
+ background-color: #111111;
786
+ color: white;
787
+ border: 1px solid #666666;
788
+ border-radius: 5px;
789
+ padding: 6px 12px;
790
+ }
791
+ QPushButton:hover {
792
+ background-color: #222222;
793
+ }
794
+ QPushButton:pressed {
795
+ background-color: #333333;
796
+ }
797
+ QGroupBox {
798
+ color: white;
799
+ font-weight: bold;
800
+ border: 1px solid #444444;
801
+ margin-top: 10px;
802
+ padding-top: 10px;
803
+ background-color: transparent;
804
+ }
805
+ QGroupBox::title {
806
+ subcontrol-origin: margin;
807
+ left: 10px;
808
+ padding: 0 5px 0 5px;
809
+ color: white;
810
+ }
811
+ QDialogButtonBox QPushButton {
812
+ background-color: #111111;
813
+ color: white;
814
+ border: 1px solid #666666;
815
+ min-width: 80px;
816
+ padding: 6px 12px;
817
+ }
818
+ QDialogButtonBox QPushButton:hover {
819
+ background-color: #222222;
820
+ }
821
+ """)
822
+
823
+ # Execute the dialog
824
+ if dialog.exec() == QDialog.DialogCode.Accepted:
825
+ label_text = combo.currentText().strip()
826
+ if label_text:
827
+ # Update last used label
828
+ self.last_used_label = label_text
829
+
830
+ # Store the color and thickness for this label
831
+ self.label_colors[label_text] = current_color.name()
832
+ self.label_thickness[label_text] = current_thickness
833
+
834
+ labeled_rect = LabeledRectangle(rect.x(), rect.y(), rect.width(), rect.height(), label_text)
835
+ # Set the rectangle color and thickness
836
+ labeled_rect.set_color(current_color)
837
+ labeled_rect.set_thickness(current_thickness) # Add this method to LabeledRectangle
838
+ self.scene.addItem(labeled_rect)
839
+ self.labeled_rectangles.append(labeled_rect)
840
+ self.save_rectangle_to_jpeg(labeled_rect)
841
+ self.show_rectangle_label_properties(label_text, current_color, current_thickness)
842
+
843
+ # Make sure this label is in recent_labels
844
+ if label_text not in self.recent_labels:
845
+ self.recent_labels.append(label_text)
846
+
847
+ else:
848
+ # Fall back to standard text input if no recent labels (similar changes needed here)
849
+ self.label_properties_manager.load_properties()
850
+ if hasattr(self.label_properties_manager, 'label_properties') and self.label_properties_manager.label_properties:
851
+ # Initialize dictionaries if they don't exist
852
+ if not hasattr(self, 'label_colors'):
853
+ self.label_colors = {}
854
+ if not hasattr(self, 'label_thickness'):
855
+ self.label_thickness = {}
856
+ if not hasattr(self, 'recent_labels'):
857
+ self.recent_labels = []
858
+
859
+ # Update from loaded properties
860
+ for label_name, props in self.label_properties_manager.label_properties.items():
861
+ if label_name not in self.recent_labels:
862
+ self.recent_labels.append(label_name)
863
+ self.label_colors[label_name] = props['color'].name() # Convert QColor to hex string
864
+ self.label_thickness[label_name] = props['thickness']
865
+
866
+ input_dialog = QDialog(self)
867
+ input_dialog.setWindowTitle("Label Rectangle")
868
+ input_dialog.setObjectName("CustomInputDialog")
869
+
870
+ layout = QVBoxLayout()
871
+
872
+ label_input = QLineEdit()
873
+ # Pre-fill with last used label if available
874
+ if self.last_used_label:
875
+ label_input.setText(self.last_used_label)
876
+ label_input.setPlaceholderText("Enter label:")
877
+
878
+ label = QLabel("Enter label:")
879
+ layout.addWidget(label)
880
+ layout.addWidget(label_input)
881
+
882
+ # Add color selection
883
+ color_frame = QFrame()
884
+ color_layout = QHBoxLayout(color_frame)
885
+ color_layout.setContentsMargins(0, 0, 0, 0)
886
+
887
+ color_label = QLabel("Color:")
888
+ color_button = QPushButton("Choose Color")
889
+ color_button.setObjectName("ColorButton")
890
+
891
+ # Add thickness selection
892
+ thickness_frame = QFrame()
893
+ thickness_layout = QHBoxLayout(thickness_frame)
894
+ thickness_layout.setContentsMargins(0, 0, 0, 0)
895
+
896
+ thickness_label = QLabel("Thickness:")
897
+ thickness_spinbox = QSpinBox()
898
+ thickness_spinbox.setMinimum(1)
899
+ thickness_spinbox.setMaximum(10)
900
+ thickness_spinbox.setSuffix(" px")
901
+
902
+ # Set default color and thickness based on last used label or defaults
903
+ if self.last_used_label and hasattr(self, 'label_colors') and self.last_used_label in self.label_colors:
904
+ current_color = QColor(self.label_colors[self.last_used_label])
905
+ else:
906
+ current_color = QColor(255, 0, 0) # Default red
907
+
908
+ if self.last_used_label and hasattr(self, 'label_thickness') and self.last_used_label in self.label_thickness:
909
+ current_thickness = self.label_thickness[self.last_used_label]
910
+ else:
911
+ current_thickness = 2 # Default thickness
912
+ thickness_spinbox.setValue(current_thickness)
913
+
914
+ color_button.setStyleSheet(f"""
915
+ QPushButton#ColorButton {{
916
+ background-color: {current_color.name()};
917
+ color: white;
918
+ border: 2px solid #666666;
919
+ border-radius: 5px;
920
+ padding: 6px 12px;
921
+ font-weight: bold;
922
+ }}
923
+ QPushButton#ColorButton:hover {{
924
+ border: 2px solid #888888;
925
+ }}
926
+ """)
927
+
928
+ def choose_color():
929
+ nonlocal current_color
930
+ color = QColorDialog.getColor(current_color, input_dialog, "Choose Rectangle Color")
931
+ if color.isValid():
932
+ current_color = color
933
+ color_button.setStyleSheet(f"""
934
+ QPushButton#ColorButton {{
935
+ background-color: {current_color.name()};
936
+ color: white;
937
+ border: 2px solid #666666;
938
+ border-radius: 5px;
939
+ padding: 6px 12px;
940
+ font-weight: bold;
941
+ }}
942
+ QPushButton#ColorButton:hover {{
943
+ border: 2px solid #888888;
944
+ }}
945
+ """)
946
+
947
+ def on_thickness_change():
948
+ nonlocal current_thickness
949
+ current_thickness = thickness_spinbox.value()
950
+
951
+ color_button.clicked.connect(choose_color)
952
+ thickness_spinbox.valueChanged.connect(on_thickness_change)
953
+
954
+ color_layout.addWidget(color_label)
955
+ color_layout.addWidget(color_button)
956
+ layout.addWidget(color_frame)
957
+
958
+ thickness_layout.addWidget(thickness_label)
959
+ thickness_layout.addWidget(thickness_spinbox)
960
+ layout.addWidget(thickness_frame)
961
+
962
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
963
+ QDialogButtonBox.StandardButton.Cancel)
964
+ button_box.accepted.connect(input_dialog.accept)
965
+ button_box.rejected.connect(input_dialog.reject)
966
+ layout.addWidget(button_box)
967
+
968
+ input_dialog.setLayout(layout)
969
+
970
+ # Apply existing stylesheet (same as before)
971
+ input_dialog.setStyleSheet("""
972
+ QDialog#CustomInputDialog {
973
+ background-color: #000000;
974
+ color: white;
975
+ font-size: 14px;
976
+ border: 1px solid #444444;
977
+ }
978
+ QLabel {
979
+ color: white;
980
+ background-color: transparent;
981
+ }
982
+ QFrame {
983
+ background-color: transparent;
984
+ }
985
+ QLineEdit {
986
+ background-color: #222222;
987
+ color: white;
988
+ border: 1px solid #555555;
989
+ padding: 5px;
990
+ }
991
+ QPushButton {
992
+ background-color: #111111;
993
+ color: white;
994
+ border: 1px solid #666666;
995
+ border-radius: 5px;
996
+ padding: 6px 12px;
997
+ }
998
+ QPushButton:hover {
999
+ background-color: #222222;
1000
+ }
1001
+ QDialog {
1002
+ background-color: #000000;
1003
+ color: white;
1004
+ font-size: 14px;
1005
+ border: 1px solid #444444;
1006
+ }
1007
+ QLabel {
1008
+ color: white;
1009
+ background-color: transparent;
1010
+ font-size: 12px;
1011
+ }
1012
+ QSpinBox {
1013
+ background-color: #111111;
1014
+ color: white;
1015
+ border: 1px solid #555555;
1016
+ padding: 5px;
1017
+ }
1018
+ QSpinBox:focus {
1019
+ border: 1px solid #666666;
1020
+ }
1021
+ QPushButton {
1022
+ background-color: #111111;
1023
+ color: white;
1024
+ border: 1px solid #666666;
1025
+ border-radius: 5px;
1026
+ padding: 6px 12px;
1027
+ }
1028
+ QPushButton:hover {
1029
+ background-color: #222222;
1030
+ }
1031
+ QPushButton:pressed {
1032
+ background-color: #333333;
1033
+ }
1034
+ QGroupBox {
1035
+ color: white;
1036
+ font-weight: bold;
1037
+ border: 1px solid #444444;
1038
+ margin-top: 10px;
1039
+ padding-top: 10px;
1040
+ background-color: transparent;
1041
+ }
1042
+ QGroupBox::title {
1043
+ subcontrol-origin: margin;
1044
+ left: 10px;
1045
+ padding: 0 5px 0 5px;
1046
+ color: white;
1047
+ }
1048
+ QDialogButtonBox QPushButton {
1049
+ background-color: #111111;
1050
+ color: white;
1051
+ border: 1px solid #666666;
1052
+ min-width: 80px;
1053
+ padding: 6px 12px;
1054
+ }
1055
+ QDialogButtonBox QPushButton:hover {
1056
+ background-color: #222222;
1057
+ }
1058
+ """)
1059
+
1060
+ if input_dialog.exec() == QDialog.DialogCode.Accepted:
1061
+ label_text = label_input.text().strip()
1062
+ if label_text:
1063
+ # Update last used label
1064
+ self.last_used_label = label_text
1065
+
1066
+ # Initialize label colors and thickness dictionaries if not exists
1067
+ if not hasattr(self, 'label_colors'):
1068
+ self.label_colors = {}
1069
+ if not hasattr(self, 'label_thickness'):
1070
+ self.label_thickness = {}
1071
+
1072
+ # Store the color and thickness for this label
1073
+ self.label_colors[label_text] = current_color.name()
1074
+ self.label_thickness[label_text] = current_thickness
1075
+
1076
+ labeled_rect = LabeledRectangle(rect.x(), rect.y(), rect.width(), rect.height(), label_text)
1077
+ # Set the rectangle color and thickness
1078
+ labeled_rect.set_color(current_color)
1079
+ labeled_rect.set_thickness(current_thickness) # Add this method to LabeledRectangle
1080
+ self.scene.addItem(labeled_rect)
1081
+ self.labeled_rectangles.append(labeled_rect)
1082
+ self.save_rectangle_to_jpeg(labeled_rect)
1083
+ self.show_rectangle_label_properties(label_text, current_color, current_thickness)
1084
+
1085
+ # Initialize recent_labels if needed
1086
+ if not hasattr(self, 'recent_labels'):
1087
+ self.recent_labels = []
1088
+ if label_text not in self.recent_labels:
1089
+ self.recent_labels.append(label_text)
1090
+ if label_text:
1091
+ self.label_properties_manager.add_label_property(label_text, current_color, current_thickness)
1092
+
1093
+ # Clean up the temporary rectangle regardless of whether we saved it
1094
+ if self.current_rect:
1095
+ self.scene.removeItem(self.current_rect)
1096
+ self.rect_start = None
1097
+ self.current_rect = None
1098
+
1099
+ elif self.rectangle_mode and event.button() == Qt.MouseButton.RightButton:
1100
+ # Handle right-click on rectangles
1101
+ scene_pos = self.mapToScene(int(event.position().x()), int(event.position().y()))
1102
+ item = self.scene.itemAt(scene_pos, self.transform())
1103
+
1104
+ # Check if the clicked item is a LabeledRectangle or if we need to check all rectangles
1105
+ if item and isinstance(item, LabeledRectangle):
1106
+ self.show_rectangle_context_menu(item, event.globalPosition().toPoint())
1107
+ return
1108
+ else:
1109
+ # Alternative approach: check all rectangles to see if click is inside any of them
1110
+ for rect in self.labeled_rectangles:
1111
+ if rect.contains(scene_pos):
1112
+ self.show_rectangle_context_menu(rect, event.globalPosition().toPoint())
1113
+ return
1114
+ print("No rectangle found at click position")
1115
+
1116
+ elif self.polygon_mode and event.button() == Qt.MouseButton.RightButton:
1117
+ # Handle right-click on rectangles
1118
+ scene_pos = self.mapToScene(int(event.position().x()), int(event.position().y()))
1119
+ item = self.scene.itemAt(scene_pos, self.transform())
1120
+ if item and hasattr(item, 'polygon'):
1121
+ self.show_polygon_context_menu(item, event.globalPosition().toPoint())
1122
+ else:
1123
+ print("No polygon found at click position")
1124
+ super().mouseReleaseEvent(event)
1125
+
1126
+ def undo_last_stroke(self):
1127
+ """
1128
+ Optimized method to remove the last stroke/operation with improved performance
1129
+ """
1130
+ if not self.points_history:
1131
+ return
1132
+
1133
+ # Get the last stroke to undo
1134
+ last_stroke = self.points_history.pop()
1135
+
1136
+ # Check if this is an erase operation
1137
+ is_erase_operation = (isinstance(last_stroke[0], tuple) and
1138
+ last_stroke[0][0] == 'erase')
1139
+
1140
+ if is_erase_operation:
1141
+ # For erase operations, restore the erased points
1142
+ restored_points = []
1143
+ for op_type, point_item in last_stroke: # Fixed syntax error here
1144
+ # Add point back to our points list
1145
+ self.points.append(point_item)
1146
+ restored_points.append(point_item)
1147
+
1148
+ # Re-add to scene efficiently without redundant checks
1149
+ if point_item.scene() is None:
1150
+ self.scene.addItem(point_item)
1151
+
1152
+ # Keep invisible as we use overlay for rendering
1153
+ point_item.setVisible(False)
1154
+ else:
1155
+ # For regular strokes, remove the points
1156
+ points_to_remove = set(last_stroke) # Use set for O(1) lookups
1157
+
1158
+ # Batch remove from points list (much faster than removing one by one)
1159
+ self.points = [p for p in self.points if p not in points_to_remove]
1160
+
1161
+ # Batch remove from scene (more efficient)
1162
+ for point_item in last_stroke:
1163
+ if point_item.scene() == self.scene:
1164
+ self.scene.removeItem(point_item)
1165
+
1166
+ # Force a complete redraw of the points overlay
1167
+ if hasattr(self, 'points_pixmap') and self.points_pixmap is not None:
1168
+ self.points_pixmap.fill(Qt.GlobalColor.transparent)
1169
+ self.last_rendered_points_count = 0
1170
+
1171
+ # Update the overlay in one operation instead of per-point
1172
+ self.update_points_overlay()
1173
+
1174
+ # Force a scene update to ensure changes are visible
1175
+ if hasattr(self, 'points_overlay_item') and self.points_overlay_item is not None:
1176
+ self.scene.update(self.points_overlay_item.boundingRect())
1177
+
1178
+ # Force garbage collection to free up memory
1179
+ gc.collect()
1180
+
1181
+ def reset_view(self):
1182
+ """Reset view to original state"""
1183
+ if self.base_pixmap:
1184
+ self.resetTransform()
1185
+ self.zoom_factor = 1.0
1186
+ self.setSceneRect(self.pixmap_item.boundingRect())
1187
+ self.fitInView(self.pixmap_item.boundingRect(), Qt.AspectRatioMode.KeepAspectRatio)
1188
+
1189
+ def keyPressEvent(self, event):
1190
+ """Handle key press events"""
1191
+ step = int(20 / self.zoom_factor) # Convert to integer with int()
1192
+
1193
+ if event.key() == Qt.Key.Key_Left:
1194
+ self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - step)
1195
+ elif event.key() == Qt.Key.Key_Right:
1196
+ self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + step)
1197
+ elif event.key() == Qt.Key.Key_Up:
1198
+ self.verticalScrollBar().setValue(self.verticalScrollBar().value() - step)
1199
+ elif event.key() == Qt.Key.Key_Down:
1200
+ self.verticalScrollBar().setValue(self.verticalScrollBar().value() + step)
1201
+ else:
1202
+ super().keyPressEvent(event)
1203
+
1204
+ # Apply the new opacity to the overlay
1205
+ if hasattr(self, 'points_overlay_item') and self.points_overlay_item:
1206
+ self.points_overlay_item.setOpacity(value / 255.0)
1207
+
1208
+ # Update the existing overlay opacity if it exists (different from points overlay)
1209
+ if self.overlay_pixmap_item:
1210
+ self.overlay_opacity = value
1211
+ self.overlay_pixmap_item.setOpacity(value / 255.0)
1212
+
1213
+ # Force scene update
1214
+ self.scene.update()