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,2073 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Tue Jun 10 10:22:00 2025
4
+
5
+ @author: pimfa
6
+ """
7
+
8
+ import cv2
9
+ import numpy as np
10
+ import sys
11
+ import os
12
+ import time
13
+ from PyQt6.QtWidgets import (
14
+ QGraphicsEllipseItem, QComboBox, QGraphicsRectItem, QInputDialog, QGraphicsItem, QGraphicsItemGroup, QGraphicsPixmapItem, QGraphicsOpacityEffect, QGraphicsView, QGraphicsScene, QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
15
+ QFileDialog, QWidget, QMessageBox, QHBoxLayout, QColorDialog, QDialog, QSlider, QFormLayout, QDialogButtonBox, QGridLayout, QProgressDialog, QCheckBox, QSpinBox, QSplashScreen, QMenu
16
+ )
17
+ from PyQt6.QtGui import QPixmap, QMouseEvent, QImage, QPainter, QColor, QPen, QBrush, QCursor, QIcon, QPainterPath, QFont
18
+ from PyQt6.QtCore import Qt, QPoint, QPointF, QTimer, QThread, pyqtSignal, QSize, QRectF, QObject, QLineF, QDateTime
19
+ import gc
20
+ import math
21
+ import traceback
22
+
23
+ from models.ZoomableGraphicsView import ZoomableGraphicsView
24
+ from models.OverlayOpacityDialog import OverlayOpacityDialog
25
+ from models.PaintSettingsDialog import PaintSettingsDialog, LabelPaintPropertiesDialog
26
+ from models.EraseSettingsDialog import EraseSettingsDialog
27
+ from models.MagicSettingsDialog import MagicSettingsDialog
28
+ from models.tools.PolygonTool import PolygonTool
29
+
30
+ class ImageViewer(QMainWindow):
31
+ def __init__(self):
32
+ super().__init__()
33
+ self.setWindowTitle("PyImageLabeling")
34
+ self.label_properties_dialogs = []
35
+ # Get screen information
36
+ self.screen = QApplication.primaryScreen()
37
+ self.screen_geometry = self.screen.availableGeometry()
38
+ self.screen_width = self.screen_geometry.width()
39
+ self.screen_height = self.screen_geometry.height()
40
+
41
+ # Calculate dynamic window size based on screen dimensions
42
+ self.window_width = int(self.screen_width * 0.85) # Use 85% of screen width
43
+ self.window_height = int(self.screen_height * 0.85) # Use 85% of screen height
44
+
45
+ # Set window position and size
46
+ self.setGeometry(
47
+ (self.screen_width - self.window_width) // 2, # Center horizontally
48
+ (self.screen_height - self.window_height) // 2, # Center vertically
49
+ self.window_width,
50
+ self.window_height
51
+ )
52
+
53
+ # Dynamic sizing for components
54
+ self.calculate_component_sizes()
55
+
56
+ # Icon
57
+ self.setWindowIcon(QIcon(self.get_icon_path("maia2")))
58
+
59
+ # Central widget
60
+ self.central_widget = QWidget()
61
+ self.setCentralWidget(self.central_widget)
62
+
63
+ # Main layout with dynamic stretch factors
64
+ self.main_layout = QHBoxLayout(self.central_widget)
65
+
66
+ # Initialize UI
67
+ self.setup_ui()
68
+
69
+ # Current settings
70
+ self.current_image_path = None
71
+ self.current_color = QColor(255, 0, 0)
72
+ self.current_radius = self.calculate_scaled_value(3) # Scale brush size
73
+ self.current_opacity = 255
74
+ self.current_label = ""
75
+ self.magic_pen_tolerance = 20
76
+ self.max_points_limite = 100000
77
+ self.eraser_size = self.calculate_scaled_value(10) # Scale eraser size
78
+ self.process_timeout = 10
79
+
80
+ self.shortcuts_visible = True
81
+ self.label_properties_dialogs_dict = {}
82
+ self.rectangle_label_properties_dialogs_dict = {}
83
+ self.polygon_label_properties_dialogs_dict = {}
84
+
85
+ def calculate_component_sizes(self):
86
+ """Calculate dynamic sizes for UI components based on screen resolution"""
87
+ # Base sizes on screen dimensions with better minimum values
88
+ base_scale = min(self.window_width / 1920, self.window_height / 1080)
89
+
90
+ # Dynamic grid cells
91
+ self.cell_width = max(100, int(self.window_width / 10))
92
+ self.cell_height = max(80, int(self.window_height / 10))
93
+
94
+ # Dynamic image container size (scales with window)
95
+ self.image_container_width = int(self.window_width * 0.75)
96
+ self.image_container_height = int(self.window_height * 0.8)
97
+
98
+ # Dynamic button sizes - ensure reasonable minimums
99
+ self.button_height = max(40, int(self.window_height * 0.05))
100
+ self.button_icon_size = max(24, int(self.button_height * 0.7))
101
+ self.button_min_width = max(80, int(self.window_width * 0.06))
102
+
103
+ # Control panel width - more adaptive
104
+ self.control_panel_width = max(120, int(self.window_width * 0.12))
105
+
106
+ # Dynamic spacing - more proportional to screen size
107
+ self.layout_spacing = max(8, int(self.window_width * 0.008))
108
+
109
+ # Font scaling with better minimum
110
+ self.base_font_size = max(9, int(self.window_width / 180))
111
+ self.app_font = QFont()
112
+ self.app_font.setPointSize(self.base_font_size)
113
+ QApplication.setFont(self.app_font)
114
+
115
+ def get_icon_path(self, icon_name):
116
+ # Assuming icons are stored in an 'icons' folder next to the script
117
+ icon_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icon')
118
+ return os.path.join(icon_dir, f"{icon_name}.png")
119
+
120
+ def calculate_scaled_value(self, base_value):
121
+ """Scale a value based on screen resolution"""
122
+ # Calculate a scaling factor based on resolution
123
+ scale_factor = min(self.screen_width / 1920, self.screen_height / 1080)
124
+ scaled_value = max(1, int(base_value * scale_factor))
125
+ return scaled_value
126
+
127
+ def setup_ui(self):
128
+ # Create central widget with dynamic layout
129
+ central_widget = QWidget()
130
+ self.setCentralWidget(central_widget)
131
+
132
+ # Use a more flexible layout
133
+ main_layout = QHBoxLayout(central_widget)
134
+ main_layout.setContentsMargins(self.layout_spacing, self.layout_spacing,
135
+ self.layout_spacing, self.layout_spacing)
136
+ main_layout.setSpacing(self.layout_spacing)
137
+
138
+ # Left side - image area
139
+ image_container = QWidget()
140
+ image_layout = QVBoxLayout(image_container)
141
+
142
+ # Top buttons row
143
+ button_row = QHBoxLayout()
144
+
145
+ # Load button with dynamic height
146
+ self.load_button = QPushButton("Load Image")
147
+ self.load_button.setFixedHeight(self.button_height)
148
+ self.load_button.setMinimumWidth(self.button_min_width)
149
+ self.load_button.clicked.connect(self.load_image)
150
+ self.load_button.setToolTip("Click to load an image into the viewer. This will allow you to select an image file from your system to display.") # Detailed tooltip
151
+ button_row.addWidget(self.load_button)
152
+
153
+ # Load Layer button with dynamic height
154
+ self.load_layer_button = QPushButton("Load Layer")
155
+ self.load_layer_button.setFixedHeight(self.button_height)
156
+ self.load_layer_button.setMinimumWidth(self.button_min_width)
157
+ self.load_layer_button.clicked.connect(self.load_layer)
158
+ self.load_layer_button.setToolTip("Click to load a new layer on top of the existing image. This allows you to add additional content or annotations to the image.") # Detailed tooltip
159
+ button_row.addWidget(self.load_layer_button)
160
+
161
+ # Unload Layer button with dynamic height
162
+ self.unload_layer_button = QPushButton("Unload Layer")
163
+ self.unload_layer_button.setFixedHeight(self.button_height)
164
+ self.unload_layer_button.setMinimumWidth(self.button_min_width)
165
+ self.unload_layer_button.clicked.connect(self.toggle_layer)
166
+ self.unload_layer_button.setToolTip("Click to remove the currently selected layer from the image. This will leave only the base image or other layers you wish to keep.") # Detailed tooltip
167
+ button_row.addWidget(self.unload_layer_button)
168
+
169
+ # Save button with dynamic height
170
+ self.save_button = QPushButton("Save Layer")
171
+ self.save_button.setFixedHeight(self.button_height)
172
+ self.save_button.setMinimumWidth(self.button_min_width)
173
+ self.save_button.clicked.connect(self.save_image)
174
+ self.save_button.setToolTip("Click to save the current layer to a file. This will store the layer as a separate image file on your system.") # Detailed tooltip
175
+ button_row.addWidget(self.save_button)
176
+
177
+ self.shortcut_button = QPushButton("Shortcut")
178
+ self.shortcut_button.setFixedHeight(self.button_height)
179
+ self.shortcut_button.setMinimumWidth(self.button_min_width)
180
+ self.shortcut_button.clicked.connect(self.toggle_shortcuts)
181
+ self.shortcut_button.setToolTip("Click to hide/show all label property dialogs or select specific ones.")
182
+ button_row.addWidget(self.shortcut_button)
183
+
184
+ button_row.addStretch(1)
185
+ image_layout.addLayout(button_row)
186
+ #========================================++>
187
+ # Image display with dynamic sizing
188
+ self.image_label = ZoomableGraphicsView()
189
+ self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
190
+ self.image_label.setStyleSheet("background-color: #ccc; border: 1px solid #000;")
191
+ self.image_label.setMinimumSize(self.image_container_width, self.image_container_height)
192
+ image_layout.addWidget(self.image_label, 1) # Give it stretch priority
193
+
194
+ main_layout.addWidget(image_container, 4) # Set stretch factor for image area
195
+
196
+ control_panel = QWidget()
197
+ control_panel.setMinimumWidth(self.control_panel_width)
198
+ control_panel.setMaximumWidth(max(200, int(self.window_width * 0.15)))
199
+
200
+ control_layout = QVBoxLayout(control_panel)
201
+ control_layout.setSpacing(max(8, int(self.button_height * 0.2))) # Fixed spacing for buttons
202
+
203
+ # Move Tools Section
204
+ move_tools_label = QLabel("Move Tools")
205
+ move_tools_label.setStyleSheet("font-weight: bold; margin-bottom: 5px;")
206
+ control_layout.addWidget(move_tools_label)
207
+
208
+ move_tools = [
209
+ ("Move", self.activate_move_mode, True, "move"),
210
+ ("Reset Move/zoom", self.image_label.reset_view, False, "reset"),
211
+ ]
212
+
213
+ self.tool_buttons = {}
214
+
215
+ for button_text, callback, checkable, icon_name in move_tools :
216
+ button = QPushButton(button_text) # Include text for better accessibility
217
+ button.setFixedHeight(self.button_height)
218
+
219
+ # Add scaled icon to button
220
+ icon_path = self.get_icon_path(icon_name)
221
+ if os.path.exists(icon_path):
222
+ button.setIcon(QIcon(icon_path))
223
+ button.setIconSize(QSize(self.button_icon_size, self.button_icon_size))
224
+
225
+ # Consistent padding based on button height
226
+ padding = max(4, int(self.button_height * 0.1))
227
+ border_radius = max(4, int(self.button_height * 0.1))
228
+
229
+ # Enhanced styling with dynamic values but consistent minimums
230
+ button.setStyleSheet(f"""
231
+ QPushButton {{
232
+ padding: {padding}px;
233
+ padding-left: {padding * 2}px;
234
+ padding-right: {padding * 2}px;
235
+ border: 1px solid #bbb;
236
+ border-radius: {border_radius}px;
237
+ background-color: #f0f0f0;
238
+ color: black;
239
+ font-size: {self.base_font_size}pt;
240
+ text-align: left;
241
+ }}
242
+ QPushButton:hover {{
243
+ background-color: #e0e0e0;
244
+ }}
245
+ QPushButton:pressed {{
246
+ background-color: #d0d0d0;
247
+ }}
248
+ QPushButton:checked {{
249
+ background-color: #c0c0c0;
250
+ border: 2px solid #808080;
251
+ }}
252
+ """)
253
+
254
+ if checkable:
255
+ button.setCheckable(True)
256
+
257
+ # Set tooltip for Move tools with specific function explanation
258
+ if button_text == "Move":
259
+ button.setToolTip("Click to activate Move mode. This allows you to move the image around in the viewer by dragging it.")
260
+ elif button_text == "Reset Move/zoom":
261
+ button.setToolTip("Click to reset the image's position and zoom level to the default view.")
262
+
263
+ button.clicked.connect(callback)
264
+ control_layout.addWidget(button)
265
+
266
+ # Store reference in dictionary for easy access
267
+ self.tool_buttons[button_text] = button
268
+
269
+ # Separator (for better UI clarity)
270
+ control_layout.addSpacing(10)
271
+ separator = QLabel("──────────────────") # Fake visual separator
272
+ separator.setStyleSheet("color: gray;")
273
+ control_layout.addWidget(separator)
274
+
275
+ # Layer Tools Section
276
+ layer_tools_label = QLabel("Layer Tools")
277
+ layer_tools_label.setStyleSheet("font-weight: bold; margin-bottom: 5px;")
278
+ control_layout.addWidget(layer_tools_label)
279
+
280
+ layer_tools = [
281
+ ("Undo", self.undo_last_stroke, False, "back"),
282
+ ("Opacity", self.toggle_opacity_mode, False, "opacity"),
283
+ ("Contour Filling", self.toggle_contour_mode, True, "fill"),
284
+ ("Paintbrush", self.toggle_paint_mode, True, "paint"),
285
+ ("Magic Pen", self.toggle_magic_pen, True, "magic"),
286
+ ("Rectangle", self.toggle_rectangle_select, True, "select"),
287
+ ("Polygon", self.toggle_polygon_select, True, "polygon"),
288
+ ("Eraser", self.toggle_erase_mode, True, "eraser"),
289
+ ("Clear All", self.toggle_clear, False, "cleaner"),
290
+ ]
291
+
292
+ for button_text, callback, checkable, icon_name in layer_tools :
293
+ button = QPushButton(button_text) # Include text for better accessibility
294
+ button.setFixedHeight(self.button_height)
295
+
296
+ # Add scaled icon to button
297
+ icon_path = self.get_icon_path(icon_name)
298
+ if os.path.exists(icon_path):
299
+ button.setIcon(QIcon(icon_path))
300
+ button.setIconSize(QSize(self.button_icon_size, self.button_icon_size))
301
+
302
+ # Consistent padding based on button height
303
+ padding = max(4, int(self.button_height * 0.1))
304
+ border_radius = max(4, int(self.button_height * 0.1))
305
+
306
+ # Enhanced styling with dynamic values but consistent minimums
307
+ button.setStyleSheet(f"""
308
+ QPushButton {{
309
+ padding: {padding}px;
310
+ padding-left: {padding * 2}px;
311
+ padding-right: {padding * 2}px;
312
+ border: 1px solid #bbb;
313
+ border-radius: {border_radius}px;
314
+ background-color: #f0f0f0;
315
+ color: black;
316
+ font-size: {self.base_font_size}pt;
317
+ text-align: left;
318
+ }}
319
+ QPushButton:hover {{
320
+ background-color: #e0e0e0;
321
+ }}
322
+ QPushButton:pressed {{
323
+ background-color: #d0d0d0;
324
+ }}
325
+ QPushButton:checked {{
326
+ background-color: #c0c0c0;
327
+ border: 2px solid #808080;
328
+ }}
329
+ """)
330
+
331
+ if checkable:
332
+ button.setCheckable(True)
333
+
334
+ # Set tooltips for Layer tools with specific explanations
335
+ if button_text == "Undo":
336
+ button.setToolTip("Click to undo the last drawing action or modification.")
337
+ elif button_text == "Opacity":
338
+ button.setToolTip("Click to toggle opacity mode, allowing you to adjust the transparency of layers.")
339
+ elif button_text == "Contour Filling":
340
+ button.setToolTip("Click to activate contour filling mode, which lets you fill in outlines of objects.")
341
+ elif button_text == "Paintbrush":
342
+ button.setToolTip("Click to activate paintbrush mode, allowing you to draw freely on the image.")
343
+ elif button_text == "Magic Pen":
344
+ button.setToolTip("Click to activate the Magic Pen mode for drawing precise, automated strokes.")
345
+ elif button_text == "Rectangle":
346
+ button.setToolTip("Click to activate the rectangle select tool for creating rectangular selections.")
347
+ elif button_text == "Polygon":
348
+ button.setToolTip("Click to activate the polygon select tool for creating polygon selections.")
349
+ elif button_text == "Eraser":
350
+ button.setToolTip("Click to activate the eraser tool, allowing you to erase parts of the image or layer.")
351
+ elif button_text == "Clear All":
352
+ button.setToolTip("Click to clear all layers and reset the image to its original state.")
353
+
354
+ button.clicked.connect(callback)
355
+ control_layout.addWidget(button)
356
+
357
+ # Store reference in dictionary for easy access
358
+ self.tool_buttons[button_text] = button
359
+
360
+ control_layout.addStretch(1)
361
+ main_layout.addWidget(control_panel, 1) # Set appropriate stretch factor
362
+
363
+
364
+ # Store references to toggleable buttons using the dictionary
365
+ self.paint_button = self.tool_buttons["Paintbrush"]
366
+ self.eraser_button = self.tool_buttons["Eraser"]
367
+ self.magic_pen_button = self.tool_buttons["Magic Pen"]
368
+ self.contour_button = self.tool_buttons["Contour Filling"]
369
+ self.select = self.tool_buttons["Rectangle"]
370
+ self.polygon = self.tool_buttons["Polygon"]
371
+ self.move_button = self.tool_buttons["Move"]
372
+ # Apply high DPI scaling
373
+ self.handle_high_dpi_screens()
374
+
375
+
376
+ def handle_high_dpi_screens(self):
377
+ """Apply additional adjustments for high DPI screens"""
378
+ # Check if we're on a high DPI screen
379
+ dpi = self.screen.logicalDotsPerInch()
380
+ if dpi > 120: # Higher than standard DPI
381
+ # Calculate DPI scaling factor
382
+ dpi_scale = dpi / 96.0
383
+
384
+ # Adjust button sizes based on DPI
385
+ for button in self.findChildren(QPushButton):
386
+ current_height = button.height()
387
+ scaled_height = max(current_height, int(current_height * dpi_scale * 0.8))
388
+ button.setMinimumHeight(scaled_height)
389
+
390
+ # Make scrollbars more touchable on high DPI screens
391
+ scrollbar_width = max(12, int(16 * dpi_scale))
392
+ self.image_label.setStyleSheet(
393
+ self.image_label.styleSheet() +
394
+ f"""
395
+ QScrollBar:vertical {{
396
+ width: {scrollbar_width}px;
397
+ }}
398
+ QScrollBar:horizontal {{
399
+ height: {scrollbar_width}px;
400
+ }}
401
+ """
402
+ )
403
+
404
+ def resizeEvent(self, event):
405
+ """Handle window resize events to adjust layout"""
406
+ super().resizeEvent(event)
407
+
408
+ # Recalculate component sizes based on new window size
409
+ self.window_width = self.width()
410
+ self.window_height = self.height()
411
+ self.calculate_component_sizes()
412
+
413
+ # Update sizes of critical components
414
+ if hasattr(self, 'image_label'):
415
+ self.image_label.setMinimumSize(self.image_container_width, self.image_container_height)
416
+
417
+ # Update button styling
418
+ if hasattr(self, 'tool_buttons'):
419
+ for button in self.tool_buttons.values():
420
+ padding = max(4, int(self.button_height * 0.1))
421
+ border_radius = max(4, int(self.button_height * 0.1))
422
+ button.setStyleSheet(f"""
423
+ QPushButton {{
424
+ padding: {padding}px;
425
+ padding-left: {padding * 2}px;
426
+ padding-right: {padding * 2}px;
427
+ border: 1px solid #bbb;
428
+ border-radius: {border_radius}px;
429
+ background-color: #f0f0f0;
430
+ color: black;
431
+ font-size: {self.base_font_size}pt;
432
+ text-align: left;
433
+ }}
434
+ QPushButton:hover {{
435
+ background-color: #e0e0e0;
436
+ }}
437
+ QPushButton:pressed {{
438
+ background-color: #d0d0d0;
439
+ }}
440
+ QPushButton:checked {{
441
+ background-color: #c0c0c0;
442
+ border: 2px solid #808080;
443
+ }}
444
+ """)
445
+ button.setFixedHeight(self.button_height)
446
+ button.setIconSize(QSize(self.button_icon_size, self.button_icon_size))
447
+
448
+ # Update the UI
449
+ self.update()
450
+
451
+ def toggle_contour_mode(self):
452
+ """Toggles between applying contour and filling contour on click."""
453
+ if not hasattr(self.image_label, 'base_pixmap') or self.image_label.base_pixmap is None:
454
+ msg_box = QMessageBox(self)
455
+ msg_box.setWindowTitle("Error")
456
+ msg_box.setText("No image loaded.")
457
+ msg_box.setStyleSheet("""
458
+ QMessageBox {
459
+ background-color: #000000; /* Pure black background */
460
+ color: white; /* White text */
461
+ font-size: 14px;
462
+ border: 1px solid #444444;
463
+ }
464
+ QLabel {
465
+ color: white; /* Ensures the message text is white */
466
+ background-color: #000000;
467
+ }
468
+ QPushButton {
469
+ background-color: #000000; /* Black buttons */
470
+ color: white;
471
+ border: 1px solid #555555;
472
+ border-radius: 5px;
473
+ padding: 5px 10px;
474
+ }
475
+ QPushButton:hover {
476
+ background-color: #222222; /* Slightly lighter on hover */
477
+ }
478
+ """)
479
+ msg_box.exec()
480
+ return
481
+
482
+ if not hasattr(self, "contour_mode_active"):
483
+ self.contour_mode_active = False # Initialize state
484
+
485
+ if self.contour_mode_active:
486
+ # If contour mode is active, turn it off
487
+ self.image_label.remove_overlay()
488
+ self.contour_mode_active = False
489
+ self.image_label.shape_fill_mode = False
490
+ self.activate_move_mode(True)
491
+ else:
492
+ # If contour mode is off, turn it on
493
+ self.image_label.apply_contour() # Apply contour detection
494
+ self.contour_mode_active = True
495
+ self.image_label.shape_fill_mode = True # Enable filling behavior
496
+ self.contour_button.setChecked(True)
497
+
498
+ self.paint_button.setChecked(False)
499
+ self.eraser_button.setChecked(False)
500
+ self.magic_pen_button.setChecked(False)
501
+ self.select.setChecked(False)
502
+ self.image_label.paint_mode = False
503
+ self.image_label.erase_mode = False
504
+ self.image_label.toggle_rectangle_mode(False)
505
+ self.move_button.setChecked(False)
506
+
507
+ def activate_move_mode(self, checked=None):
508
+ """Activates move mode and disables other tools."""
509
+ if checked is None:
510
+ checked = not self.move_button.isChecked() # Toggle if not explicitly set
511
+
512
+ if checked:
513
+ # When enabling move mode, disable all other tool buttons
514
+ self.paint_button.setChecked(False)
515
+ self.eraser_button.setChecked(False)
516
+ self.magic_pen_button.setChecked(False)
517
+ self.contour_button.setChecked(False)
518
+ self.select.setChecked(False)
519
+ self.polygon.setChecked(False)
520
+
521
+ # Disable other modes in the image label
522
+ self.image_label.paint_mode = False
523
+ self.image_label.erase_mode = False
524
+ self.image_label.magic_pen_mode = False
525
+ self.image_label.shape_fill_mode = False
526
+ self.image_label.toggle_rectangle_mode(False)
527
+ self.move_button.setChecked(True)
528
+ else:
529
+ # Disable drag mode when move is turned off
530
+ self.image_label.setDragMode(QGraphicsView.DragMode.NoDrag)
531
+ self.move_button.setChecked(False)
532
+
533
+ def initialize_move_mode(self):
534
+ """Set move mode as the default when application starts"""
535
+ self.activate_move_mode(True)
536
+
537
+ def finalize_setup(self):
538
+ # Call this at the end of setup_ui
539
+ self.activate_move_mode(True)
540
+
541
+ def toggle_magic_pen(self, enabled):
542
+ if enabled:
543
+ settings_dialog = MagicSettingsDialog(self, current_tolerance=self.magic_pen_tolerance, current_timeout=self.process_timeout, current_max_points=self.max_points_limite)
544
+ if settings_dialog.exec():
545
+ # Store selected tolerance
546
+ self.image_label.magic_pen_tolerance = settings_dialog.get_tolerance()
547
+ self.image_label.process_timeout = settings_dialog.get_timeout()
548
+ self.image_label.max_points_limite= settings_dialog.get_max_points_limit()
549
+
550
+ self.image_label.toggle_magic_pen(True)
551
+
552
+ self.paint_button.setChecked(False)
553
+ self.eraser_button.setChecked(False)
554
+ self.select.setChecked(False)
555
+ self.contour_button.setChecked(False)
556
+ self.move_button.setChecked(False)
557
+ self.polygon.setChecked(False)
558
+
559
+ self.image_label.paint_mode = False
560
+ self.image_label.erase_mode = False
561
+ self.image_label.toggle_rectangle_mode(False)
562
+ else:
563
+ self.magic_pen_button.setChecked(False)
564
+ self.activate_move_mode(True)
565
+ else:
566
+ self.image_label.toggle_magic_pen(False)
567
+ self.activate_move_mode(True)
568
+
569
+ def toggle_paint_mode(self, enabled):
570
+ if enabled:
571
+ settings_dialog = PaintSettingsDialog(
572
+ self,
573
+ current_color=self.current_color,
574
+ current_radius=self.current_radius,
575
+ current_opacity=self.current_opacity,
576
+ current_label=self.current_label
577
+ )
578
+ if settings_dialog.exec():
579
+ # Store the settings
580
+ self.current_color, self.current_radius, self.current_opacity, self.current_label = settings_dialog.get_settings()
581
+
582
+ # Apply settings to the image label
583
+ self.image_label.point_color = self.current_color
584
+ self.image_label.point_radius = self.current_radius
585
+ self.image_label.point_opacity = self.current_opacity
586
+ self.image_label.point_label = self.current_label
587
+
588
+ self.image_label.toggle_paint_mode(True)
589
+ self.eraser_button.setChecked(False)
590
+ self.magic_pen_button.setChecked(False)
591
+ self.select.setChecked(False)
592
+ self.contour_button.setChecked(False)
593
+ self.move_button.setChecked(False)
594
+ self.polygon.setChecked(False)
595
+
596
+ self.image_label.erase_mode = False
597
+ self.image_label.magic_pen_mode = False
598
+ self.image_label.toggle_rectangle_mode(False)
599
+
600
+ self.show_label_properties()
601
+ else:
602
+ self.paint_button.setChecked(False)
603
+ self.activate_move_mode(True)
604
+ else:
605
+ self.image_label.toggle_paint_mode(False)
606
+ self.activate_move_mode(True)
607
+
608
+ def activate_paint_tool_with_properties(self, label, color, radius, opacity):
609
+ """Activate paint tool with specific label properties"""
610
+ # Set the current properties
611
+ self.current_label = label
612
+ self.current_color = color
613
+ self.current_radius = radius
614
+ self.current_opacity = opacity
615
+
616
+ # Apply settings to the image label
617
+ if hasattr(self, 'image_label'):
618
+ self.image_label.point_color = color
619
+ self.image_label.point_radius = radius
620
+ self.image_label.point_opacity = opacity
621
+ self.image_label.point_label = label
622
+
623
+ # Activate paint mode
624
+ self.image_label.toggle_paint_mode(True)
625
+
626
+ # Deactivate other modes
627
+ self.image_label.erase_mode = False
628
+ self.image_label.magic_pen_mode = False
629
+ self.image_label.toggle_rectangle_mode(False)
630
+ self.image_label.toggle_polygon_mode(False)
631
+
632
+ # Update UI buttons
633
+ if hasattr(self, 'paint_button'):
634
+ self.paint_button.setChecked(True)
635
+ if hasattr(self, 'eraser_button'):
636
+ self.eraser_button.setChecked(False)
637
+ if hasattr(self, 'magic_pen_button'):
638
+ self.magic_pen_button.setChecked(False)
639
+ if hasattr(self, 'select'):
640
+ self.select.setChecked(False)
641
+ if hasattr(self, 'contour_button'):
642
+ self.contour_button.setChecked(False)
643
+ if hasattr(self, 'move_button'):
644
+ self.move_button.setChecked(False)
645
+ if hasattr(self, 'polygon'):
646
+ self.polygon.setChecked(False)
647
+
648
+ def on_label_properties_widget_clicked(self):
649
+ """Handle click on label properties widget to activate paint tool"""
650
+ # Define the properties you want to set
651
+ label = self.current_label # Use the current label
652
+ color = self.current_color # Use the current color
653
+ radius = self.current_radius # Use the current radius
654
+ opacity = self.current_opacity # Use the current opacity
655
+
656
+ # Call the method to activate the paint tool with the specified properties
657
+ self.activate_paint_tool_with_properties(label, color, radius, opacity)
658
+
659
+ # Optional: Print debug info to verify the click is registered
660
+ print(f"Activating paint tool with: Label={label}, Color={color}, Radius={radius}, Opacity={opacity}")
661
+
662
+ def toggle_paint_mode(self, enabled):
663
+ if enabled:
664
+ settings_dialog = PaintSettingsDialog(
665
+ self,
666
+ current_color=self.current_color,
667
+ current_radius=self.current_radius,
668
+ current_opacity=self.current_opacity,
669
+ current_label=self.current_label
670
+ )
671
+ if settings_dialog.exec():
672
+ # Store the settings
673
+ self.current_color, self.current_radius, self.current_opacity, self.current_label = settings_dialog.get_settings()
674
+
675
+ # Apply settings to the image label
676
+ self.image_label.point_color = self.current_color
677
+ self.image_label.point_radius = self.current_radius
678
+ self.image_label.point_opacity = self.current_opacity
679
+ self.image_label.point_label = self.current_label
680
+
681
+ self.image_label.toggle_paint_mode(True)
682
+ self.eraser_button.setChecked(False)
683
+ self.magic_pen_button.setChecked(False)
684
+ self.select.setChecked(False)
685
+ self.contour_button.setChecked(False)
686
+ self.move_button.setChecked(False)
687
+ self.polygon.setChecked(False)
688
+
689
+ self.image_label.erase_mode = False
690
+ self.image_label.magic_pen_mode = False
691
+ self.image_label.toggle_rectangle_mode(False)
692
+
693
+ self.show_label_properties()
694
+ else:
695
+ self.paint_button.setChecked(False)
696
+ self.activate_move_mode(True)
697
+ else:
698
+ self.image_label.toggle_paint_mode(False)
699
+ self.activate_move_mode(True)
700
+
701
+ def activate_paint_tool_with_properties(self, label, color, radius, opacity):
702
+ """Activate paint tool with specific label properties"""
703
+ # Set the current properties
704
+ self.current_label = label
705
+ self.current_color = color
706
+ self.current_radius = radius
707
+ self.current_opacity = opacity
708
+
709
+ # Apply settings to the image label
710
+ if hasattr(self, 'image_label'):
711
+ self.image_label.point_color = color
712
+ self.image_label.point_radius = radius
713
+ self.image_label.point_opacity = opacity
714
+ self.image_label.point_label = label
715
+
716
+ # Activate paint mode
717
+ self.image_label.toggle_paint_mode(True)
718
+
719
+ # Deactivate other modes
720
+ self.image_label.erase_mode = False
721
+ self.image_label.magic_pen_mode = False
722
+ self.image_label.toggle_rectangle_mode(False)
723
+
724
+ # Update UI buttons
725
+ if hasattr(self, 'paint_button'):
726
+ self.paint_button.setChecked(True)
727
+ if hasattr(self, 'eraser_button'):
728
+ self.eraser_button.setChecked(False)
729
+ if hasattr(self, 'magic_pen_button'):
730
+ self.magic_pen_button.setChecked(False)
731
+ if hasattr(self, 'select'):
732
+ self.select.setChecked(False)
733
+ if hasattr(self, 'contour_button'):
734
+ self.contour_button.setChecked(False)
735
+ if hasattr(self, 'move_button'):
736
+ self.move_button.setChecked(False)
737
+ if hasattr(self, 'polygon'):
738
+ self.polygon.setChecked(False)
739
+
740
+ def on_label_properties_widget_clicked(self):
741
+ """Handle click on label properties widget to activate paint tool"""
742
+ # Define the properties you want to set
743
+ label = self.current_label # Use the current label
744
+ color = self.current_color # Use the current color
745
+ radius = self.current_radius # Use the current radius
746
+ opacity = self.current_opacity # Use the current opacity
747
+
748
+ # Call the method to activate the paint tool with the specified properties
749
+ self.activate_paint_tool_with_properties(label, color, radius, opacity)
750
+
751
+ # Optional: Print debug info to verify the click is registered
752
+ print(f"Activating paint tool with: Label={label}, Color={color}, Radius={radius}, Opacity={opacity}")
753
+
754
+ def show_label_properties(self):
755
+ # Initialize the dictionary if it doesn't exist
756
+ if not hasattr(self, 'label_properties_dialogs_dict'):
757
+ self.label_properties_dialogs_dict = {}
758
+
759
+ # Check if a dialog for this label already exists
760
+ if self.current_label in self.label_properties_dialogs_dict:
761
+ # Get the existing dialog
762
+ existing_dialog = self.label_properties_dialogs_dict[self.current_label]
763
+
764
+ # Check if the dialog still exists and is valid
765
+ if existing_dialog and not existing_dialog.isHidden():
766
+ # Update the existing dialog with current properties
767
+ existing_dialog.update_properties(
768
+ self.current_label,
769
+ self.current_color,
770
+ self.current_radius,
771
+ self.current_opacity
772
+ )
773
+ # Bring the existing dialog to front
774
+ existing_dialog.raise_()
775
+ existing_dialog.activateWindow()
776
+ return
777
+ else:
778
+ # Remove the invalid dialog from the dictionary
779
+ del self.label_properties_dialogs_dict[self.current_label]
780
+
781
+ total_dialogs = len(self.label_properties_dialogs_dict) + len(self.rectangle_label_properties_dialogs_dict)
782
+ if total_dialogs == 0:
783
+ self.shortcut_button.setText("Hide Shortcuts")
784
+ elif not self.label_properties_dialogs_dict:
785
+ self.shortcut_button.setText("Hide Shortcuts")
786
+
787
+ # Create a new dialog only if one doesn't exist for this label
788
+ label_properties_dialog = LabelPaintPropertiesDialog(self)
789
+
790
+ # Update the properties
791
+ label_properties_dialog.update_properties(
792
+ self.current_label,
793
+ self.current_color,
794
+ self.current_radius,
795
+ self.current_opacity
796
+ )
797
+
798
+ # Store the dialog in the dictionary with the label as key
799
+ self.label_properties_dialogs_dict[self.current_label] = label_properties_dialog
800
+
801
+ # Connect the dialog's close event to clean up the dictionary
802
+ def on_dialog_closed():
803
+ if self.current_label in self.label_properties_dialogs_dict:
804
+ del self.label_properties_dialogs_dict[self.current_label]
805
+
806
+ # Connect to the finished signal (emitted when dialog is closed)
807
+ label_properties_dialog.finished.connect(on_dialog_closed)
808
+
809
+ widgets_to_try = [
810
+ 'properties_widget',
811
+ 'label_widget',
812
+ 'paint_widget',
813
+ 'main_widget',
814
+ 'content_widget',
815
+ 'central_widget'
816
+ ]
817
+
818
+ connected = False
819
+ for widget_name in widgets_to_try:
820
+ if hasattr(label_properties_dialog, widget_name):
821
+ widget = getattr(label_properties_dialog, widget_name)
822
+ if hasattr(widget, 'clicked'):
823
+ try:
824
+ widget.clicked.disconnect()
825
+ except:
826
+ pass
827
+ widget.clicked.connect(self.on_label_properties_widget_clicked)
828
+ print(f"Connected click signal to {widget_name} for label: {self.current_label}")
829
+ connected = True
830
+ break
831
+ elif hasattr(widget, 'mousePressEvent'):
832
+ # For non-button widgets, override mousePressEvent
833
+ widget.mousePressEvent = lambda event: self.on_label_properties_widget_clicked()
834
+ print(f"Connected mouse press event to {widget_name} for label: {self.current_label}")
835
+ connected = True
836
+ break
837
+
838
+ if not connected:
839
+ print("Warning: Could not find clickable widget in dialog")
840
+ print(f"Available attributes: {[attr for attr in dir(label_properties_dialog) if not attr.startswith('_')]}")
841
+
842
+ # Fallback: Make the entire dialog clickable
843
+ original_mouse_press = label_properties_dialog.mousePressEvent
844
+ def dialog_mouse_press(event):
845
+ self.on_label_properties_widget_clicked()
846
+ if original_mouse_press:
847
+ original_mouse_press(event)
848
+ label_properties_dialog.mousePressEvent = dialog_mouse_press
849
+ print("Using fallback: entire dialog is now clickable")
850
+
851
+ # Show the dialog
852
+ label_properties_dialog.show()
853
+
854
+ # Position the dialog at the top of the screen
855
+ screen_geometry = QApplication.primaryScreen().availableGeometry()
856
+ x = screen_geometry.width() - label_properties_dialog.width() - 10
857
+
858
+ # Calculate y position based on existing dialogs
859
+ existing_dialogs = [d for d in self.label_properties_dialogs_dict.values() if d and d.isVisible()]
860
+ y = 10 + len(existing_dialogs) * (label_properties_dialog.height() + 10)
861
+
862
+ label_properties_dialog.move(x, y)
863
+
864
+ def toggle_shortcuts(self):
865
+ """Toggle visibility of all label properties dialogs or show selection menu"""
866
+ # Get dictionaries safely - they might be in different objects
867
+ paint_dialogs = getattr(self, 'label_properties_dialogs_dict', {})
868
+ rectangle_dialogs = getattr(self, 'rectangle_label_properties_dialogs_dict', {})
869
+ polygon_dialogs = getattr(self, 'polygon_label_properties_dialogs_dict', {})
870
+
871
+ # Also check if rectangle dialogs are in another object (like ZoomableGraphicsView)
872
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'rectangle_label_properties_dialogs_dict'):
873
+ rectangle_dialogs = self.image_label.rectangle_label_properties_dialogs_dict
874
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'polygon_label_properties_dialogs_dict'):
875
+ polygon_dialogs = self.image_label.polygon_label_properties_dialogs_dict
876
+
877
+ total_dialogs = len(paint_dialogs) + len(rectangle_dialogs) + len(polygon_dialogs)
878
+
879
+ if total_dialogs == 0:
880
+ QMessageBox.information(self, "No Labels", "No label properties dialogs are currently open.")
881
+ return
882
+
883
+ # If more than 3 dialogs total, show selection menu
884
+ if total_dialogs > 3:
885
+ self.show_shortcut_selection_menu()
886
+ else:
887
+ # Simple toggle for few dialogs
888
+ if self.shortcuts_visible:
889
+ self.hide_all_shortcuts()
890
+ else:
891
+ self.show_all_shortcuts()
892
+
893
+ def show_shortcut_selection_menu(self):
894
+ """Show a menu to select which dialogs to show/hide"""
895
+ menu = QMenu(self)
896
+
897
+ # Get dictionaries safely
898
+ paint_dialogs = getattr(self, 'label_properties_dialogs_dict', {})
899
+ rectangle_dialogs = getattr(self, 'rectangle_label_properties_dialogs_dict', {})
900
+ polygon_dialogs = getattr(self, 'polygon_label_properties_dialogs_dict', {})
901
+
902
+ # Check if rectangle dialogs are in another object
903
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'rectangle_label_properties_dialogs_dict'):
904
+ rectangle_dialogs = self.image_label.rectangle_label_properties_dialogs_dict
905
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'polygon_label_properties_dialogs_dict'):
906
+ polygon_dialogs = self.image_label.polygon_label_properties_dialogs_dict
907
+
908
+ # Add "Toggle All" option
909
+ toggle_all_action = menu.addAction("Toggle All")
910
+ toggle_all_action.triggered.connect(self.toggle_all_shortcuts)
911
+ menu.addSeparator()
912
+
913
+ # Add paint label options
914
+ if paint_dialogs:
915
+ paint_submenu = menu.addMenu("Paint Labels")
916
+ for label, dialog in paint_dialogs.items():
917
+ if dialog and not dialog.isHidden():
918
+ action = paint_submenu.addAction(f"Hide: {label}")
919
+ action.triggered.connect(lambda checked, l=label, t="paint": self.hide_specific_shortcut(l, t))
920
+ else:
921
+ action = paint_submenu.addAction(f"Show: {label}")
922
+ action.triggered.connect(lambda checked, l=label, t="paint": self.show_specific_shortcut(l, t))
923
+
924
+ # Add rectangle label options
925
+ if rectangle_dialogs:
926
+ rectangle_submenu = menu.addMenu("Rectangle Labels")
927
+ for label, dialog in rectangle_dialogs.items():
928
+ if dialog and not dialog.isHidden():
929
+ action = rectangle_submenu.addAction(f"Hide: {label}")
930
+ action.triggered.connect(lambda checked, l=label, t="rectangle": self.hide_specific_shortcut(l, t))
931
+ else:
932
+ action = rectangle_submenu.addAction(f"Show: {label}")
933
+ action.triggered.connect(lambda checked, l=label, t="rectangle": self.show_specific_shortcut(l, t))
934
+
935
+ if polygon_dialogs:
936
+ polygon_submenu = menu.addMenu("Polygon Labels")
937
+ for label, dialog in polygon_dialogs.items():
938
+ if dialog and not dialog.isHidden():
939
+ action = polygon_submenu.addAction(f"Hide: {label}")
940
+ action.triggered.connect(lambda checked, l=label, t="polygon": self.hide_specific_shortcut(l, t))
941
+ else:
942
+ action = polygon_submenu.addAction(f"Show: {label}")
943
+ action.triggered.connect(lambda checked, l=label, t="polygon": self.show_specific_shortcut(l, t))
944
+
945
+ # Show menu at button position
946
+ button_pos = self.shortcut_button.mapToGlobal(self.shortcut_button.rect().bottomLeft())
947
+ menu.exec(button_pos)
948
+
949
+ def toggle_all_shortcuts(self):
950
+ """Toggle all shortcuts at once"""
951
+ if self.shortcuts_visible:
952
+ self.hide_all_shortcuts()
953
+ else:
954
+ self.show_all_shortcuts()
955
+
956
+ def hide_all_shortcuts(self):
957
+ """Hide all label properties dialogs"""
958
+ # Hide paint dialogs
959
+ paint_dialogs = getattr(self, 'label_properties_dialogs_dict', {})
960
+ for dialog in paint_dialogs.values():
961
+ if dialog and dialog.isVisible():
962
+ dialog.hide()
963
+
964
+ # Hide rectangle dialogs
965
+ rectangle_dialogs = getattr(self, 'rectangle_label_properties_dialogs_dict', {})
966
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'rectangle_label_properties_dialogs_dict'):
967
+ rectangle_dialogs = self.image_label.rectangle_label_properties_dialogs_dict
968
+
969
+ for dialog in rectangle_dialogs.values():
970
+ if dialog and dialog.isVisible():
971
+ dialog.hide()
972
+
973
+ polygon_dialogs = getattr(self, 'polygon_label_properties_dialogs_dict', {})
974
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'polygon_label_properties_dialogs_dict'):
975
+ polygon_dialogs = self.image_label.polygon_label_properties_dialogs_dict
976
+
977
+ for dialog in polygon_dialogs.values():
978
+ if dialog and dialog.isVisible():
979
+ dialog.hide()
980
+
981
+ self.shortcuts_visible = False
982
+ self.shortcut_button.setText("Show Shortcuts")
983
+
984
+ def show_all_shortcuts(self):
985
+ """Show all label properties dialogs"""
986
+ # Show paint dialogs
987
+ paint_dialogs = getattr(self, 'label_properties_dialogs_dict', {})
988
+ for dialog in paint_dialogs.values():
989
+ if dialog:
990
+ dialog.show()
991
+
992
+ # Show rectangle dialogs
993
+ rectangle_dialogs = getattr(self, 'rectangle_label_properties_dialogs_dict', {})
994
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'rectangle_label_properties_dialogs_dict'):
995
+ rectangle_dialogs = self.image_label.rectangle_label_properties_dialogs_dict
996
+
997
+ for dialog in rectangle_dialogs.values():
998
+ if dialog:
999
+ dialog.show()
1000
+
1001
+ polygon_dialogs = getattr(self, 'polygon_label_properties_dialogs_dict', {})
1002
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'polygon_label_properties_dialogs_dict'):
1003
+ polygon_dialogs = self.image_label.polygon_label_properties_dialogs_dict
1004
+
1005
+ for dialog in polygon_dialogs.values():
1006
+ if dialog:
1007
+ dialog.show()
1008
+
1009
+ self.shortcuts_visible = True
1010
+ self.shortcut_button.setText("Hide Shortcuts")
1011
+
1012
+ def hide_specific_shortcut(self, label, dialog_type="paint"):
1013
+ """Hide a specific label properties dialog"""
1014
+ if dialog_type == "paint":
1015
+ paint_dialogs = getattr(self, 'label_properties_dialogs_dict', {})
1016
+ if label in paint_dialogs:
1017
+ dialog = paint_dialogs[label]
1018
+ if dialog:
1019
+ dialog.hide()
1020
+
1021
+ elif dialog_type == "rectangle":
1022
+ rectangle_dialogs = getattr(self, 'rectangle_label_properties_dialogs_dict', {})
1023
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'rectangle_label_properties_dialogs_dict'):
1024
+ rectangle_dialogs = self.image_label.rectangle_label_properties_dialogs_dict
1025
+
1026
+ if label in rectangle_dialogs:
1027
+ dialog = rectangle_dialogs[label]
1028
+ if dialog:
1029
+ dialog.hide()
1030
+
1031
+ elif dialog_type == "polygon":
1032
+ polygon_dialogs = getattr(self, 'polygon_label_properties_dialogs_dict', {})
1033
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'polygon_label_properties_dialogs_dict'):
1034
+ polygon_dialogs = self.image_label.polygon_label_properties_dialogs_dict
1035
+
1036
+ if label in polygon_dialogs:
1037
+ dialog = polygon_dialogs[label]
1038
+ if dialog:
1039
+ dialog.hide()
1040
+
1041
+ def show_specific_shortcut(self, label, dialog_type="paint"):
1042
+ """Show a specific label properties dialog"""
1043
+ if dialog_type == "paint":
1044
+ paint_dialogs = getattr(self, 'label_properties_dialogs_dict', {})
1045
+ if label in paint_dialogs:
1046
+ dialog = paint_dialogs[label]
1047
+ if dialog:
1048
+ dialog.show()
1049
+ elif dialog_type == "rectangle":
1050
+ rectangle_dialogs = getattr(self, 'rectangle_label_properties_dialogs_dict', {})
1051
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'rectangle_label_properties_dialogs_dict'):
1052
+ rectangle_dialogs = self.image_label.rectangle_label_properties_dialogs_dict
1053
+
1054
+ if label in rectangle_dialogs:
1055
+ dialog = rectangle_dialogs[label]
1056
+ if dialog:
1057
+ dialog.show()
1058
+ elif dialog_type == "polygon":
1059
+ polygon_dialogs = getattr(self, 'polygon_label_properties_dialogs_dict', {})
1060
+ if hasattr(self, 'image_label') and hasattr(self.image_label, 'polygon_label_properties_dialogs_dict'):
1061
+ polygon_dialogs = self.image_label.polygon_label_properties_dialogs_dict
1062
+
1063
+ if label in polygon_dialogs:
1064
+ dialog = polygon_dialogs[label]
1065
+ if dialog:
1066
+ dialog.show()
1067
+
1068
+ def toggle_erase_mode(self, enabled):
1069
+ if enabled :
1070
+ settings_dialog = EraseSettingsDialog(self, current_eraser_size=self.eraser_size, absolute_mode=getattr(self.image_label, 'absolute_erase_mode', False))
1071
+ if settings_dialog.exec():
1072
+ self.eraser_size, absolute_mode = settings_dialog.get_settings()
1073
+ self.image_label.eraser_size = self.eraser_size
1074
+ self.image_label.absolute_erase_mode = absolute_mode
1075
+ self.image_label.toggle_erase_mode(True)
1076
+ self.paint_button.setChecked(False)
1077
+ self.select.setChecked(False)
1078
+ self.contour_button.setChecked(False)
1079
+ self.magic_pen_button.setChecked(False)
1080
+ self.move_button.setChecked(False)
1081
+ self.polygon.setChecked(False)
1082
+
1083
+ self.image_label.paint_mode = False
1084
+ self.image_label.magic_pen_mode = False
1085
+ self.image_label.toggle_rectangle_mode(False)
1086
+ else:
1087
+ self.eraser_button.setChecked(False)
1088
+ self.activate_move_mode(True)
1089
+ else:
1090
+ self.image_label.toggle_erase_mode(False)
1091
+ self.activate_move_mode(True)
1092
+
1093
+ def toggle_opacity_mode(self, enabled):
1094
+ settings_dialog = OverlayOpacityDialog(self, current_opacity=self.image_label.overlay_opacity)
1095
+
1096
+ # Aperçu en temps réel pendant que l'utilisateur bouge le slider
1097
+ settings_dialog.slider.valueChanged.connect(
1098
+ lambda value: self.image_label.update_overlay_opacity(value)
1099
+ )
1100
+
1101
+ if settings_dialog.exec():
1102
+ new_opacity = settings_dialog.slider.value()
1103
+ self.image_label.update_overlay_opacity(new_opacity)
1104
+ else:
1105
+ # Si l'utilisateur annule, remet l'ancienne valeur
1106
+ self.image_label.update_overlay_opacity(self.image_label.overlay_opacity)
1107
+
1108
+ def toggle_rectangle_select(self, checked):
1109
+ """Toggle rectangle selection mode and deactivate other tools"""
1110
+ if checked:
1111
+ # Show dialog to choose between YOLO and Classification
1112
+ choice_dialog = QDialog(self)
1113
+ choice_dialog.setWindowTitle("Rectangle Mode Selection")
1114
+ choice_dialog.setMinimumWidth(300)
1115
+ choice_dialog.setStyleSheet("""
1116
+ QDialog {
1117
+ background-color: #000000;
1118
+ color: white;
1119
+ border: 1px solid #444444;
1120
+ }
1121
+ QLabel {
1122
+ color: white;
1123
+ font-size: 14px;
1124
+ }
1125
+ QPushButton {
1126
+ background-color: #000000;
1127
+ color: white;
1128
+ border: 1px solid #555555;
1129
+ border-radius: 5px;
1130
+ padding: 5px 10px;
1131
+ margin: 5px;
1132
+ }
1133
+ QPushButton:hover {
1134
+ background-color: #222222;
1135
+ }
1136
+ """)
1137
+
1138
+ layout = QVBoxLayout()
1139
+ layout.addWidget(QLabel("Select rectangle mode:"))
1140
+
1141
+ button_layout = QHBoxLayout()
1142
+ yolo_button = QPushButton("Label-free")
1143
+ classification_button = QPushButton("Labelisation")
1144
+ cancel_button = QPushButton("Cancel")
1145
+
1146
+ button_layout.addWidget(yolo_button)
1147
+ button_layout.addWidget(classification_button)
1148
+ button_layout.addWidget(cancel_button)
1149
+
1150
+ layout.addLayout(button_layout)
1151
+ choice_dialog.setLayout(layout)
1152
+
1153
+ # Track which mode was selected
1154
+ selected_mode = None
1155
+
1156
+ def select_yolo():
1157
+ nonlocal selected_mode
1158
+ selected_mode = "yolo"
1159
+ choice_dialog.accept()
1160
+
1161
+ def select_classification():
1162
+ nonlocal selected_mode
1163
+ selected_mode = "classification"
1164
+ choice_dialog.accept()
1165
+
1166
+ # Connect buttons
1167
+ yolo_button.clicked.connect(select_yolo)
1168
+ classification_button.clicked.connect(select_classification)
1169
+ cancel_button.clicked.connect(choice_dialog.reject)
1170
+
1171
+ # Show dialog and get result
1172
+ result = choice_dialog.exec()
1173
+
1174
+ if result != 1 or selected_mode is None: # User cancelled or closed dialog
1175
+ self.select.setChecked(False) # Uncheck the rectangle button
1176
+ return
1177
+
1178
+ # Store the selected mode
1179
+ self.rectangle_mode_type = selected_mode
1180
+
1181
+ # First, uncheck all other tool buttons
1182
+ self.paint_button.setChecked(False)
1183
+ self.eraser_button.setChecked(False)
1184
+ self.magic_pen_button.setChecked(False)
1185
+ self.contour_button.setChecked(False)
1186
+ self.move_button.setChecked(False)
1187
+ self.polygon.setChecked(False)
1188
+
1189
+ self.image_label.paint_mode = False
1190
+ self.image_label.erase_mode = False
1191
+ self.image_label.magic_pen_mode = False
1192
+ self.image_label.toggle_polygon_mode(False)
1193
+
1194
+ # Enable rectangle mode
1195
+ self.image_label.toggle_rectangle_mode(True)
1196
+
1197
+ # Set the rectangle mode type in the image_label
1198
+ self.image_label.rectangle_mode_type = selected_mode
1199
+
1200
+ else:
1201
+ # When turning off rectangle mode, clear all active selections
1202
+ self.image_label.toggle_rectangle_mode(False)
1203
+ self.image_label.clear_rectangles()
1204
+ self.activate_move_mode(True)
1205
+
1206
+ def activate_rectangle_tool_with_properties(self, label, color, thickness):
1207
+ """Activate rectangle tool with specific label properties"""
1208
+ # Set the current properties
1209
+ self.current_rectangle_label = label
1210
+ self.current_rectangle_color = color
1211
+ self.current_rectangle_thickness = thickness
1212
+
1213
+ # Apply settings to the image label
1214
+ if hasattr(self, 'image_label'):
1215
+ self.image_label.rectangle_color = color
1216
+ self.image_label.rectangle_thickness = thickness
1217
+ self.image_label.rectangle_label = label
1218
+
1219
+ # Activate rectangle mode
1220
+ self.image_label.toggle_rectangle_mode(True)
1221
+
1222
+ # Deactivate other modes
1223
+ self.image_label.toggle_paint_mode(False)
1224
+ self.image_label.toggle_polygon_mode(False)
1225
+ self.image_label.erase_mode = False
1226
+ self.image_label.magic_pen_mode = False
1227
+
1228
+ # Update UI buttons
1229
+ if hasattr(self, 'select'):
1230
+ self.select.setChecked(True)
1231
+ if hasattr(self, 'paint_button'):
1232
+ self.paint_button.setChecked(False)
1233
+ if hasattr(self, 'eraser_button'):
1234
+ self.eraser_button.setChecked(False)
1235
+ if hasattr(self, 'magic_pen_button'):
1236
+ self.magic_pen_button.setChecked(False)
1237
+ if hasattr(self, 'contour_button'):
1238
+ self.contour_button.setChecked(False)
1239
+ if hasattr(self, 'move_button'):
1240
+ self.move_button.setChecked(False)
1241
+ if hasattr(self, 'polygon'):
1242
+ self.polygon.setChecked(False)
1243
+
1244
+ print(f"Rectangle tool activated with: Label={label}, Color={color}, Thickness={thickness}")
1245
+
1246
+ def activate_polygon_tool_with_properties(self, label, color, thickness):
1247
+ """Activate rectangle tool with specific label properties"""
1248
+ # Set the current properties
1249
+ self.current_polygon_label = label
1250
+ self.current_polygon_color = color
1251
+ self.current_polygon_thickness = thickness
1252
+
1253
+ # Apply settings to the image label
1254
+ if hasattr(self, 'image_label'):
1255
+ PolygonTool.default_polygon_color = color
1256
+ PolygonTool.default_polygon_thickness = thickness
1257
+ PolygonTool.last_used_label = label
1258
+
1259
+ # Activate rectangle mode
1260
+ self.image_label.toggle_polygon_mode(True)
1261
+
1262
+ # Deactivate other modes
1263
+ self.image_label.toggle_paint_mode(False)
1264
+ self.image_label.toggle_rectangle_mode(False)
1265
+ self.image_label.erase_mode = False
1266
+ self.image_label.magic_pen_mode = False
1267
+
1268
+ # Update UI buttons
1269
+ if hasattr(self, 'select'):
1270
+ self.select.setChecked(False)
1271
+ if hasattr(self, 'paint_button'):
1272
+ self.paint_button.setChecked(False)
1273
+ if hasattr(self, 'eraser_button'):
1274
+ self.eraser_button.setChecked(False)
1275
+ if hasattr(self, 'magic_pen_button'):
1276
+ self.magic_pen_button.setChecked(False)
1277
+ if hasattr(self, 'contour_button'):
1278
+ self.contour_button.setChecked(False)
1279
+ if hasattr(self, 'move_button'):
1280
+ self.move_button.setChecked(False)
1281
+ if hasattr(self, 'polygon'):
1282
+ self.polygon.setChecked(True)
1283
+
1284
+ print(f"Polygon tool activated with: Label={label}, Color={color}, Thickness={thickness}")
1285
+
1286
+ def toggle_polygon_select(self, checked):
1287
+ """Toggle rectangle selection mode and deactivate other tools"""
1288
+ if checked:
1289
+ # First, uncheck all other tool buttons
1290
+ self.paint_button.setChecked(False)
1291
+ self.eraser_button.setChecked(False)
1292
+ self.magic_pen_button.setChecked(False)
1293
+ self.contour_button.setChecked(False)
1294
+ self.move_button.setChecked(False)
1295
+ self.select.setChecked(False)
1296
+
1297
+ self.image_label.paint_mode = False
1298
+ self.image_label.erase_mode = False
1299
+ self.image_label.magic_pen_mode = False
1300
+ self.image_label.toggle_rectangle_mode(False)
1301
+
1302
+ # Enable rectangle mode
1303
+ self.image_label.toggle_polygon_mode(True)
1304
+
1305
+ else:
1306
+ # When turning off rectangle mode, clear all active selections
1307
+ self.image_label.toggle_polygon_mode(False)
1308
+ self.image_label.clear_polygons()
1309
+ self.activate_move_mode(True)
1310
+
1311
+ def toggle_clear(self):
1312
+ """Display a confirmation dialog before clearing all points."""
1313
+ confirm_dialog = QMessageBox(self)
1314
+ confirm_dialog.setWindowTitle("Confirm Clear All")
1315
+ confirm_dialog.setText("Are you sure you want to clear all points?")
1316
+ confirm_dialog.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
1317
+ confirm_dialog.setDefaultButton(QMessageBox.StandardButton.No)
1318
+
1319
+ # For better UX, use an icon
1320
+ confirm_dialog.setIcon(QMessageBox.Icon.Question)
1321
+
1322
+ # Get the user's response
1323
+ response = confirm_dialog.exec()
1324
+
1325
+ # If user confirmed, clear all points
1326
+ if response == QMessageBox.StandardButton.Yes:
1327
+ self.image_label.clear_points()
1328
+
1329
+ def load_image(self):
1330
+ file_dialog = QFileDialog()
1331
+ file_path, _ = file_dialog.getOpenFileName(
1332
+ self, "Open Image", "", "Images (*.png *.xpm *.jpg *.jpeg *.bmp *.gif)"
1333
+ )
1334
+
1335
+ if file_path:
1336
+ image = QImage(file_path)
1337
+ if not image.isNull():
1338
+ self.image_label.clear_rectangles()
1339
+ self.image_label.clear_points()
1340
+ self.current_image_path = file_path
1341
+ pixmap = QPixmap.fromImage(image)
1342
+ self.image_label.setBasePixmap(pixmap)
1343
+ self.image_label.reset_view()
1344
+ self.activate_move_mode(True)
1345
+ else:
1346
+ msg_box = QMessageBox(self)
1347
+ msg_box.setWindowTitle("Error")
1348
+ msg_box.setText("Could not load layer.")
1349
+ msg_box.setStyleSheet("""
1350
+ QMessageBox {
1351
+ background-color: #000000; /* Pure black background */
1352
+ color: white; /* White text */
1353
+ font-size: 14px;
1354
+ border: 1px solid #444444;
1355
+ }
1356
+ QLabel {
1357
+ color: white; /* Ensures the message text is white */
1358
+ background-color: #000000;
1359
+ }
1360
+ QPushButton {
1361
+ background-color: #000000; /* Black buttons */
1362
+ color: white;
1363
+ border: 1px solid #555555;
1364
+ border-radius: 5px;
1365
+ padding: 5px 10px;
1366
+ }
1367
+ QPushButton:hover {
1368
+ background-color: #222222; /* Slightly lighter on hover */
1369
+ }
1370
+ """)
1371
+ msg_box.exec()
1372
+
1373
+ def load_layer(self):
1374
+ """Load an overlay layer and align it with the base image"""
1375
+ file_path, _ = QFileDialog.getOpenFileName(
1376
+ self, "Load Layer", "", "Images (*.png *.xpm *.jpg *.jpeg *.bmp *.gif)"
1377
+ )
1378
+ if not file_path:
1379
+ return
1380
+
1381
+ try:
1382
+ # Load the overlay image
1383
+ overlay_image = QImage(file_path)
1384
+ if overlay_image.isNull():
1385
+ msg_box = QMessageBox(self)
1386
+ msg_box.setWindowTitle("Error")
1387
+ msg_box.setText("Failed to load layer.")
1388
+ msg_box.setStyleSheet("""
1389
+ QMessageBox {
1390
+ background-color: #000000; /* Pure black background */
1391
+ color: white; /* White text */
1392
+ font-size: 14px;
1393
+ border: 1px solid #444444;
1394
+ }
1395
+ QLabel {
1396
+ color: white; /* Ensures the message text is white */
1397
+ background-color: #000000;
1398
+ }
1399
+ QPushButton {
1400
+ background-color: #000000; /* Black buttons */
1401
+ color: white;
1402
+ border: 1px solid #555555;
1403
+ border-radius: 5px;
1404
+ padding: 5px 10px;
1405
+ }
1406
+ QPushButton:hover {
1407
+ background-color: #222222; /* Slightly lighter on hover */
1408
+ }
1409
+ """)
1410
+ msg_box.exec()
1411
+ return
1412
+
1413
+ # Remove any existing overlay
1414
+ if hasattr(self.image_label, 'remove_overlay'):
1415
+ self.image_label.remove_overlay()
1416
+
1417
+ # Create pixmap from image
1418
+ overlay_pixmap = QPixmap.fromImage(overlay_image)
1419
+
1420
+ # Add overlay to scene in ZoomableGraphicsView
1421
+ if hasattr(self.image_label, 'add_overlay'):
1422
+ self.image_label.add_overlay(overlay_pixmap)
1423
+ else:
1424
+ # Assuming image_label is ZoomableGraphicsView
1425
+ self._add_overlay_to_graphics_view(overlay_pixmap)
1426
+
1427
+ # Add to UI (toolbar or status bar)
1428
+ self.statusBar().showMessage(f"Layer loaded: {os.path.basename(file_path)}", 3000)
1429
+ except Exception as e:
1430
+ QMessageBox.warning(self, "Error", f"Failed to load layer: {str(e)}")
1431
+
1432
+ def _add_overlay_to_graphics_view(self, overlay_pixmap):
1433
+ """Add an overlay to ZoomableGraphicsView with proper alignment"""
1434
+ if not hasattr(self.image_label, 'scene') or not self.image_label.base_pixmap:
1435
+ msg_box = QMessageBox(self)
1436
+ msg_box.setWindowTitle("Error")
1437
+ msg_box.setText("Please loade a bas image first.")
1438
+ msg_box.setStyleSheet("""
1439
+ QMessageBox {
1440
+ background-color: #000000; /* Pure black background */
1441
+ color: white; /* White text */
1442
+ font-size: 14px;
1443
+ border: 1px solid #444444;
1444
+ }
1445
+ QLabel {
1446
+ color: white; /* Ensures the message text is white */
1447
+ background-color: #000000;
1448
+ }
1449
+ QPushButton {
1450
+ background-color: #000000; /* Black buttons */
1451
+ color: white;
1452
+ border: 1px solid #555555;
1453
+ border-radius: 5px;
1454
+ padding: 5px 10px;
1455
+ }
1456
+ QPushButton:hover {
1457
+ background-color: #222222; /* Slightly lighter on hover */
1458
+ }
1459
+ """)
1460
+ msg_box.exec()
1461
+ return
1462
+
1463
+ # Resize overlay to match base image size if needed
1464
+ base_size = self.image_label.base_pixmap.size()
1465
+ if overlay_pixmap.size() != base_size:
1466
+ overlay_pixmap = overlay_pixmap.scaled(
1467
+ base_size,
1468
+ Qt.AspectRatioMode.IgnoreAspectRatio,
1469
+ Qt.TransformationMode.SmoothTransformation
1470
+ )
1471
+
1472
+ # Create the overlay pixmap item and add to scene
1473
+ overlay_item = self.image_label.scene.addPixmap(overlay_pixmap)
1474
+ overlay_item.setZValue(1) # Layer above base image (which is at Z=0)
1475
+ overlay_item.setPos(0, 0) # Align with base image
1476
+
1477
+ # Store reference to overlay item
1478
+ self.image_label.overlay_pixmap_item = overlay_item
1479
+
1480
+ # Set initial opacity
1481
+ self.image_label.overlay_opacity = 128 # 50% opacity by default
1482
+ overlay_item.setOpacity(self.image_label.overlay_opacity / 255.0)
1483
+
1484
+ # Update the view
1485
+ self.image_label.scene.update()
1486
+
1487
+ def toggle_layer(self):
1488
+ if self.image_label.remove_overlay():
1489
+ msg_box = QMessageBox(self)
1490
+ msg_box.setWindowTitle("Layer Remove")
1491
+ msg_box.setText("Layer has been removed.")
1492
+ msg_box.setStyleSheet("""
1493
+ QMessageBox {
1494
+ background-color: #000000; /* Pure black background */
1495
+ color: white; /* White text */
1496
+ font-size: 14px;
1497
+ border: 1px solid #444444;
1498
+ }
1499
+ QLabel {
1500
+ color: white; /* Ensures the message text is white */
1501
+ background-color: #000000;
1502
+ }
1503
+ QPushButton {
1504
+ background-color: #000000; /* Black buttons */
1505
+ color: white;
1506
+ border: 1px solid #555555;
1507
+ border-radius: 5px;
1508
+ padding: 5px 10px;
1509
+ }
1510
+ QPushButton:hover {
1511
+ background-color: #222222; /* Slightly lighter on hover */
1512
+ }
1513
+ """)
1514
+ msg_box.exec()
1515
+ else:
1516
+ msg_box = QMessageBox(self)
1517
+ msg_box.setWindowTitle("Layer Remove")
1518
+ msg_box.setText("No Layer loaded to remove.")
1519
+ msg_box.setStyleSheet("""
1520
+ QMessageBox {
1521
+ background-color: #000000; /* Pure black background */
1522
+ color: white; /* White text */
1523
+ font-size: 14px;
1524
+ border: 1px solid #444444;
1525
+ }
1526
+ QLabel {
1527
+ color: white; /* Ensures the message text is white */
1528
+ background-color: #000000;
1529
+ }
1530
+ QPushButton {
1531
+ background-color: #000000; /* Black buttons */
1532
+ color: white;
1533
+ border: 1px solid #555555;
1534
+ border-radius: 5px;
1535
+ padding: 5px 10px;
1536
+ }
1537
+ QPushButton:hover {
1538
+ background-color: #222222; /* Slightly lighter on hover */
1539
+ }
1540
+ """)
1541
+ msg_box.exec()
1542
+
1543
+ def undo_last_stroke(self):
1544
+ self.image_label.undo_last_stroke()
1545
+
1546
+ def save_image(self):
1547
+ # Check if there are rectangles to save (for YOLO mode)
1548
+ has_rectangles = (hasattr(self.image_label, 'labeled_rectangles') and
1549
+ self.image_label.labeled_rectangles) or \
1550
+ (hasattr(self.image_label, 'rectangle_items') and
1551
+ self.image_label.rectangle_items)
1552
+ has_polygons = (hasattr(self.image_label, 'polygon_items') and
1553
+ self.image_label.polygon_items)
1554
+
1555
+ # Check if there are drawing points
1556
+ has_drawings = hasattr(self.image_label, 'points') and self.image_label.points
1557
+
1558
+ if not has_drawings and not has_rectangles and not has_polygons:
1559
+ msg_box = QMessageBox(self)
1560
+ msg_box.setWindowTitle("Error")
1561
+ msg_box.setText("No drawing or rectangles or polygons to save.")
1562
+ msg_box.setStyleSheet("""
1563
+ QMessageBox {
1564
+ background-color: #000000;
1565
+ color: white;
1566
+ font-size: 14px;
1567
+ border: 1px solid #444444;
1568
+ }
1569
+ QLabel {
1570
+ color: white;
1571
+ background-color: #000000;
1572
+ }
1573
+ QPushButton {
1574
+ background-color: #000000;
1575
+ color: white;
1576
+ border: 1px solid #555555;
1577
+ border-radius: 5px;
1578
+ padding: 5px 10px;
1579
+ }
1580
+ QPushButton:hover {
1581
+ background-color: #222222;
1582
+ }
1583
+ """)
1584
+ msg_box.exec()
1585
+ return
1586
+
1587
+ # Show dialog to choose save type
1588
+ save_type_dialog = QDialog(self)
1589
+ save_type_dialog.setWindowTitle("Save Type")
1590
+ save_type_dialog.setMinimumWidth(350)
1591
+ save_type_dialog.setStyleSheet("""
1592
+ QDialog {
1593
+ background-color: #000000;
1594
+ color: white;
1595
+ border: 1px solid #444444;
1596
+ }
1597
+ QLabel {
1598
+ color: white;
1599
+ font-size: 14px;
1600
+ }
1601
+ QPushButton {
1602
+ background-color: #000000;
1603
+ color: white;
1604
+ border: 1px solid #555555;
1605
+ border-radius: 5px;
1606
+ padding: 5px 10px;
1607
+ margin: 5px;
1608
+ }
1609
+ QPushButton:hover {
1610
+ background-color: #222222;
1611
+ }
1612
+ """)
1613
+
1614
+ layout = QVBoxLayout()
1615
+ layout.addWidget(QLabel("Select save type:"))
1616
+
1617
+ button_layout = QHBoxLayout()
1618
+ save_image_button = QPushButton("Save as Image")
1619
+ save_coordinates_button = QPushButton("Save Coordinates")
1620
+ cancel_button = QPushButton("Cancel")
1621
+
1622
+ button_layout.addWidget(save_image_button)
1623
+ button_layout.addWidget(save_coordinates_button)
1624
+ button_layout.addWidget(cancel_button)
1625
+
1626
+ layout.addLayout(button_layout)
1627
+ save_type_dialog.setLayout(layout)
1628
+
1629
+ # Track which type was selected
1630
+ save_type = None
1631
+
1632
+ def save_as_image():
1633
+ nonlocal save_type
1634
+ save_type = "image"
1635
+ save_type_dialog.accept()
1636
+
1637
+ def save_as_coordinates():
1638
+ nonlocal save_type
1639
+ save_type = "coordinates"
1640
+ save_type_dialog.accept()
1641
+
1642
+ # Connect buttons
1643
+ save_image_button.clicked.connect(save_as_image)
1644
+ save_coordinates_button.clicked.connect(save_as_coordinates)
1645
+ cancel_button.clicked.connect(save_type_dialog.reject)
1646
+
1647
+ # Show dialog and get result
1648
+ result = save_type_dialog.exec()
1649
+
1650
+ if result != 1 or save_type is None: # User cancelled or closed dialog
1651
+ return
1652
+
1653
+ # Handle coordinate saving
1654
+ if save_type == "coordinates":
1655
+ self.save_coordinates()
1656
+ return
1657
+
1658
+ # Original image saving logic continues here...
1659
+ # If we have rectangles, save the entire image with rectangles
1660
+ if has_rectangles:
1661
+ success = self.image_label.save_entire_image_with_rectangles()
1662
+ if success:
1663
+ msg_box = QMessageBox(self)
1664
+ msg_box.setWindowTitle("Success")
1665
+ msg_box.setText("Saved the entire image with shapes successfully.")
1666
+ msg_box.setStyleSheet("""
1667
+ QMessageBox {
1668
+ background-color: #000000;
1669
+ color: white;
1670
+ font-size: 14px;
1671
+ border: 1px solid #444444;
1672
+ }
1673
+ QLabel {
1674
+ color: white;
1675
+ background-color: #000000;
1676
+ }
1677
+ QPushButton {
1678
+ background-color: #000000;
1679
+ color: white;
1680
+ border: 1px solid #555555;
1681
+ border-radius: 5px;
1682
+ padding: 5px 10px;
1683
+ }
1684
+ QPushButton:hover {
1685
+ background-color: #222222;
1686
+ }
1687
+ """)
1688
+ msg_box.exec()
1689
+ return
1690
+
1691
+ if has_polygons:
1692
+ success = self.image_label.save_entire_image_with_polygons()
1693
+ if success:
1694
+ msg_box = QMessageBox(self)
1695
+ msg_box.setWindowTitle("Success")
1696
+ msg_box.setText("Saved the entire image with shapes successfully.")
1697
+ msg_box.setStyleSheet("""
1698
+ QMessageBox {
1699
+ background-color: #000000;
1700
+ color: white;
1701
+ font-size: 14px;
1702
+ border: 1px solid #444444;
1703
+ }
1704
+ QLabel {
1705
+ color: white;
1706
+ background-color: #000000;
1707
+ }
1708
+ QPushButton {
1709
+ background-color: #000000;
1710
+ color: white;
1711
+ border: 1px solid #555555;
1712
+ border-radius: 5px;
1713
+ padding: 5px 10px;
1714
+ }
1715
+ QPushButton:hover {
1716
+ background-color: #222222;
1717
+ }
1718
+ """)
1719
+ msg_box.exec()
1720
+ return
1721
+
1722
+ # Original drawing save logic (if no rectangles but has drawings)
1723
+ if has_drawings:
1724
+ # Show dialog to choose save mode
1725
+ save_mode_dialog = QDialog(self)
1726
+ save_mode_dialog.setWindowTitle("Save Mode")
1727
+ save_mode_dialog.setMinimumWidth(300)
1728
+ save_mode_dialog.setStyleSheet("""
1729
+ QDialog {
1730
+ background-color: #000000;
1731
+ color: white;
1732
+ border: 1px solid #444444;
1733
+ }
1734
+ QLabel {
1735
+ color: white;
1736
+ font-size: 14px;
1737
+ }
1738
+ QPushButton {
1739
+ background-color: #000000;
1740
+ color: white;
1741
+ border: 1px solid #555555;
1742
+ border-radius: 5px;
1743
+ padding: 5px 10px;
1744
+ margin: 5px;
1745
+ }
1746
+ QPushButton:hover {
1747
+ background-color: #222222;
1748
+ }
1749
+ """)
1750
+
1751
+ layout = QVBoxLayout()
1752
+ layout.addWidget(QLabel("Select save mode:"))
1753
+
1754
+ button_layout = QHBoxLayout()
1755
+ drawing_only_button = QPushButton("Drawing Only")
1756
+ all_layers_button = QPushButton("All Layers")
1757
+ cancel_button = QPushButton("Cancel")
1758
+
1759
+ button_layout.addWidget(drawing_only_button)
1760
+ button_layout.addWidget(all_layers_button)
1761
+ button_layout.addWidget(cancel_button)
1762
+
1763
+ layout.addLayout(button_layout)
1764
+ save_mode_dialog.setLayout(layout)
1765
+
1766
+ # Track which mode was selected
1767
+ save_mode = None
1768
+
1769
+ def save_drawing_only():
1770
+ nonlocal save_mode
1771
+ save_mode = "drawing_only"
1772
+ save_mode_dialog.accept()
1773
+
1774
+ def save_all_layers():
1775
+ nonlocal save_mode
1776
+ save_mode = "all_layers"
1777
+ save_mode_dialog.accept()
1778
+
1779
+ # Connect buttons
1780
+ drawing_only_button.clicked.connect(save_drawing_only)
1781
+ all_layers_button.clicked.connect(save_all_layers)
1782
+ cancel_button.clicked.connect(save_mode_dialog.reject)
1783
+
1784
+ # Show dialog and get result
1785
+ result = save_mode_dialog.exec()
1786
+
1787
+ if result != 1 or save_mode is None: # User cancelled or closed dialog
1788
+ return
1789
+
1790
+ # Prepare save directory
1791
+ save_dir = os.path.join(os.getcwd(), 'save')
1792
+ if not os.path.exists(save_dir):
1793
+ os.makedirs(save_dir)
1794
+
1795
+ base_name = os.path.basename(self.current_image_path) if self.current_image_path else "untitled"
1796
+ name, ext = os.path.splitext(base_name)
1797
+
1798
+ # Set filename based on save mode
1799
+ if save_mode == "all_layers":
1800
+ save_path = os.path.join(save_dir, f"{name}_all_layers.png")
1801
+ else:
1802
+ save_path = os.path.join(save_dir, f"{name}_drawing_only.png")
1803
+
1804
+ # Get the scene bounding rectangle
1805
+ scene_rect = self.image_label.scene.itemsBoundingRect()
1806
+ width, height = int(scene_rect.width()), int(scene_rect.height())
1807
+
1808
+ if width <= 0 or height <= 0:
1809
+ msg_box = QMessageBox(self)
1810
+ msg_box.setWindowTitle("Error")
1811
+ msg_box.setText("Drawing area is empty.")
1812
+ msg_box.setStyleSheet("""
1813
+ QMessageBox {
1814
+ background-color: #000000;
1815
+ color: white;
1816
+ font-size: 14px;
1817
+ border: 1px solid #444444;
1818
+ }
1819
+ QLabel {
1820
+ color: white;
1821
+ background-color: #000000;
1822
+ }
1823
+ QPushButton {
1824
+ background-color: #000000;
1825
+ color: white;
1826
+ border: 1px solid #555555;
1827
+ border-radius: 5px;
1828
+ padding: 5px 10px;
1829
+ }
1830
+ QPushButton:hover {
1831
+ background-color: #222222;
1832
+ }
1833
+ """)
1834
+ msg_box.exec()
1835
+ return
1836
+
1837
+ # Create an image with a transparent background
1838
+ final_image = QImage(width, height, QImage.Format.Format_ARGB32)
1839
+ final_image.fill(Qt.GlobalColor.transparent)
1840
+
1841
+ painter = QPainter(final_image)
1842
+
1843
+ # Store current opacity to restore it later
1844
+ current_opacity = self.image_label.overlay_opacity
1845
+
1846
+ if save_mode == "all_layers":
1847
+ # For "all layers" mode, make sure the background is visible
1848
+ if self.image_label.pixmap_item:
1849
+ self.image_label.pixmap_item.setVisible(True)
1850
+ else:
1851
+ # For "drawing only" mode, hide the background
1852
+ if self.image_label.pixmap_item:
1853
+ self.image_label.pixmap_item.setVisible(False)
1854
+
1855
+ # Make drawing fully opaque for "drawing only" mode
1856
+ self.image_label.update_overlay_opacity(255)
1857
+
1858
+ # Render the scene
1859
+ self.image_label.scene.render(painter, QRectF(0, 0, width, height), scene_rect)
1860
+
1861
+ # Restore the background image visibility and original opacity
1862
+ if self.image_label.pixmap_item:
1863
+ self.image_label.pixmap_item.setVisible(True)
1864
+
1865
+ # Restore the original opacity if it was changed
1866
+ if save_mode == "drawing_only":
1867
+ self.image_label.update_overlay_opacity(current_opacity)
1868
+
1869
+ painter.end()
1870
+
1871
+ # Save the final image
1872
+ final_image.save(save_path)
1873
+ QMessageBox.information(self, "Success", f"Image saved to {save_path}")
1874
+
1875
+ def save_coordinates(self):
1876
+ """Save coordinates and labeling data to JSON file"""
1877
+ import json
1878
+
1879
+ # Prepare save directory
1880
+ save_dir = os.path.join(os.getcwd(), 'save')
1881
+ if not os.path.exists(save_dir):
1882
+ os.makedirs(save_dir)
1883
+
1884
+ base_name = os.path.basename(self.current_image_path) if self.current_image_path else "untitled"
1885
+ name, ext = os.path.splitext(base_name)
1886
+ save_path = os.path.join(save_dir, f"{name}_coordinates.json")
1887
+
1888
+ data = {
1889
+ "image_path": self.current_image_path,
1890
+ "timestamp": str(QDateTime.currentDateTime().toString()),
1891
+ "drawings": [],
1892
+ "rectangles": [],
1893
+ "polygons": []
1894
+ }
1895
+
1896
+ # Save drawing points
1897
+ points_to_save = []
1898
+ if hasattr(self.image_label, 'points') and self.image_label.points:
1899
+ points_to_save = self.image_label.points
1900
+ elif hasattr(self, 'points') and self.points:
1901
+ points_to_save = self.points
1902
+
1903
+ if points_to_save:
1904
+ for point in points_to_save:
1905
+ # Get position safely
1906
+ pos = point.get_position()
1907
+
1908
+ label_val = ""
1909
+ try:
1910
+ if hasattr(point, 'fixed_label'):
1911
+ label_val = point.fixed_label
1912
+ except:
1913
+ label_val = ""
1914
+
1915
+ # Get color as string safely
1916
+ color_str = "#000000"
1917
+ try:
1918
+ if hasattr(point, 'fixed_color') and point.fixed_color:
1919
+ color_str = point.fixed_color.name()
1920
+ except:
1921
+ color_str = "#000000"
1922
+
1923
+ # Get opacity safely
1924
+ opacity_val = 1.0
1925
+ try:
1926
+ if hasattr(point, 'fixed_opacity'):
1927
+ opacity_val = float(point.fixed_opacity) / 255.0
1928
+ except:
1929
+ opacity_val = 1.0
1930
+
1931
+ # Get radius safely
1932
+ radius_val = 0
1933
+ try:
1934
+ if hasattr(point, '_fixed_radius'):
1935
+ radius_val = float(point._fixed_radius)
1936
+ except:
1937
+ radius_val = 0
1938
+
1939
+ data["drawings"].append({
1940
+ "x": float(pos.x()),
1941
+ "y": float(pos.y()),
1942
+ "Label": label_val,
1943
+ "radius": radius_val,
1944
+ "color": color_str,
1945
+ "opacity": opacity_val,
1946
+ "type": "drawing_point"
1947
+ })
1948
+
1949
+ with open(save_path, 'w') as f:
1950
+ json.dump(data, f, indent=4)
1951
+
1952
+ # Save rectangles
1953
+ if hasattr(self.image_label, 'labeled_rectangles') and self.image_label.labeled_rectangles:
1954
+ for rect_data in self.image_label.labeled_rectangles:
1955
+ rect = rect_data.rect()
1956
+ data["rectangles"].append({
1957
+ "x": rect.x(),
1958
+ "y": rect.y(),
1959
+ "width": rect.width(),
1960
+ "height": rect.height(),
1961
+ "label": rect_data.get_label(),
1962
+ "color": rect_data.get_color().name(),
1963
+ "type": "rectangle"
1964
+ })
1965
+ elif hasattr(self.image_label, 'rectangle_items') and self.image_label.rectangle_items:
1966
+ for rect_item in self.image_label.rectangle_items:
1967
+ rect = rect_item.rect()
1968
+ data["rectangles"].append({
1969
+ "x": rect.x(),
1970
+ "y": rect.y(),
1971
+ "width": rect.width(),
1972
+ "height": rect.height(),
1973
+ "type": "rectangle"
1974
+ })
1975
+
1976
+ # Save polygons
1977
+ if hasattr(self.image_label, 'polygon_items') and self.image_label.polygon_items:
1978
+ for poly_item in self.image_label.polygon_items:
1979
+ polygon = poly_item.polygon()
1980
+ points = []
1981
+ for i in range(polygon.count()):
1982
+ point = polygon.at(i)
1983
+ points.append({"x": point.x(), "y": point.y()})
1984
+ data["polygons"].append({
1985
+ "points": points,
1986
+ "type": "polygon"
1987
+ })
1988
+
1989
+ # Save to JSON file
1990
+ try:
1991
+ with open(save_path, 'w') as f:
1992
+ json.dump(data, f, indent=2)
1993
+
1994
+ msg_box = QMessageBox(self)
1995
+ msg_box.setWindowTitle("Success")
1996
+ msg_box.setText(f"Coordinates saved to {save_path}")
1997
+ msg_box.setStyleSheet("""
1998
+ QMessageBox {
1999
+ background-color: #000000;
2000
+ color: white;
2001
+ font-size: 14px;
2002
+ border: 1px solid #444444;
2003
+ }
2004
+ QLabel {
2005
+ color: white;
2006
+ background-color: #000000;
2007
+ }
2008
+ QPushButton {
2009
+ background-color: #000000;
2010
+ color: white;
2011
+ border: 1px solid #555555;
2012
+ border-radius: 5px;
2013
+ padding: 5px 10px;
2014
+ }
2015
+ QPushButton:hover {
2016
+ background-color: #222222;
2017
+ }
2018
+ """)
2019
+ msg_box.exec()
2020
+
2021
+ except Exception as e:
2022
+ msg_box = QMessageBox(self)
2023
+ msg_box.setWindowTitle("Error")
2024
+ msg_box.setText(f"Failed to save coordinates: {str(e)}")
2025
+ msg_box.setStyleSheet("""
2026
+ QMessageBox {
2027
+ background-color: #000000;
2028
+ color: white;
2029
+ font-size: 14px;
2030
+ border: 1px solid #444444;
2031
+ }
2032
+ QLabel {
2033
+ color: white;
2034
+ background-color: #000000;
2035
+ }
2036
+ QPushButton {
2037
+ background-color: #000000;
2038
+ color: white;
2039
+ border: 1px solid #555555;
2040
+ border-radius: 5px;
2041
+ padding: 5px 10px;
2042
+ }
2043
+ QPushButton:hover {
2044
+ background-color: #222222;
2045
+ }
2046
+ """)
2047
+ msg_box.exec()
2048
+
2049
+ def main():
2050
+ app = QApplication(sys.argv)
2051
+
2052
+ # Splash screen
2053
+ splash_pix = QPixmap(get_icon_path("logoMAIA"))
2054
+ splash = QSplashScreen(splash_pix, Qt.WindowType.SplashScreen)
2055
+ splash.show()
2056
+
2057
+ time.sleep(2)
2058
+
2059
+ viewer = ImageViewer()
2060
+ viewer.show()
2061
+
2062
+ # Close the splash screen and start the main application
2063
+ splash.close()
2064
+
2065
+ sys.exit(app.exec())
2066
+
2067
+ def get_icon_path(icon_name):
2068
+ # Assuming icons are stored in an 'icons' folder next to the script
2069
+ icon_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icon')
2070
+ return os.path.join(icon_dir, f"{icon_name}.png")
2071
+
2072
+ if __name__ == "__main__":
2073
+ main()