PyImageLabeling 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyImageLabeling/__init__.py +22 -0
- PyImageLabeling/config.json +289 -0
- PyImageLabeling/controller/Controller.py +25 -0
- PyImageLabeling/controller/Events.py +147 -0
- PyImageLabeling/controller/FileEvents.py +69 -0
- PyImageLabeling/controller/ImageEvents.py +32 -0
- PyImageLabeling/controller/LabelEvents.py +219 -0
- PyImageLabeling/controller/LabelingEvents.py +123 -0
- PyImageLabeling/controller/settings/ContourFillinSetting.py +93 -0
- PyImageLabeling/controller/settings/CoutourFillingApplyCancel.py +37 -0
- PyImageLabeling/controller/settings/EraserSetting.py +73 -0
- PyImageLabeling/controller/settings/LabelSetting.py +91 -0
- PyImageLabeling/controller/settings/MagicPenSetting.py +125 -0
- PyImageLabeling/controller/settings/OpacitySetting.py +66 -0
- PyImageLabeling/controller/settings/PaintBrushSetting.py +66 -0
- PyImageLabeling/icons/apply.png +0 -0
- PyImageLabeling/icons/asterisk-green.png +0 -0
- PyImageLabeling/icons/asterisk-red.png +0 -0
- PyImageLabeling/icons/back.png +0 -0
- PyImageLabeling/icons/border.png +0 -0
- PyImageLabeling/icons/cancel.png +0 -0
- PyImageLabeling/icons/cleaner.png +0 -0
- PyImageLabeling/icons/close.png +0 -0
- PyImageLabeling/icons/down.png +0 -0
- PyImageLabeling/icons/ellipse.png +0 -0
- PyImageLabeling/icons/eraser.png +0 -0
- PyImageLabeling/icons/filling.png +0 -0
- PyImageLabeling/icons/logoMAIA.png +0 -0
- PyImageLabeling/icons/magic.png +0 -0
- PyImageLabeling/icons/maia.png +0 -0
- PyImageLabeling/icons/maia1.png +0 -0
- PyImageLabeling/icons/maia3.ico +0 -0
- PyImageLabeling/icons/maia_icon.png +0 -0
- PyImageLabeling/icons/move.png +0 -0
- PyImageLabeling/icons/opacity.png +0 -0
- PyImageLabeling/icons/open_image.png +0 -0
- PyImageLabeling/icons/open_layer.png +0 -0
- PyImageLabeling/icons/paint.png +0 -0
- PyImageLabeling/icons/plus.png +0 -0
- PyImageLabeling/icons/polygon.png +0 -0
- PyImageLabeling/icons/rectangle.png +0 -0
- PyImageLabeling/icons/reset.png +0 -0
- PyImageLabeling/icons/save.png +0 -0
- PyImageLabeling/icons/setting.png +0 -0
- PyImageLabeling/icons/transparency.png:Zone.Identifier +4 -0
- PyImageLabeling/icons/up.png +0 -0
- PyImageLabeling/icons/visibility.png +0 -0
- PyImageLabeling/icons/zoom_minus.png +0 -0
- PyImageLabeling/icons/zoom_plus.png +0 -0
- PyImageLabeling/model/Core.py +795 -0
- PyImageLabeling/model/File/Files.py +166 -0
- PyImageLabeling/model/File/NextImage.py +36 -0
- PyImageLabeling/model/File/PreviousImage.py +19 -0
- PyImageLabeling/model/Image/MoveImage.py +32 -0
- PyImageLabeling/model/Image/ResetMoveZoomImage.py +16 -0
- PyImageLabeling/model/Image/ZoomMinus.py +25 -0
- PyImageLabeling/model/Image/ZoomPlus.py +16 -0
- PyImageLabeling/model/Labeling/ClearAll.py +22 -0
- PyImageLabeling/model/Labeling/ContourFilling.py +135 -0
- PyImageLabeling/model/Labeling/Ellipse.py +350 -0
- PyImageLabeling/model/Labeling/Eraser.py +131 -0
- PyImageLabeling/model/Labeling/MagicPen.py +131 -0
- PyImageLabeling/model/Labeling/PaintBrush.py +207 -0
- PyImageLabeling/model/Labeling/Polygon.py +279 -0
- PyImageLabeling/model/Labeling/Rectangle.py +248 -0
- PyImageLabeling/model/Labeling/Undo.py +12 -0
- PyImageLabeling/model/Model.py +40 -0
- PyImageLabeling/model/Utils.py +40 -0
- PyImageLabeling/old_version/label_rectangle_properties.json +6 -0
- PyImageLabeling/old_version/main.py +2073 -0
- PyImageLabeling/old_version/models/EraseSettingsDialog.py +51 -0
- PyImageLabeling/old_version/models/LabeledRectangle.py +80 -0
- PyImageLabeling/old_version/models/MagicSettingsDialog.py +119 -0
- PyImageLabeling/old_version/models/OverlayOpacityDialog.py +63 -0
- PyImageLabeling/old_version/models/PaintSettingsDialog.py +289 -0
- PyImageLabeling/old_version/models/PointItem.py +66 -0
- PyImageLabeling/old_version/models/ProcessWorker.py +52 -0
- PyImageLabeling/old_version/models/ZoomableGraphicsView.py +1214 -0
- PyImageLabeling/old_version/models/tools/ContourTool.py +279 -0
- PyImageLabeling/old_version/models/tools/EraserTool.py +290 -0
- PyImageLabeling/old_version/models/tools/MagicPenTool.py +199 -0
- PyImageLabeling/old_version/models/tools/OverlayTool.py +179 -0
- PyImageLabeling/old_version/models/tools/PaintTool.py +68 -0
- PyImageLabeling/old_version/models/tools/PolygonTool.py +786 -0
- PyImageLabeling/old_version/models/tools/RectangleTool.py +1036 -0
- PyImageLabeling/parameters.json +1 -0
- PyImageLabeling/style.css +611 -0
- PyImageLabeling/view/Builder.py +333 -0
- PyImageLabeling/view/QBackgroundItem.py +30 -0
- PyImageLabeling/view/QWidgets.py +10 -0
- PyImageLabeling/view/View.py +226 -0
- PyImageLabeling/view/ZoomableGraphicsView.py +91 -0
- PyImageLabeling/view/__init__.py +0 -0
- pyimagelabeling-1.0.0.dist-info/METADATA +55 -0
- pyimagelabeling-1.0.0.dist-info/RECORD +99 -0
- pyimagelabeling-1.0.0.dist-info/WHEEL +5 -0
- pyimagelabeling-1.0.0.dist-info/licenses/LICENCE +22 -0
- pyimagelabeling-1.0.0.dist-info/top_level.txt +2 -0
- pypi/publish_pypi.py +18 -0
|
@@ -0,0 +1,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()
|