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,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()
|