lazylabel-gui 1.0.7__py3-none-any.whl → 1.0.8__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.
- lazylabel/controls.py +138 -40
- lazylabel/custom_file_system_model.py +25 -16
- lazylabel/main.py +288 -220
- lazylabel/numeric_table_widget_item.py +0 -1
- lazylabel/utils.py +1 -1
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.8.dist-info}/METADATA +1 -1
- lazylabel_gui-1.0.8.dist-info/RECORD +17 -0
- lazylabel_gui-1.0.7.dist-info/RECORD +0 -17
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.8.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.8.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.8.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.8.dist-info}/top_level.txt +0 -0
lazylabel/controls.py
CHANGED
@@ -11,6 +11,9 @@ from PyQt6.QtWidgets import (
|
|
11
11
|
QComboBox,
|
12
12
|
QHeaderView,
|
13
13
|
QCheckBox,
|
14
|
+
QSlider,
|
15
|
+
QGroupBox,
|
16
|
+
QSplitter,
|
14
17
|
)
|
15
18
|
from PyQt6.QtCore import Qt
|
16
19
|
from .reorderable_class_table import ReorderableClassTable
|
@@ -21,88 +24,171 @@ class ControlPanel(QWidget):
|
|
21
24
|
super().__init__(parent)
|
22
25
|
layout = QVBoxLayout(self)
|
23
26
|
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
27
|
+
|
28
|
+
toggle_layout = QHBoxLayout()
|
29
|
+
self.btn_toggle_visibility = QPushButton("< Hide")
|
30
|
+
self.btn_toggle_visibility.setToolTip("Hide this panel")
|
31
|
+
toggle_layout.addWidget(self.btn_toggle_visibility)
|
32
|
+
toggle_layout.addStretch()
|
33
|
+
layout.addLayout(toggle_layout)
|
34
|
+
|
35
|
+
self.main_controls_widget = QWidget()
|
36
|
+
main_layout = QVBoxLayout(self.main_controls_widget)
|
37
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
38
|
+
|
24
39
|
self.mode_label = QLabel("Mode: Points")
|
25
40
|
font = self.mode_label.font()
|
26
41
|
font.setPointSize(14)
|
27
42
|
font.setBold(True)
|
28
43
|
self.mode_label.setFont(font)
|
29
|
-
|
44
|
+
main_layout.addWidget(self.mode_label)
|
30
45
|
|
31
|
-
# Mode Buttons
|
32
46
|
self.btn_sam_mode = QPushButton("Point Mode (1)")
|
33
47
|
self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
|
34
48
|
self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
|
35
49
|
self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
|
36
50
|
self.btn_selection_mode = QPushButton("Selection Mode (E)")
|
37
51
|
self.btn_selection_mode.setToolTip("Toggle segment selection (E)")
|
38
|
-
|
39
|
-
|
40
|
-
|
52
|
+
main_layout.addWidget(self.btn_sam_mode)
|
53
|
+
main_layout.addWidget(self.btn_polygon_mode)
|
54
|
+
main_layout.addWidget(self.btn_selection_mode)
|
41
55
|
|
42
|
-
|
56
|
+
main_layout.addSpacing(20)
|
43
57
|
line1 = QFrame()
|
44
58
|
line1.setFrameShape(QFrame.Shape.HLine)
|
45
|
-
|
46
|
-
|
59
|
+
main_layout.addWidget(line1)
|
60
|
+
main_layout.addSpacing(10)
|
47
61
|
|
48
|
-
# Action Buttons
|
49
62
|
self.btn_fit_view = QPushButton("Fit View (.)")
|
50
63
|
self.btn_fit_view.setToolTip("Reset image zoom and pan to fit the view (.)")
|
51
64
|
self.btn_clear_points = QPushButton("Clear Clicks (C)")
|
52
65
|
self.btn_clear_points.setToolTip("Clear current temporary points/vertices (C)")
|
53
|
-
|
54
|
-
|
66
|
+
main_layout.addWidget(self.btn_fit_view)
|
67
|
+
main_layout.addWidget(self.btn_clear_points)
|
68
|
+
|
69
|
+
main_layout.addSpacing(10)
|
55
70
|
|
56
|
-
|
71
|
+
settings_group = QGroupBox("Settings")
|
72
|
+
settings_layout = QVBoxLayout()
|
57
73
|
|
58
|
-
# Settings
|
59
74
|
self.chk_auto_save = QCheckBox("Auto-Save on Navigate")
|
60
75
|
self.chk_auto_save.setToolTip(
|
61
76
|
"Automatically save work when using arrow keys to change images."
|
62
77
|
)
|
63
78
|
self.chk_auto_save.setChecked(True)
|
64
|
-
|
79
|
+
settings_layout.addWidget(self.chk_auto_save)
|
80
|
+
|
81
|
+
self.chk_save_npz = QCheckBox("Save .npz")
|
82
|
+
self.chk_save_npz.setChecked(True)
|
83
|
+
self.chk_save_npz.setToolTip(
|
84
|
+
"Save the final mask as a compressed NumPy NPZ file."
|
85
|
+
)
|
86
|
+
settings_layout.addWidget(self.chk_save_npz)
|
87
|
+
|
88
|
+
self.chk_save_txt = QCheckBox("Save .txt")
|
89
|
+
self.chk_save_txt.setChecked(True)
|
90
|
+
self.chk_save_txt.setToolTip(
|
91
|
+
"Save bounding box annotations in YOLO TXT format."
|
92
|
+
)
|
93
|
+
settings_layout.addWidget(self.chk_save_txt)
|
94
|
+
|
95
|
+
self.chk_save_class_aliases = QCheckBox("Save Class Aliases (.json)")
|
96
|
+
self.chk_save_class_aliases.setToolTip(
|
97
|
+
"Save class aliases to a companion JSON file."
|
98
|
+
)
|
99
|
+
self.chk_save_class_aliases.setChecked(False)
|
100
|
+
settings_layout.addWidget(self.chk_save_class_aliases)
|
101
|
+
|
102
|
+
settings_group.setLayout(settings_layout)
|
103
|
+
main_layout.addWidget(settings_group)
|
104
|
+
|
105
|
+
sliders_group = QGroupBox("Adjustments")
|
106
|
+
sliders_layout = QVBoxLayout()
|
107
|
+
|
108
|
+
self.size_label = QLabel("Annotation Size: 1.0x")
|
109
|
+
self.size_slider = QSlider(Qt.Orientation.Horizontal)
|
110
|
+
self.size_slider.setRange(1, 50)
|
111
|
+
self.size_slider.setValue(10)
|
112
|
+
self.size_slider.setToolTip("Adjusts the size of points and lines (Ctrl +/-)")
|
113
|
+
sliders_layout.addWidget(self.size_label)
|
114
|
+
sliders_layout.addWidget(self.size_slider)
|
115
|
+
|
116
|
+
sliders_layout.addSpacing(10)
|
117
|
+
|
118
|
+
self.pan_label = QLabel("Pan Speed: 1.0x")
|
119
|
+
self.pan_slider = QSlider(Qt.Orientation.Horizontal)
|
120
|
+
self.pan_slider.setRange(1, 100)
|
121
|
+
self.pan_slider.setValue(10)
|
122
|
+
self.pan_slider.setToolTip(
|
123
|
+
"Adjusts the speed of WASD panning. Hold Shift for 5x boost."
|
124
|
+
)
|
125
|
+
sliders_layout.addWidget(self.pan_label)
|
126
|
+
sliders_layout.addWidget(self.pan_slider)
|
65
127
|
|
66
|
-
|
128
|
+
sliders_layout.addSpacing(10)
|
129
|
+
|
130
|
+
self.join_label = QLabel("Polygon Join Distance: 2px")
|
131
|
+
self.join_slider = QSlider(Qt.Orientation.Horizontal)
|
132
|
+
self.join_slider.setRange(1, 10)
|
133
|
+
self.join_slider.setValue(2)
|
134
|
+
self.join_slider.setToolTip("The pixel distance to 'snap' a polygon closed.")
|
135
|
+
sliders_layout.addWidget(self.join_label)
|
136
|
+
sliders_layout.addWidget(self.join_slider)
|
137
|
+
|
138
|
+
sliders_group.setLayout(sliders_layout)
|
139
|
+
main_layout.addWidget(sliders_group)
|
140
|
+
|
141
|
+
main_layout.addStretch()
|
67
142
|
|
68
|
-
# Notification Label
|
69
143
|
self.notification_label = QLabel("")
|
70
144
|
font = self.notification_label.font()
|
71
145
|
font.setItalic(True)
|
72
146
|
self.notification_label.setFont(font)
|
73
|
-
self.notification_label.setStyleSheet(
|
74
|
-
"color: #ffa500;"
|
75
|
-
) # Orange color for visibility
|
147
|
+
self.notification_label.setStyleSheet("color: #ffa500;")
|
76
148
|
self.notification_label.setWordWrap(True)
|
77
|
-
|
149
|
+
main_layout.addWidget(self.notification_label)
|
78
150
|
|
79
|
-
# Device Label
|
80
151
|
self.device_label = QLabel("Device: Unknown")
|
81
|
-
|
152
|
+
main_layout.addWidget(self.device_label)
|
153
|
+
|
154
|
+
layout.addWidget(self.main_controls_widget)
|
82
155
|
self.setFixedWidth(250)
|
83
156
|
|
84
157
|
|
85
158
|
class RightPanel(QWidget):
|
86
159
|
def __init__(self, parent=None):
|
87
160
|
super().__init__(parent)
|
88
|
-
|
161
|
+
self.v_layout = QVBoxLayout(self)
|
162
|
+
|
163
|
+
toggle_layout = QHBoxLayout()
|
164
|
+
toggle_layout.addStretch()
|
165
|
+
self.btn_toggle_visibility = QPushButton("Hide >")
|
166
|
+
self.btn_toggle_visibility.setToolTip("Hide this panel")
|
167
|
+
toggle_layout.addWidget(self.btn_toggle_visibility)
|
168
|
+
self.v_layout.addLayout(toggle_layout)
|
169
|
+
|
170
|
+
self.main_controls_widget = QWidget()
|
171
|
+
main_layout = QVBoxLayout(self.main_controls_widget)
|
172
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
173
|
+
|
174
|
+
v_splitter = QSplitter(Qt.Orientation.Vertical)
|
89
175
|
|
90
|
-
# File Explorer
|
91
|
-
|
176
|
+
# --- File Explorer Widget ---
|
177
|
+
file_explorer_widget = QWidget()
|
178
|
+
file_explorer_layout = QVBoxLayout(file_explorer_widget)
|
179
|
+
file_explorer_layout.setContentsMargins(0, 0, 0, 0)
|
92
180
|
self.btn_open_folder = QPushButton("Open Image Folder")
|
93
181
|
self.btn_open_folder.setToolTip("Open a directory of images")
|
94
182
|
self.file_tree = QTreeView()
|
95
183
|
file_explorer_layout.addWidget(self.btn_open_folder)
|
96
184
|
file_explorer_layout.addWidget(self.file_tree)
|
97
|
-
|
185
|
+
v_splitter.addWidget(file_explorer_widget)
|
98
186
|
|
99
|
-
#
|
100
|
-
|
101
|
-
|
102
|
-
|
187
|
+
# --- Segment List Widget ---
|
188
|
+
segment_widget = QWidget()
|
189
|
+
segment_layout = QVBoxLayout(segment_widget)
|
190
|
+
segment_layout.setContentsMargins(0, 0, 0, 0)
|
103
191
|
|
104
|
-
# Segment Table
|
105
|
-
segment_layout = QVBoxLayout()
|
106
192
|
class_filter_layout = QHBoxLayout()
|
107
193
|
class_filter_layout.addWidget(QLabel("Filter Class:"))
|
108
194
|
self.class_filter_combo = QComboBox()
|
@@ -112,7 +198,9 @@ class RightPanel(QWidget):
|
|
112
198
|
|
113
199
|
self.segment_table = QTableWidget()
|
114
200
|
self.segment_table.setColumnCount(3)
|
115
|
-
self.segment_table.setHorizontalHeaderLabels(
|
201
|
+
self.segment_table.setHorizontalHeaderLabels(
|
202
|
+
["Segment ID", "Class ID", "Alias"]
|
203
|
+
)
|
116
204
|
self.segment_table.horizontalHeader().setSectionResizeMode(
|
117
205
|
QHeaderView.ResizeMode.Stretch
|
118
206
|
)
|
@@ -120,6 +208,7 @@ class RightPanel(QWidget):
|
|
120
208
|
QAbstractItemView.SelectionBehavior.SelectRows
|
121
209
|
)
|
122
210
|
self.segment_table.setSortingEnabled(True)
|
211
|
+
self.segment_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
123
212
|
segment_layout.addWidget(self.segment_table)
|
124
213
|
|
125
214
|
segment_action_layout = QHBoxLayout()
|
@@ -134,30 +223,39 @@ class RightPanel(QWidget):
|
|
134
223
|
segment_action_layout.addWidget(self.btn_merge_selection)
|
135
224
|
segment_action_layout.addWidget(self.btn_delete_selection)
|
136
225
|
segment_layout.addLayout(segment_action_layout)
|
137
|
-
|
226
|
+
v_splitter.addWidget(segment_widget)
|
138
227
|
|
139
|
-
# Class Table
|
140
|
-
|
228
|
+
# --- Class Table Widget ---
|
229
|
+
class_widget = QWidget()
|
230
|
+
class_layout = QVBoxLayout(class_widget)
|
231
|
+
class_layout.setContentsMargins(0, 0, 0, 0)
|
141
232
|
class_layout.addWidget(QLabel("Class Order:"))
|
142
233
|
self.class_table = ReorderableClassTable()
|
143
234
|
self.class_table.setToolTip(
|
144
|
-
"
|
235
|
+
"Double-click to set class aliases and drag to reorder channels for saving."
|
145
236
|
)
|
146
237
|
self.class_table.setColumnCount(2)
|
147
|
-
self.class_table.setHorizontalHeaderLabels(["Alias", "
|
238
|
+
self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
|
148
239
|
self.class_table.horizontalHeader().setSectionResizeMode(
|
149
240
|
0, QHeaderView.ResizeMode.Stretch
|
150
241
|
)
|
151
242
|
self.class_table.horizontalHeader().setSectionResizeMode(
|
152
243
|
1, QHeaderView.ResizeMode.ResizeToContents
|
153
244
|
)
|
154
|
-
|
245
|
+
self.class_table.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
|
155
246
|
class_layout.addWidget(self.class_table)
|
156
247
|
self.btn_reassign_classes = QPushButton("Reassign Class IDs")
|
157
248
|
self.btn_reassign_classes.setToolTip(
|
158
249
|
"Re-index class channels based on the current order in this table"
|
159
250
|
)
|
160
251
|
class_layout.addWidget(self.btn_reassign_classes)
|
161
|
-
|
252
|
+
v_splitter.addWidget(class_widget)
|
253
|
+
|
254
|
+
main_layout.addWidget(v_splitter)
|
255
|
+
|
256
|
+
self.status_label = QLabel("")
|
257
|
+
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
258
|
+
main_layout.addWidget(self.status_label)
|
162
259
|
|
260
|
+
self.v_layout.addWidget(self.main_controls_widget)
|
163
261
|
self.setFixedWidth(350)
|
@@ -13,11 +13,10 @@ class CustomFileSystemModel(QFileSystemModel):
|
|
13
13
|
|
14
14
|
def set_highlighted_path(self, path):
|
15
15
|
self.highlighted_path = os.path.normpath(path) if path else None
|
16
|
-
# Trigger repaint of the entire view
|
17
16
|
self.layoutChanged.emit()
|
18
17
|
|
19
18
|
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
20
|
-
return
|
19
|
+
return 3
|
21
20
|
|
22
21
|
def headerData(
|
23
22
|
self,
|
@@ -32,14 +31,15 @@ class CustomFileSystemModel(QFileSystemModel):
|
|
32
31
|
if section == 0:
|
33
32
|
return "File Name"
|
34
33
|
if section == 1:
|
35
|
-
return "
|
34
|
+
return ".npz"
|
35
|
+
if section == 2:
|
36
|
+
return ".txt"
|
36
37
|
return super().headerData(section, orientation, role)
|
37
38
|
|
38
39
|
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
39
40
|
if not index.isValid():
|
40
41
|
return None
|
41
42
|
|
42
|
-
# Handle the temporary highlight for saving
|
43
43
|
if role == Qt.ItemDataRole.BackgroundRole:
|
44
44
|
filePath = os.path.normpath(self.filePath(index))
|
45
45
|
if (
|
@@ -47,17 +47,26 @@ class CustomFileSystemModel(QFileSystemModel):
|
|
47
47
|
and os.path.splitext(filePath)[0]
|
48
48
|
== os.path.splitext(self.highlighted_path)[0]
|
49
49
|
):
|
50
|
-
return QBrush(QColor(40, 80, 40))
|
51
|
-
|
52
|
-
if index.column() ==
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
50
|
+
return QBrush(QColor(40, 80, 40))
|
51
|
+
|
52
|
+
if index.column() > 0 and role == Qt.ItemDataRole.CheckStateRole:
|
53
|
+
filePath = self.filePath(index.siblingAtColumn(0))
|
54
|
+
base_path = os.path.splitext(filePath)[0]
|
55
|
+
|
56
|
+
if index.column() == 1:
|
57
|
+
check_path = base_path + ".npz"
|
58
|
+
elif index.column() == 2:
|
59
|
+
check_path = base_path + ".txt"
|
60
|
+
else:
|
61
|
+
return None
|
62
|
+
|
63
|
+
return (
|
64
|
+
Qt.CheckState.Checked
|
65
|
+
if os.path.exists(check_path)
|
66
|
+
else Qt.CheckState.Unchecked
|
67
|
+
)
|
68
|
+
|
69
|
+
if index.column() > 0 and role == Qt.ItemDataRole.DisplayRole:
|
70
|
+
return ""
|
62
71
|
|
63
72
|
return super().data(index, role)
|
lazylabel/main.py
CHANGED
@@ -3,6 +3,7 @@ import os
|
|
3
3
|
import numpy as np
|
4
4
|
import qdarktheme
|
5
5
|
import cv2
|
6
|
+
import json
|
6
7
|
from PyQt6.QtWidgets import (
|
7
8
|
QApplication,
|
8
9
|
QMainWindow,
|
@@ -15,6 +16,8 @@ from PyQt6.QtWidgets import (
|
|
15
16
|
QTableWidgetItem,
|
16
17
|
QGraphicsPolygonItem,
|
17
18
|
QTableWidgetSelectionRange,
|
19
|
+
QSpacerItem,
|
20
|
+
QHeaderView,
|
18
21
|
)
|
19
22
|
from PyQt6.QtGui import (
|
20
23
|
QPixmap,
|
@@ -60,14 +63,15 @@ class MainWindow(QMainWindow):
|
|
60
63
|
self.current_file_index = QModelIndex()
|
61
64
|
|
62
65
|
self.next_class_id = 0
|
66
|
+
self.class_aliases = {}
|
63
67
|
|
64
|
-
self.
|
68
|
+
self._original_point_radius = 0.3
|
69
|
+
self._original_line_thickness = 0.5
|
70
|
+
self.point_radius = self._original_point_radius
|
71
|
+
self.line_thickness = self._original_line_thickness
|
65
72
|
|
66
|
-
self.
|
67
|
-
self.
|
68
|
-
|
69
|
-
self._original_point_radius = self.point_radius
|
70
|
-
self._original_line_thickness = self.line_thickness
|
73
|
+
self.pan_multiplier = 1.0
|
74
|
+
self.polygon_join_threshold = 2
|
71
75
|
|
72
76
|
self.point_items, self.positive_points, self.negative_points = [], [], []
|
73
77
|
self.polygon_points, self.polygon_preview_items = [], []
|
@@ -87,6 +91,10 @@ class MainWindow(QMainWindow):
|
|
87
91
|
self.file_model = CustomFileSystemModel()
|
88
92
|
self.right_panel.file_tree.setModel(self.file_model)
|
89
93
|
self.right_panel.file_tree.setColumnWidth(0, 200)
|
94
|
+
file_tree = self.right_panel.file_tree
|
95
|
+
header = file_tree.header()
|
96
|
+
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
97
|
+
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
90
98
|
|
91
99
|
main_layout = QHBoxLayout()
|
92
100
|
main_layout.addWidget(self.control_panel)
|
@@ -101,6 +109,7 @@ class MainWindow(QMainWindow):
|
|
101
109
|
)
|
102
110
|
self.setup_connections()
|
103
111
|
self.set_sam_mode()
|
112
|
+
self.set_annotation_size(10)
|
104
113
|
|
105
114
|
def setup_connections(self):
|
106
115
|
self._original_mouse_press = self.viewer.scene().mousePressEvent
|
@@ -122,7 +131,6 @@ class MainWindow(QMainWindow):
|
|
122
131
|
self.right_panel.segment_table.itemSelectionChanged.connect(
|
123
132
|
self.highlight_selected_segments
|
124
133
|
)
|
125
|
-
self.right_panel.segment_table.itemChanged.connect(self.handle_class_id_change)
|
126
134
|
self.right_panel.class_table.itemChanged.connect(self.handle_alias_change)
|
127
135
|
self.right_panel.btn_reassign_classes.clicked.connect(self.reassign_class_ids)
|
128
136
|
self.right_panel.class_filter_combo.currentIndexChanged.connect(
|
@@ -137,11 +145,131 @@ class MainWindow(QMainWindow):
|
|
137
145
|
self.control_panel.btn_clear_points.clicked.connect(self.clear_all_points)
|
138
146
|
self.control_panel.btn_fit_view.clicked.connect(self.viewer.fitInView)
|
139
147
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
148
|
+
self.control_panel.size_slider.valueChanged.connect(self.set_annotation_size)
|
149
|
+
self.control_panel.pan_slider.valueChanged.connect(self.set_pan_multiplier)
|
150
|
+
self.control_panel.join_slider.valueChanged.connect(
|
151
|
+
self.set_polygon_join_threshold
|
152
|
+
)
|
153
|
+
|
154
|
+
self.control_panel.chk_save_npz.stateChanged.connect(
|
155
|
+
self.handle_save_checkbox_change
|
156
|
+
)
|
157
|
+
self.control_panel.chk_save_txt.stateChanged.connect(
|
158
|
+
self.handle_save_checkbox_change
|
159
|
+
)
|
160
|
+
|
161
|
+
self.control_panel.btn_toggle_visibility.clicked.connect(self.toggle_left_panel)
|
162
|
+
self.right_panel.btn_toggle_visibility.clicked.connect(self.toggle_right_panel)
|
163
|
+
|
164
|
+
QShortcut(QKeySequence(Qt.Key.Key_Right), self, self.load_next_image)
|
165
|
+
QShortcut(QKeySequence(Qt.Key.Key_Left), self, self.load_previous_image)
|
166
|
+
QShortcut(QKeySequence(Qt.Key.Key_1), self, self.set_sam_mode)
|
167
|
+
QShortcut(QKeySequence(Qt.Key.Key_2), self, self.set_polygon_mode)
|
168
|
+
QShortcut(QKeySequence(Qt.Key.Key_E), self, self.toggle_selection_mode)
|
169
|
+
QShortcut(QKeySequence(Qt.Key.Key_Q), self, self.toggle_pan_mode)
|
170
|
+
QShortcut(QKeySequence(Qt.Key.Key_R), self, self.toggle_edit_mode)
|
171
|
+
QShortcut(QKeySequence(Qt.Key.Key_C), self, self.clear_all_points)
|
172
|
+
QShortcut(QKeySequence(Qt.Key.Key_Escape), self, self.handle_escape_press)
|
173
|
+
QShortcut(QKeySequence(Qt.Key.Key_V), self, self.delete_selected_segments)
|
174
|
+
QShortcut(
|
175
|
+
QKeySequence(Qt.Key.Key_Backspace), self, self.delete_selected_segments
|
176
|
+
)
|
177
|
+
QShortcut(QKeySequence(Qt.Key.Key_M), self, self.handle_merge_press)
|
178
|
+
QShortcut(QKeySequence("Ctrl+Z"), self, self.undo_last_action)
|
179
|
+
QShortcut(
|
180
|
+
QKeySequence("Ctrl+A"), self, self.right_panel.segment_table.selectAll
|
181
|
+
)
|
182
|
+
QShortcut(QKeySequence(Qt.Key.Key_Space), self, self.handle_space_press)
|
183
|
+
QShortcut(QKeySequence(Qt.Key.Key_Return), self, self.handle_enter_press)
|
184
|
+
QShortcut(QKeySequence(Qt.Key.Key_Enter), self, self.handle_enter_press)
|
185
|
+
QShortcut(QKeySequence(Qt.Key.Key_Period), self, self.viewer.fitInView)
|
186
|
+
|
187
|
+
def toggle_left_panel(self):
|
188
|
+
is_visible = self.control_panel.main_controls_widget.isVisible()
|
189
|
+
self.control_panel.main_controls_widget.setVisible(not is_visible)
|
190
|
+
if is_visible:
|
191
|
+
self.control_panel.btn_toggle_visibility.setText("> Show")
|
192
|
+
self.control_panel.setFixedWidth(
|
193
|
+
self.control_panel.btn_toggle_visibility.sizeHint().width() + 20
|
194
|
+
)
|
195
|
+
else:
|
196
|
+
self.control_panel.btn_toggle_visibility.setText("< Hide")
|
197
|
+
self.control_panel.setFixedWidth(250)
|
198
|
+
|
199
|
+
def toggle_right_panel(self):
|
200
|
+
is_visible = self.right_panel.main_controls_widget.isVisible()
|
201
|
+
self.right_panel.main_controls_widget.setVisible(not is_visible)
|
202
|
+
layout = self.right_panel.v_layout
|
203
|
+
|
204
|
+
if is_visible: # Content is now hidden
|
205
|
+
layout.addStretch(1)
|
206
|
+
self.right_panel.btn_toggle_visibility.setText("< Show")
|
207
|
+
self.right_panel.setFixedWidth(
|
208
|
+
self.right_panel.btn_toggle_visibility.sizeHint().width() + 20
|
209
|
+
)
|
210
|
+
else: # Content is now visible
|
211
|
+
# Remove the stretch so the content can expand
|
212
|
+
for i in range(layout.count()):
|
213
|
+
item = layout.itemAt(i)
|
214
|
+
if isinstance(item, QSpacerItem):
|
215
|
+
layout.removeItem(item)
|
216
|
+
break
|
217
|
+
self.right_panel.btn_toggle_visibility.setText("Hide >")
|
218
|
+
self.right_panel.setFixedWidth(350)
|
219
|
+
|
220
|
+
def handle_save_checkbox_change(self):
|
221
|
+
is_npz_checked = self.control_panel.chk_save_npz.isChecked()
|
222
|
+
is_txt_checked = self.control_panel.chk_save_txt.isChecked()
|
223
|
+
|
224
|
+
if not is_npz_checked and not is_txt_checked:
|
225
|
+
sender = self.sender()
|
226
|
+
if sender == self.control_panel.chk_save_npz:
|
227
|
+
self.control_panel.chk_save_txt.setChecked(True)
|
228
|
+
else:
|
229
|
+
self.control_panel.chk_save_npz.setChecked(True)
|
230
|
+
|
231
|
+
def set_annotation_size(self, value):
|
232
|
+
multiplier = value / 10.0
|
233
|
+
self.point_radius = self._original_point_radius * multiplier
|
234
|
+
self.line_thickness = self._original_line_thickness * multiplier
|
235
|
+
|
236
|
+
self.control_panel.size_label.setText(f"Annotation Size: {multiplier:.1f}x")
|
237
|
+
|
238
|
+
if self.control_panel.size_slider.value() != value:
|
239
|
+
self.control_panel.size_slider.setValue(value)
|
240
|
+
|
241
|
+
self.display_all_segments()
|
242
|
+
self.clear_all_points()
|
243
|
+
|
244
|
+
def set_pan_multiplier(self, value):
|
245
|
+
self.pan_multiplier = value / 10.0
|
246
|
+
self.control_panel.pan_label.setText(f"Pan Speed: {self.pan_multiplier:.1f}x")
|
247
|
+
|
248
|
+
def set_polygon_join_threshold(self, value):
|
249
|
+
self.polygon_join_threshold = value
|
250
|
+
self.control_panel.join_label.setText(f"Polygon Join Distance: {value}px")
|
251
|
+
|
252
|
+
def handle_escape_press(self):
|
253
|
+
self.right_panel.segment_table.clearSelection()
|
254
|
+
self.right_panel.class_table.clearSelection()
|
255
|
+
self.clear_all_points()
|
256
|
+
self.viewer.setFocus()
|
257
|
+
|
258
|
+
def handle_space_press(self):
|
259
|
+
if self.mode == "polygon" and self.polygon_points:
|
260
|
+
self.finalize_polygon()
|
261
|
+
else:
|
262
|
+
self.save_current_segment()
|
263
|
+
|
264
|
+
def handle_enter_press(self):
|
265
|
+
if self.mode == "polygon" and self.polygon_points:
|
266
|
+
self.finalize_polygon()
|
267
|
+
else:
|
268
|
+
self.save_output_to_npz()
|
269
|
+
|
270
|
+
def handle_merge_press(self):
|
271
|
+
self.assign_selected_to_class()
|
272
|
+
self.right_panel.segment_table.clearSelection()
|
145
273
|
|
146
274
|
def show_notification(self, message, duration=3000):
|
147
275
|
self.control_panel.notification_label.setText(message)
|
@@ -152,10 +280,8 @@ class MainWindow(QMainWindow):
|
|
152
280
|
def _get_color_for_class(self, class_id):
|
153
281
|
if class_id is None:
|
154
282
|
return QColor.fromHsv(0, 0, 128)
|
155
|
-
|
156
283
|
hue = int((class_id * 222.4922359) % 360)
|
157
284
|
color = QColor.fromHsv(hue, 220, 220)
|
158
|
-
|
159
285
|
if not color.isValid():
|
160
286
|
return QColor(Qt.GlobalColor.white)
|
161
287
|
return color
|
@@ -256,6 +382,7 @@ class MainWindow(QMainWindow):
|
|
256
382
|
self.reset_state()
|
257
383
|
self.viewer.set_photo(pixmap)
|
258
384
|
self.sam_model.set_image(self.current_image_path)
|
385
|
+
self.load_class_aliases()
|
259
386
|
self.load_existing_mask()
|
260
387
|
self.right_panel.file_tree.setCurrentIndex(index)
|
261
388
|
self.viewer.setFocus()
|
@@ -288,9 +415,8 @@ class MainWindow(QMainWindow):
|
|
288
415
|
|
289
416
|
def reset_state(self):
|
290
417
|
self.clear_all_points()
|
291
|
-
# Preserve aliases between images in the same session
|
292
|
-
# self.class_aliases.clear()
|
293
418
|
self.segments.clear()
|
419
|
+
self.class_aliases.clear()
|
294
420
|
self.next_class_id = 0
|
295
421
|
self.update_all_lists()
|
296
422
|
items_to_remove = [
|
@@ -305,83 +431,55 @@ class MainWindow(QMainWindow):
|
|
305
431
|
|
306
432
|
def keyPressEvent(self, event):
|
307
433
|
key, mods = event.key(), event.modifiers()
|
308
|
-
|
434
|
+
|
435
|
+
if event.isAutoRepeat() and key not in {
|
436
|
+
Qt.Key.Key_W,
|
437
|
+
Qt.Key.Key_A,
|
438
|
+
Qt.Key.Key_S,
|
439
|
+
Qt.Key.Key_D,
|
440
|
+
}:
|
309
441
|
return
|
310
442
|
|
311
|
-
|
443
|
+
shift_multiplier = 5.0 if mods & Qt.KeyboardModifier.ShiftModifier else 1.0
|
312
444
|
|
313
445
|
if key == Qt.Key.Key_W:
|
314
|
-
amount = int(
|
446
|
+
amount = int(
|
447
|
+
self.viewer.height() * 0.1 * self.pan_multiplier * shift_multiplier
|
448
|
+
)
|
315
449
|
self.viewer.verticalScrollBar().setValue(
|
316
450
|
self.viewer.verticalScrollBar().value() - amount
|
317
451
|
)
|
318
452
|
elif key == Qt.Key.Key_S:
|
319
|
-
amount = int(
|
453
|
+
amount = int(
|
454
|
+
self.viewer.height() * 0.1 * self.pan_multiplier * shift_multiplier
|
455
|
+
)
|
320
456
|
self.viewer.verticalScrollBar().setValue(
|
321
457
|
self.viewer.verticalScrollBar().value() + amount
|
322
458
|
)
|
323
|
-
elif key == Qt.Key.Key_A
|
324
|
-
amount = int(
|
459
|
+
elif key == Qt.Key.Key_A:
|
460
|
+
amount = int(
|
461
|
+
self.viewer.width() * 0.1 * self.pan_multiplier * shift_multiplier
|
462
|
+
)
|
325
463
|
self.viewer.horizontalScrollBar().setValue(
|
326
464
|
self.viewer.horizontalScrollBar().value() - amount
|
327
465
|
)
|
328
466
|
elif key == Qt.Key.Key_D:
|
329
|
-
amount = int(
|
467
|
+
amount = int(
|
468
|
+
self.viewer.width() * 0.1 * self.pan_multiplier * shift_multiplier
|
469
|
+
)
|
330
470
|
self.viewer.horizontalScrollBar().setValue(
|
331
471
|
self.viewer.horizontalScrollBar().value() + amount
|
332
472
|
)
|
333
|
-
elif key == Qt.Key.Key_Period:
|
334
|
-
self.viewer.fitInView()
|
335
|
-
# Other keybindings
|
336
|
-
elif key == Qt.Key.Key_1:
|
337
|
-
self.set_sam_mode()
|
338
|
-
elif key == Qt.Key.Key_2:
|
339
|
-
self.set_polygon_mode()
|
340
|
-
elif key == Qt.Key.Key_E:
|
341
|
-
self.toggle_selection_mode()
|
342
|
-
elif key == Qt.Key.Key_Q:
|
343
|
-
self.toggle_pan_mode()
|
344
|
-
elif key == Qt.Key.Key_R:
|
345
|
-
self.toggle_edit_mode()
|
346
|
-
elif key == Qt.Key.Key_C or key == Qt.Key.Key_Escape:
|
347
|
-
self.clear_all_points()
|
348
|
-
elif key == Qt.Key.Key_V or key == Qt.Key.Key_Backspace:
|
349
|
-
self.delete_selected_segments()
|
350
|
-
elif key == Qt.Key.Key_M:
|
351
|
-
self.assign_selected_to_class()
|
352
|
-
self.right_panel.segment_table.clearSelection()
|
353
|
-
elif key == Qt.Key.Key_Z and mods == Qt.KeyboardModifier.ControlModifier:
|
354
|
-
self.undo_last_action()
|
355
|
-
elif key == Qt.Key.Key_A and mods == Qt.KeyboardModifier.ControlModifier:
|
356
|
-
self.right_panel.segment_table.selectAll()
|
357
|
-
elif key == Qt.Key.Key_Space:
|
358
|
-
if self.mode == "polygon" and self.polygon_points:
|
359
|
-
self.finalize_polygon()
|
360
|
-
else:
|
361
|
-
self.save_current_segment()
|
362
|
-
elif key == Qt.Key.Key_Return or key == Qt.Key.Key_Enter:
|
363
|
-
if self.mode == "polygon" and self.polygon_points:
|
364
|
-
self.finalize_polygon()
|
365
|
-
else:
|
366
|
-
self.save_output_to_npz()
|
367
473
|
elif (
|
368
474
|
key == Qt.Key.Key_Equal or key == Qt.Key.Key_Plus
|
369
475
|
) and mods == Qt.KeyboardModifier.ControlModifier:
|
370
|
-
|
371
|
-
self.
|
372
|
-
20, self.line_thickness + self._original_line_thickness
|
373
|
-
)
|
374
|
-
self.display_all_segments()
|
375
|
-
self.clear_all_points()
|
476
|
+
current_val = self.control_panel.size_slider.value()
|
477
|
+
self.control_panel.size_slider.setValue(current_val + 1)
|
376
478
|
elif key == Qt.Key.Key_Minus and mods == Qt.KeyboardModifier.ControlModifier:
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
0.5, self.line_thickness - self._original_line_thickness
|
382
|
-
)
|
383
|
-
self.display_all_segments()
|
384
|
-
self.clear_all_points()
|
479
|
+
current_val = self.control_panel.size_slider.value()
|
480
|
+
self.control_panel.size_slider.setValue(current_val - 1)
|
481
|
+
else:
|
482
|
+
super().keyPressEvent(event)
|
385
483
|
|
386
484
|
def scene_mouse_press(self, event):
|
387
485
|
self._original_mouse_press(event)
|
@@ -432,15 +530,12 @@ class MainWindow(QMainWindow):
|
|
432
530
|
elif self.mode == "polygon" and self.polygon_points:
|
433
531
|
if self.rubber_band_line is None:
|
434
532
|
self.rubber_band_line = QGraphicsLineItem()
|
435
|
-
|
436
533
|
line_color = QColor(Qt.GlobalColor.white)
|
437
534
|
line_color.setAlpha(150)
|
438
|
-
|
439
535
|
self.rubber_band_line.setPen(
|
440
536
|
QPen(line_color, self.line_thickness, Qt.PenStyle.DotLine)
|
441
537
|
)
|
442
538
|
self.viewer.scene().addItem(self.rubber_band_line)
|
443
|
-
|
444
539
|
self.rubber_band_line.setLine(
|
445
540
|
self.polygon_points[-1].x(),
|
446
541
|
self.polygon_points[-1].y(),
|
@@ -454,7 +549,6 @@ class MainWindow(QMainWindow):
|
|
454
549
|
def scene_mouse_release(self, event):
|
455
550
|
if self.mode == "pan":
|
456
551
|
self.viewer.set_cursor(Qt.CursorShape.OpenHandCursor)
|
457
|
-
|
458
552
|
if self.mode == "edit" and self.is_dragging_polygon:
|
459
553
|
self.is_dragging_polygon = False
|
460
554
|
self.drag_initial_vertices.clear()
|
@@ -463,12 +557,10 @@ class MainWindow(QMainWindow):
|
|
463
557
|
def undo_last_action(self):
|
464
558
|
if self.mode == "polygon" and self.polygon_points:
|
465
559
|
self.polygon_points.pop()
|
466
|
-
|
467
560
|
for item in self.polygon_preview_items:
|
468
561
|
if item.scene():
|
469
562
|
self.viewer.scene().removeItem(item)
|
470
563
|
self.polygon_preview_items.clear()
|
471
|
-
|
472
564
|
for point in self.polygon_points:
|
473
565
|
point_diameter = self.point_radius * 2
|
474
566
|
point_color = QColor(Qt.GlobalColor.blue)
|
@@ -483,21 +575,17 @@ class MainWindow(QMainWindow):
|
|
483
575
|
dot.setPen(QPen(Qt.GlobalColor.transparent))
|
484
576
|
self.viewer.scene().addItem(dot)
|
485
577
|
self.polygon_preview_items.append(dot)
|
486
|
-
|
487
578
|
self.draw_polygon_preview()
|
488
|
-
|
489
579
|
elif self.mode == "sam_points" and self.point_items:
|
490
580
|
item_to_remove = self.point_items.pop()
|
491
581
|
point_pos = item_to_remove.rect().topLeft() + QPointF(
|
492
582
|
self.point_radius, self.point_radius
|
493
583
|
)
|
494
584
|
point_coords = [int(point_pos.x()), int(point_pos.y())]
|
495
|
-
|
496
585
|
if point_coords in self.positive_points:
|
497
586
|
self.positive_points.remove(point_coords)
|
498
587
|
elif point_coords in self.negative_points:
|
499
588
|
self.negative_points.remove(point_coords)
|
500
|
-
|
501
589
|
self.viewer.scene().removeItem(item_to_remove)
|
502
590
|
self.update_segmentation()
|
503
591
|
|
@@ -574,14 +662,13 @@ class MainWindow(QMainWindow):
|
|
574
662
|
if existing_class_ids:
|
575
663
|
target_class_id = min(existing_class_ids)
|
576
664
|
else:
|
577
|
-
target_class_id = self.
|
665
|
+
target_class_id = self.next_class_id
|
578
666
|
|
579
667
|
for i in selected_indices:
|
580
668
|
self.segments[i]["class_id"] = target_class_id
|
581
669
|
|
582
670
|
self._update_next_class_id()
|
583
671
|
self.update_all_lists()
|
584
|
-
self.right_panel.segment_table.clearSelection()
|
585
672
|
self.viewer.setFocus()
|
586
673
|
|
587
674
|
def rasterize_polygon(self, vertices):
|
@@ -606,7 +693,6 @@ class MainWindow(QMainWindow):
|
|
606
693
|
for i, seg_dict in enumerate(self.segments):
|
607
694
|
self.segment_items[i] = []
|
608
695
|
class_id = seg_dict.get("class_id")
|
609
|
-
|
610
696
|
base_color = self._get_color_for_class(class_id)
|
611
697
|
|
612
698
|
if seg_dict["type"] == "Polygon":
|
@@ -621,7 +707,6 @@ class MainWindow(QMainWindow):
|
|
621
707
|
poly_item.setPen(QPen(Qt.GlobalColor.transparent))
|
622
708
|
self.viewer.scene().addItem(poly_item)
|
623
709
|
self.segment_items[i].append(poly_item)
|
624
|
-
|
625
710
|
base_color.setAlpha(150)
|
626
711
|
vertex_color = QBrush(base_color)
|
627
712
|
point_diameter = self.point_radius * 2
|
@@ -658,7 +743,6 @@ class MainWindow(QMainWindow):
|
|
658
743
|
hover_pixmap = mask_to_pixmap(
|
659
744
|
seg_dict["mask"], base_color.getRgb()[:3], alpha=170
|
660
745
|
)
|
661
|
-
|
662
746
|
pixmap_item = HoverablePixmapItem()
|
663
747
|
pixmap_item.set_pixmaps(default_pixmap, hover_pixmap)
|
664
748
|
self.viewer.scene().addItem(pixmap_item)
|
@@ -697,7 +781,7 @@ class MainWindow(QMainWindow):
|
|
697
781
|
self.highlight_items.append(highlight_item)
|
698
782
|
|
699
783
|
def update_all_lists(self):
|
700
|
-
self.update_class_list()
|
784
|
+
self.update_class_list()
|
701
785
|
self.update_class_filter_combo()
|
702
786
|
self.update_segment_table()
|
703
787
|
self.display_all_segments()
|
@@ -727,19 +811,24 @@ class MainWindow(QMainWindow):
|
|
727
811
|
for row, (original_index, seg) in enumerate(display_segments):
|
728
812
|
class_id = seg.get("class_id")
|
729
813
|
color = self._get_color_for_class(class_id)
|
730
|
-
|
731
814
|
class_id_str = str(class_id) if class_id is not None else "N/A"
|
815
|
+
|
816
|
+
alias_str = "N/A"
|
817
|
+
if class_id is not None:
|
818
|
+
alias_str = self.class_aliases.get(class_id, str(class_id))
|
819
|
+
alias_item = QTableWidgetItem(alias_str)
|
820
|
+
|
732
821
|
index_item = NumericTableWidgetItem(str(original_index + 1))
|
733
822
|
class_item = NumericTableWidgetItem(class_id_str)
|
734
|
-
type_item = QTableWidgetItem(seg.get("type", "N/A"))
|
735
823
|
|
736
824
|
index_item.setFlags(index_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
737
|
-
|
825
|
+
class_item.setFlags(class_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
826
|
+
alias_item.setFlags(alias_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
738
827
|
index_item.setData(Qt.ItemDataRole.UserRole, original_index)
|
739
828
|
|
740
829
|
table.setItem(row, 0, index_item)
|
741
830
|
table.setItem(row, 1, class_item)
|
742
|
-
table.setItem(row, 2,
|
831
|
+
table.setItem(row, 2, alias_item)
|
743
832
|
|
744
833
|
for col in range(table.columnCount()):
|
745
834
|
if table.item(row, col):
|
@@ -759,19 +848,7 @@ class MainWindow(QMainWindow):
|
|
759
848
|
class_table = self.right_panel.class_table
|
760
849
|
class_table.blockSignals(True)
|
761
850
|
|
762
|
-
|
763
|
-
current_aliases = {}
|
764
|
-
for row in range(class_table.rowCount()):
|
765
|
-
try:
|
766
|
-
alias = class_table.item(row, 0).text()
|
767
|
-
cid = int(class_table.item(row, 1).text())
|
768
|
-
current_aliases[cid] = alias
|
769
|
-
except (AttributeError, ValueError):
|
770
|
-
continue
|
771
|
-
self.class_aliases.update(current_aliases)
|
772
|
-
|
773
|
-
class_table.clearContents()
|
774
|
-
|
851
|
+
preserved_aliases = self.class_aliases.copy()
|
775
852
|
unique_class_ids = sorted(
|
776
853
|
list(
|
777
854
|
{
|
@@ -781,19 +858,22 @@ class MainWindow(QMainWindow):
|
|
781
858
|
}
|
782
859
|
)
|
783
860
|
)
|
784
|
-
class_table.setRowCount(len(unique_class_ids))
|
785
861
|
|
862
|
+
new_aliases = {}
|
863
|
+
for cid in unique_class_ids:
|
864
|
+
new_aliases[cid] = preserved_aliases.get(cid, str(cid))
|
865
|
+
|
866
|
+
self.class_aliases = new_aliases
|
867
|
+
|
868
|
+
class_table.clearContents()
|
869
|
+
class_table.setRowCount(len(unique_class_ids))
|
786
870
|
for row, cid in enumerate(unique_class_ids):
|
787
|
-
|
788
|
-
alias_item = QTableWidgetItem(alias)
|
871
|
+
alias_item = QTableWidgetItem(self.class_aliases.get(cid))
|
789
872
|
id_item = QTableWidgetItem(str(cid))
|
790
|
-
|
791
873
|
id_item.setFlags(id_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
792
|
-
|
793
874
|
color = self._get_color_for_class(cid)
|
794
875
|
alias_item.setBackground(QBrush(color))
|
795
876
|
id_item.setBackground(QBrush(color))
|
796
|
-
|
797
877
|
class_table.setItem(row, 0, alias_item)
|
798
878
|
class_table.setItem(row, 1, id_item)
|
799
879
|
|
@@ -810,7 +890,6 @@ class MainWindow(QMainWindow):
|
|
810
890
|
}
|
811
891
|
)
|
812
892
|
)
|
813
|
-
|
814
893
|
current_selection = combo.currentText()
|
815
894
|
combo.blockSignals(True)
|
816
895
|
combo.clear()
|
@@ -821,7 +900,6 @@ class MainWindow(QMainWindow):
|
|
821
900
|
for cid in unique_class_ids
|
822
901
|
]
|
823
902
|
)
|
824
|
-
|
825
903
|
if combo.findText(current_selection) > -1:
|
826
904
|
combo.setCurrentText(current_selection)
|
827
905
|
else:
|
@@ -830,7 +908,6 @@ class MainWindow(QMainWindow):
|
|
830
908
|
|
831
909
|
def reassign_class_ids(self):
|
832
910
|
class_table = self.right_panel.class_table
|
833
|
-
|
834
911
|
ordered_ids = []
|
835
912
|
for row in range(class_table.rowCount()):
|
836
913
|
id_item = class_table.item(row, 1)
|
@@ -839,75 +916,37 @@ class MainWindow(QMainWindow):
|
|
839
916
|
ordered_ids.append(int(id_item.text()))
|
840
917
|
except ValueError:
|
841
918
|
continue
|
842
|
-
|
843
919
|
id_map = {old_id: new_id for new_id, old_id in enumerate(ordered_ids)}
|
844
|
-
|
845
920
|
for seg in self.segments:
|
846
921
|
old_id = seg.get("class_id")
|
847
922
|
if old_id in id_map:
|
848
923
|
seg["class_id"] = id_map[old_id]
|
849
|
-
|
850
924
|
new_aliases = {
|
851
925
|
id_map[old_id]: self.class_aliases.get(old_id, str(old_id))
|
852
926
|
for old_id in ordered_ids
|
853
927
|
if old_id in self.class_aliases
|
854
928
|
}
|
855
929
|
self.class_aliases = new_aliases
|
856
|
-
|
857
930
|
self._update_next_class_id()
|
858
931
|
self.update_all_lists()
|
859
932
|
self.viewer.setFocus()
|
860
933
|
|
861
934
|
def handle_alias_change(self, item):
|
862
|
-
if item.column() != 0:
|
935
|
+
if item.column() != 0:
|
863
936
|
return
|
864
|
-
|
865
937
|
class_table = self.right_panel.class_table
|
866
938
|
class_table.blockSignals(True)
|
867
|
-
|
868
939
|
id_item = class_table.item(item.row(), 1)
|
869
940
|
if id_item:
|
870
941
|
try:
|
871
942
|
class_id = int(id_item.text())
|
872
943
|
self.class_aliases[class_id] = item.text()
|
873
944
|
except (ValueError, AttributeError):
|
874
|
-
pass
|
875
|
-
|
945
|
+
pass
|
876
946
|
class_table.blockSignals(False)
|
877
|
-
self.update_class_filter_combo() # Refresh filter to show new alias
|
878
947
|
|
879
|
-
|
880
|
-
|
881
|
-
return
|
882
|
-
table = self.right_panel.segment_table
|
883
|
-
index_item = table.item(item.row(), 0)
|
884
|
-
if not index_item:
|
885
|
-
return
|
886
|
-
|
887
|
-
table.blockSignals(True)
|
888
|
-
try:
|
889
|
-
new_class_id_text = item.text()
|
890
|
-
if not new_class_id_text.strip():
|
891
|
-
raise ValueError("Class ID cannot be empty.")
|
892
|
-
new_class_id = int(new_class_id_text)
|
893
|
-
original_index = index_item.data(Qt.ItemDataRole.UserRole)
|
894
|
-
|
895
|
-
if original_index is None or original_index >= len(self.segments):
|
896
|
-
raise IndexError("Invalid segment index found in table.")
|
897
|
-
|
898
|
-
self.segments[original_index]["class_id"] = new_class_id
|
899
|
-
self._update_next_class_id()
|
900
|
-
self.update_all_lists()
|
901
|
-
except (ValueError, TypeError, AttributeError, IndexError) as e:
|
902
|
-
original_index = index_item.data(Qt.ItemDataRole.UserRole)
|
903
|
-
if original_index is not None and original_index < len(self.segments):
|
904
|
-
original_class_id = self.segments[original_index].get("class_id")
|
905
|
-
item.setText(
|
906
|
-
str(original_class_id) if original_class_id is not None else "N/A"
|
907
|
-
)
|
908
|
-
finally:
|
909
|
-
table.blockSignals(False)
|
910
|
-
self.viewer.setFocus()
|
948
|
+
self.update_class_filter_combo()
|
949
|
+
self.update_segment_table()
|
911
950
|
|
912
951
|
def get_selected_segment_indices(self):
|
913
952
|
table = self.right_panel.segment_table
|
@@ -920,89 +959,110 @@ class MainWindow(QMainWindow):
|
|
920
959
|
]
|
921
960
|
|
922
961
|
def save_output_to_npz(self):
|
923
|
-
|
962
|
+
save_npz = self.control_panel.chk_save_npz.isChecked()
|
963
|
+
save_txt = self.control_panel.chk_save_txt.isChecked()
|
964
|
+
save_aliases = self.control_panel.chk_save_class_aliases.isChecked()
|
965
|
+
|
966
|
+
if not self.current_image_path or not any([save_npz, save_txt, save_aliases]):
|
924
967
|
return
|
968
|
+
|
925
969
|
self.right_panel.status_label.setText("Saving...")
|
926
970
|
QApplication.processEvents()
|
927
971
|
|
928
|
-
|
929
|
-
h, w = (
|
930
|
-
self.viewer._pixmap_item.pixmap().height(),
|
931
|
-
self.viewer._pixmap_item.pixmap().width(),
|
932
|
-
)
|
933
|
-
|
934
|
-
class_table = self.right_panel.class_table
|
935
|
-
ordered_ids = [
|
936
|
-
int(class_table.item(row, 1).text())
|
937
|
-
for row in range(class_table.rowCount())
|
938
|
-
if class_table.item(row, 1) is not None
|
939
|
-
]
|
940
|
-
|
941
|
-
if not ordered_ids:
|
942
|
-
self.right_panel.status_label.setText("Save failed: No classes defined.")
|
943
|
-
QTimer.singleShot(3000, lambda: self.right_panel.status_label.clear())
|
944
|
-
return
|
945
|
-
|
946
|
-
id_map = {old_id: new_id for new_id, old_id in enumerate(ordered_ids)}
|
947
|
-
num_final_classes = len(ordered_ids)
|
948
|
-
final_mask_tensor = np.zeros((h, w, num_final_classes), dtype=np.uint8)
|
972
|
+
saved_something = False
|
949
973
|
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
if seg["type"] == "Polygon"
|
958
|
-
else seg.get("mask")
|
959
|
-
)
|
960
|
-
if mask is not None:
|
961
|
-
final_mask_tensor[:, :, new_channel_idx] = np.logical_or(
|
962
|
-
final_mask_tensor[:, :, new_channel_idx], mask
|
974
|
+
if save_npz or save_txt:
|
975
|
+
if not self.segments:
|
976
|
+
self.show_notification("No segments to save.")
|
977
|
+
else:
|
978
|
+
h, w = (
|
979
|
+
self.viewer._pixmap_item.pixmap().height(),
|
980
|
+
self.viewer._pixmap_item.pixmap().width(),
|
963
981
|
)
|
982
|
+
class_table = self.right_panel.class_table
|
983
|
+
ordered_ids = [
|
984
|
+
int(class_table.item(row, 1).text())
|
985
|
+
for row in range(class_table.rowCount())
|
986
|
+
if class_table.item(row, 1) is not None
|
987
|
+
]
|
964
988
|
|
965
|
-
|
989
|
+
if not ordered_ids:
|
990
|
+
self.show_notification("No classes defined for mask saving.")
|
991
|
+
else:
|
992
|
+
id_map = {
|
993
|
+
old_id: new_id for new_id, old_id in enumerate(ordered_ids)
|
994
|
+
}
|
995
|
+
num_final_classes = len(ordered_ids)
|
996
|
+
final_mask_tensor = np.zeros(
|
997
|
+
(h, w, num_final_classes), dtype=np.uint8
|
998
|
+
)
|
966
999
|
|
967
|
-
|
968
|
-
|
1000
|
+
for seg in self.segments:
|
1001
|
+
class_id = seg.get("class_id")
|
1002
|
+
if class_id not in id_map:
|
1003
|
+
continue
|
1004
|
+
new_channel_idx = id_map[class_id]
|
1005
|
+
mask = (
|
1006
|
+
self.rasterize_polygon(seg["vertices"])
|
1007
|
+
if seg["type"] == "Polygon"
|
1008
|
+
else seg.get("mask")
|
1009
|
+
)
|
1010
|
+
if mask is not None:
|
1011
|
+
final_mask_tensor[:, :, new_channel_idx] = np.logical_or(
|
1012
|
+
final_mask_tensor[:, :, new_channel_idx], mask
|
1013
|
+
)
|
1014
|
+
if save_npz:
|
1015
|
+
npz_path = os.path.splitext(self.current_image_path)[0] + ".npz"
|
1016
|
+
np.savez_compressed(
|
1017
|
+
npz_path, mask=final_mask_tensor.astype(np.uint8)
|
1018
|
+
)
|
1019
|
+
self.file_model.set_highlighted_path(npz_path)
|
1020
|
+
QTimer.singleShot(
|
1021
|
+
1500, lambda: self.file_model.set_highlighted_path(None)
|
1022
|
+
)
|
1023
|
+
saved_something = True
|
1024
|
+
if save_txt:
|
1025
|
+
self.generate_yolo_annotations(final_mask_tensor)
|
1026
|
+
saved_something = True
|
1027
|
+
|
1028
|
+
if save_aliases:
|
1029
|
+
aliases_path = os.path.splitext(self.current_image_path)[0] + ".json"
|
1030
|
+
aliases_to_save = {str(k): v for k, v in self.class_aliases.items()}
|
1031
|
+
with open(aliases_path, "w") as f:
|
1032
|
+
json.dump(aliases_to_save, f, indent=4)
|
1033
|
+
saved_something = True
|
1034
|
+
|
1035
|
+
if saved_something:
|
1036
|
+
self.right_panel.status_label.setText("Saved!")
|
1037
|
+
else:
|
1038
|
+
self.right_panel.status_label.clear()
|
969
1039
|
|
970
|
-
self.right_panel.status_label.setText("Saved!")
|
971
|
-
self.generate_yolo_annotations(npz_file_path=output_path)
|
972
1040
|
QTimer.singleShot(3000, lambda: self.right_panel.status_label.clear())
|
973
1041
|
|
974
|
-
def generate_yolo_annotations(self,
|
1042
|
+
def generate_yolo_annotations(self, mask_tensor):
|
975
1043
|
output_path = os.path.splitext(self.current_image_path)[0] + ".txt"
|
976
|
-
|
977
|
-
|
978
|
-
img = npz_data["mask"][:, :, :]
|
979
|
-
num_channels = img.shape[2] # C
|
980
|
-
h, w = img.shape[:2] # H, W
|
1044
|
+
h, w, num_channels = mask_tensor.shape
|
981
1045
|
|
982
1046
|
directory_path = os.path.dirname(output_path)
|
983
1047
|
os.makedirs(directory_path, exist_ok=True)
|
984
1048
|
|
985
1049
|
yolo_annotations = []
|
986
|
-
|
987
1050
|
for channel in range(num_channels):
|
988
|
-
single_channel_image =
|
1051
|
+
single_channel_image = mask_tensor[:, :, channel]
|
1052
|
+
if not np.any(single_channel_image):
|
1053
|
+
continue
|
1054
|
+
|
989
1055
|
contours, _ = cv2.findContours(
|
990
1056
|
single_channel_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
991
1057
|
)
|
992
|
-
|
993
|
-
class_id = channel # Use the channel index as the class ID
|
994
|
-
|
1058
|
+
class_id = channel
|
995
1059
|
for contour in contours:
|
996
1060
|
x, y, width, height = cv2.boundingRect(contour)
|
997
|
-
center_x = x + width / 2
|
998
|
-
center_y = y + height / 2
|
999
|
-
|
1000
|
-
normalized_center_x = center_x / w
|
1001
|
-
normalized_center_y = center_y / h
|
1061
|
+
center_x = (x + width / 2) / w
|
1062
|
+
center_y = (y + height / 2) / h
|
1002
1063
|
normalized_width = width / w
|
1003
1064
|
normalized_height = height / h
|
1004
|
-
|
1005
|
-
yolo_entry = f"{class_id} {normalized_center_x} {normalized_center_y} {normalized_width} {normalized_height}"
|
1065
|
+
yolo_entry = f"{class_id} {center_x} {center_y} {normalized_width} {normalized_height}"
|
1006
1066
|
yolo_annotations.append(yolo_entry)
|
1007
1067
|
|
1008
1068
|
with open(output_path, "w") as file:
|
@@ -1040,6 +1100,20 @@ class MainWindow(QMainWindow):
|
|
1040
1100
|
self.update_all_lists()
|
1041
1101
|
self.viewer.setFocus()
|
1042
1102
|
|
1103
|
+
def load_class_aliases(self):
|
1104
|
+
if not self.current_image_path:
|
1105
|
+
return
|
1106
|
+
json_path = os.path.splitext(self.current_image_path)[0] + ".json"
|
1107
|
+
if os.path.exists(json_path):
|
1108
|
+
try:
|
1109
|
+
with open(json_path, "r") as f:
|
1110
|
+
loaded_aliases = json.load(f)
|
1111
|
+
# JSON loads keys as strings, convert them to int
|
1112
|
+
self.class_aliases = {int(k): v for k, v in loaded_aliases.items()}
|
1113
|
+
except (json.JSONDecodeError, ValueError) as e:
|
1114
|
+
print(f"Error loading class aliases from {json_path}: {e}")
|
1115
|
+
self.class_aliases.clear()
|
1116
|
+
|
1043
1117
|
def load_existing_mask(self):
|
1044
1118
|
if not self.current_image_path:
|
1045
1119
|
return
|
@@ -1068,12 +1142,10 @@ class MainWindow(QMainWindow):
|
|
1068
1142
|
def add_point(self, pos, positive):
|
1069
1143
|
point_list = self.positive_points if positive else self.negative_points
|
1070
1144
|
point_list.append([int(pos.x()), int(pos.y())])
|
1071
|
-
|
1072
1145
|
point_color = (
|
1073
1146
|
QColor(Qt.GlobalColor.green) if positive else QColor(Qt.GlobalColor.red)
|
1074
1147
|
)
|
1075
1148
|
point_color.setAlpha(150)
|
1076
|
-
|
1077
1149
|
point_diameter = self.point_radius * 2
|
1078
1150
|
point_item = QGraphicsEllipseItem(
|
1079
1151
|
pos.x() - self.point_radius,
|
@@ -1120,17 +1192,15 @@ class MainWindow(QMainWindow):
|
|
1120
1192
|
(pos.x() - self.polygon_points[0].x()) ** 2
|
1121
1193
|
+ (pos.y() - self.polygon_points[0].y()) ** 2
|
1122
1194
|
)
|
1123
|
-
<
|
1195
|
+
< self.polygon_join_threshold**2
|
1124
1196
|
):
|
1125
1197
|
if len(self.polygon_points) > 2:
|
1126
1198
|
self.finalize_polygon()
|
1127
1199
|
return
|
1128
1200
|
self.polygon_points.append(pos)
|
1129
1201
|
point_diameter = self.point_radius * 2
|
1130
|
-
|
1131
1202
|
point_color = QColor(Qt.GlobalColor.blue)
|
1132
1203
|
point_color.setAlpha(150)
|
1133
|
-
|
1134
1204
|
dot = QGraphicsEllipseItem(
|
1135
1205
|
pos.x() - self.point_radius,
|
1136
1206
|
pos.y() - self.point_radius,
|
@@ -1144,7 +1214,6 @@ class MainWindow(QMainWindow):
|
|
1144
1214
|
self.draw_polygon_preview()
|
1145
1215
|
|
1146
1216
|
def draw_polygon_preview(self):
|
1147
|
-
# Clean up old preview lines/polygons
|
1148
1217
|
for item in self.polygon_preview_items:
|
1149
1218
|
if not isinstance(item, QGraphicsEllipseItem):
|
1150
1219
|
if item.scene():
|
@@ -1154,7 +1223,6 @@ class MainWindow(QMainWindow):
|
|
1154
1223
|
for item in self.polygon_preview_items
|
1155
1224
|
if isinstance(item, QGraphicsEllipseItem)
|
1156
1225
|
]
|
1157
|
-
|
1158
1226
|
if len(self.polygon_points) > 2:
|
1159
1227
|
preview_poly = QGraphicsPolygonItem(QPolygonF(self.polygon_points))
|
1160
1228
|
preview_poly.setBrush(QBrush(QColor(0, 255, 255, 100)))
|
lazylabel/utils.py
CHANGED
@@ -5,7 +5,7 @@ from PyQt6.QtGui import QImage, QPixmap
|
|
5
5
|
def mask_to_pixmap(mask, color, alpha=150):
|
6
6
|
colored_mask = np.zeros((mask.shape[0], mask.shape[1], 4), dtype=np.uint8)
|
7
7
|
colored_mask[mask, :3] = color
|
8
|
-
colored_mask[mask, 3] = alpha
|
8
|
+
colored_mask[mask, 3] = alpha
|
9
9
|
image = QImage(
|
10
10
|
colored_mask.data, mask.shape[1], mask.shape[0], QImage.Format.Format_RGBA8888
|
11
11
|
)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
lazylabel/controls.py,sha256=kminJPdKDzrSacTaUuLYXVW_nyTOcoGV_iWlMHyOfsA,10533
|
2
|
+
lazylabel/custom_file_system_model.py,sha256=q4axrAQPQeURVz-sFeVl4s9pyY05OE9A2Ej95Ju1rXM,2488
|
3
|
+
lazylabel/editable_vertex.py,sha256=itGcZG5MyuctGfxjINu8IJBYFFsCGuE_YtsrfHICjiw,1115
|
4
|
+
lazylabel/hoverable_pixelmap_item.py,sha256=kJFOp7WXiyHpNf7l73TZjiob85jgP30b5MZvu_z5L3c,728
|
5
|
+
lazylabel/hoverable_polygon_item.py,sha256=-0l8C8PfsXtJGqvZZ2qtizxHmFwO8RCwz5UfjKpDvzY,775
|
6
|
+
lazylabel/main.py,sha256=C-ZHVFzAbnGPRDFCBTLvn3uBMuu_qq4UUsfF8cZmdns,50862
|
7
|
+
lazylabel/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
|
8
|
+
lazylabel/photo_viewer.py,sha256=PNgm0gU2gnIqvRkrGlQugdobGsKwAi3m3X6ZF487lCo,2055
|
9
|
+
lazylabel/reorderable_class_table.py,sha256=4c-iuSkPcmk5Aey5n2zz49O85x9TQPujKG-JLxtuBCo,2406
|
10
|
+
lazylabel/sam_model.py,sha256=9NB51Xq1P5dIxZMBdttwwRlszlJR3U5HRs83QsPLxNE,2595
|
11
|
+
lazylabel/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
|
12
|
+
lazylabel_gui-1.0.8.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
|
13
|
+
lazylabel_gui-1.0.8.dist-info/METADATA,sha256=1JlchgipzdT-mspfXCovQ1FalX0vyg4jdxFfaK1F9eA,6292
|
14
|
+
lazylabel_gui-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
15
|
+
lazylabel_gui-1.0.8.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
|
16
|
+
lazylabel_gui-1.0.8.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
|
17
|
+
lazylabel_gui-1.0.8.dist-info/RECORD,,
|
@@ -1,17 +0,0 @@
|
|
1
|
-
lazylabel/controls.py,sha256=WfI0aIO1nfJ7YGTsEyP5Oc6Cd5A1Tbar943eaE_-KEY,6312
|
2
|
-
lazylabel/custom_file_system_model.py,sha256=YSM1CN5bza7Q2Cjb4unXgvc-SbYwvhUQNS1_G7ncofk,2302
|
3
|
-
lazylabel/editable_vertex.py,sha256=itGcZG5MyuctGfxjINu8IJBYFFsCGuE_YtsrfHICjiw,1115
|
4
|
-
lazylabel/hoverable_pixelmap_item.py,sha256=kJFOp7WXiyHpNf7l73TZjiob85jgP30b5MZvu_z5L3c,728
|
5
|
-
lazylabel/hoverable_polygon_item.py,sha256=-0l8C8PfsXtJGqvZZ2qtizxHmFwO8RCwz5UfjKpDvzY,775
|
6
|
-
lazylabel/main.py,sha256=TPpPIv74IetnwQbQh8pUtUey74KO3rISFXPBLJSlrtM,46445
|
7
|
-
lazylabel/numeric_table_widget_item.py,sha256=ZnwaUvCeOGEX504DfbLHWKMKVMt5zSjdQkPjPCuYCcY,346
|
8
|
-
lazylabel/photo_viewer.py,sha256=PNgm0gU2gnIqvRkrGlQugdobGsKwAi3m3X6ZF487lCo,2055
|
9
|
-
lazylabel/reorderable_class_table.py,sha256=4c-iuSkPcmk5Aey5n2zz49O85x9TQPujKG-JLxtuBCo,2406
|
10
|
-
lazylabel/sam_model.py,sha256=9NB51Xq1P5dIxZMBdttwwRlszlJR3U5HRs83QsPLxNE,2595
|
11
|
-
lazylabel/utils.py,sha256=3NfzgxTgYoevv82owmcGzLqrIBdE_B4DlWgumVf4y_E,448
|
12
|
-
lazylabel_gui-1.0.7.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
|
13
|
-
lazylabel_gui-1.0.7.dist-info/METADATA,sha256=MskYLxZx4sccjcAcf2RWH_4LLPho4Xz0w8W8f8z7iU4,6292
|
14
|
-
lazylabel_gui-1.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
15
|
-
lazylabel_gui-1.0.7.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
|
16
|
-
lazylabel_gui-1.0.7.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
|
17
|
-
lazylabel_gui-1.0.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|