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