coralnet-toolbox 0.0.75__py2.py3-none-any.whl → 0.0.76__py2.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.
- coralnet_toolbox/Annotations/QtPolygonAnnotation.py +57 -12
- coralnet_toolbox/Annotations/QtRectangleAnnotation.py +44 -14
- coralnet_toolbox/Explorer/transformer_models.py +13 -2
- coralnet_toolbox/IO/QtExportMaskAnnotations.py +538 -403
- coralnet_toolbox/Icons/system_monitor.png +0 -0
- coralnet_toolbox/QtEventFilter.py +4 -4
- coralnet_toolbox/QtMainWindow.py +104 -64
- coralnet_toolbox/QtProgressBar.py +1 -0
- coralnet_toolbox/QtSystemMonitor.py +370 -0
- coralnet_toolbox/Results/ConvertResults.py +14 -8
- coralnet_toolbox/Results/ResultsProcessor.py +3 -2
- coralnet_toolbox/SAM/QtDeployGenerator.py +1 -1
- coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +15 -10
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +10 -6
- coralnet_toolbox/Tile/QtTileBatchInference.py +4 -4
- coralnet_toolbox/Tools/QtSAMTool.py +140 -91
- coralnet_toolbox/Transformers/Models/GroundingDINO.py +72 -0
- coralnet_toolbox/Transformers/Models/OWLViT.py +72 -0
- coralnet_toolbox/Transformers/Models/OmDetTurbo.py +68 -0
- coralnet_toolbox/Transformers/Models/QtBase.py +120 -0
- coralnet_toolbox/{AutoDistill → Transformers}/Models/__init__.py +1 -1
- coralnet_toolbox/{AutoDistill → Transformers}/QtBatchInference.py +15 -15
- coralnet_toolbox/{AutoDistill → Transformers}/QtDeployModel.py +18 -16
- coralnet_toolbox/{AutoDistill → Transformers}/__init__.py +1 -1
- coralnet_toolbox/__init__.py +1 -1
- coralnet_toolbox/utilities.py +0 -15
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.76.dist-info}/METADATA +9 -9
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.76.dist-info}/RECORD +33 -31
- coralnet_toolbox/AutoDistill/Models/GroundingDINO.py +0 -81
- coralnet_toolbox/AutoDistill/Models/OWLViT.py +0 -76
- coralnet_toolbox/AutoDistill/Models/OmDetTurbo.py +0 -75
- coralnet_toolbox/AutoDistill/Models/QtBase.py +0 -112
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.76.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.76.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.76.dist-info}/licenses/LICENSE.txt +0 -0
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.76.dist-info}/top_level.txt +0 -0
@@ -1,20 +1,19 @@
|
|
1
1
|
import warnings
|
2
|
-
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
3
|
-
|
4
2
|
import os
|
5
3
|
import ujson as json
|
6
4
|
|
7
5
|
import cv2
|
8
6
|
import numpy as np
|
9
7
|
import rasterio
|
10
|
-
from PIL import Image
|
8
|
+
from PIL import Image, ImageColor
|
11
9
|
|
12
|
-
from PyQt5.QtCore import Qt
|
10
|
+
from PyQt5.QtCore import Qt, pyqtSignal
|
11
|
+
from PyQt5.QtGui import QColor, QPainter, QPen
|
13
12
|
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
14
13
|
QCheckBox, QComboBox, QLineEdit, QPushButton, QFileDialog,
|
15
|
-
QApplication, QMessageBox, QLabel,
|
16
|
-
|
17
|
-
|
14
|
+
QApplication, QMessageBox, QLabel, QTableWidgetItem,
|
15
|
+
QButtonGroup, QWidget, QTableWidget, QHeaderView,
|
16
|
+
QAbstractItemView, QSpinBox, QRadioButton, QColorDialog)
|
18
17
|
|
19
18
|
from coralnet_toolbox.Annotations.QtPatchAnnotation import PatchAnnotation
|
20
19
|
from coralnet_toolbox.Annotations.QtPolygonAnnotation import PolygonAnnotation
|
@@ -22,12 +21,56 @@ from coralnet_toolbox.Annotations.QtRectangleAnnotation import RectangleAnnotati
|
|
22
21
|
from coralnet_toolbox.Annotations.QtMultiPolygonAnnotation import MultiPolygonAnnotation
|
23
22
|
|
24
23
|
from coralnet_toolbox.QtProgressBar import ProgressBar
|
25
|
-
|
26
24
|
from coralnet_toolbox.Icons import get_icon
|
27
25
|
|
26
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
27
|
+
|
28
28
|
|
29
29
|
# ----------------------------------------------------------------------------------------------------------------------
|
30
|
-
# Classes
|
30
|
+
# Helper Classes
|
31
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
32
|
+
|
33
|
+
|
34
|
+
class ColorSwatchWidget(QWidget):
|
35
|
+
"""A simple widget to display a color swatch with a border."""
|
36
|
+
def __init__(self, color, parent=None):
|
37
|
+
super().__init__(parent)
|
38
|
+
self.color = color
|
39
|
+
self.setFixedSize(24, 24)
|
40
|
+
|
41
|
+
def paintEvent(self, event):
|
42
|
+
painter = QPainter(self)
|
43
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
44
|
+
|
45
|
+
# Set the brush for the fill color
|
46
|
+
painter.setBrush(self.color)
|
47
|
+
|
48
|
+
# Set the pen for the black border
|
49
|
+
pen = QPen(QColor("black"))
|
50
|
+
pen.setWidth(1)
|
51
|
+
painter.setPen(pen)
|
52
|
+
|
53
|
+
# Draw the rectangle, adjusted inward so the border is fully visible
|
54
|
+
painter.drawRect(self.rect().adjusted(0, 0, -1, -1))
|
55
|
+
|
56
|
+
def setColor(self, color):
|
57
|
+
"""Update the swatch's color and repaint."""
|
58
|
+
self.color = color
|
59
|
+
self.update() # Triggers a repaint
|
60
|
+
|
61
|
+
|
62
|
+
class ClickableColorSwatchWidget(ColorSwatchWidget):
|
63
|
+
"""A ColorSwatchWidget that emits a clicked signal."""
|
64
|
+
clicked = pyqtSignal()
|
65
|
+
|
66
|
+
def mousePressEvent(self, event):
|
67
|
+
if event.button() == Qt.LeftButton:
|
68
|
+
self.clicked.emit()
|
69
|
+
super().mousePressEvent(event)
|
70
|
+
|
71
|
+
|
72
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
73
|
+
# Main Dialog Class
|
31
74
|
# ----------------------------------------------------------------------------------------------------------------------
|
32
75
|
|
33
76
|
|
@@ -40,60 +83,70 @@ class ExportMaskAnnotations(QDialog):
|
|
40
83
|
self.annotation_window = main_window.annotation_window
|
41
84
|
|
42
85
|
self.setWindowIcon(get_icon("coral.png"))
|
43
|
-
self.setWindowTitle("Export
|
44
|
-
self.resize(
|
86
|
+
self.setWindowTitle("Export Annotations to Masks")
|
87
|
+
self.resize(1000, 800)
|
45
88
|
|
46
|
-
self.
|
47
|
-
self.
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
self.setup_info_layout()
|
55
|
-
|
56
|
-
self.
|
57
|
-
|
58
|
-
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
self.
|
65
|
-
|
66
|
-
self.
|
89
|
+
self.mask_mode = 'semantic' # 'semantic', 'sfm', or 'rgb'
|
90
|
+
self.rgb_background_color = QColor(0, 0, 0)
|
91
|
+
|
92
|
+
# Main layout for the dialog
|
93
|
+
self.main_layout = QVBoxLayout(self)
|
94
|
+
|
95
|
+
# Top section
|
96
|
+
top_section = QVBoxLayout()
|
97
|
+
self.setup_info_layout(parent_layout=top_section)
|
98
|
+
self.setup_output_layout(parent_layout=top_section)
|
99
|
+
self.setup_mask_format_layout(parent_layout=top_section)
|
100
|
+
self.main_layout.addLayout(top_section)
|
101
|
+
|
102
|
+
# Middle section
|
103
|
+
columns_layout = QHBoxLayout()
|
104
|
+
left_col = QVBoxLayout()
|
105
|
+
right_col = QVBoxLayout()
|
106
|
+
|
107
|
+
self.setup_annotation_layout(parent_layout=left_col)
|
108
|
+
self.setup_image_selection_layout(parent_layout=left_col)
|
109
|
+
self.setup_label_layout(parent_layout=right_col)
|
110
|
+
|
111
|
+
columns_layout.addLayout(left_col, 1)
|
112
|
+
columns_layout.addLayout(right_col, 2)
|
113
|
+
self.main_layout.addLayout(columns_layout)
|
114
|
+
|
115
|
+
# Bottom buttons
|
116
|
+
self.setup_buttons_layout(parent_layout=self.main_layout)
|
117
|
+
|
118
|
+
# Set initial mode and update UI
|
119
|
+
self.semantic_radio.setChecked(True)
|
120
|
+
self.update_ui_for_mode()
|
67
121
|
|
68
122
|
def showEvent(self, event):
|
69
|
-
"""Handle the show event"""
|
70
123
|
super().showEvent(event)
|
71
|
-
|
72
|
-
self.update_label_selection_list()
|
124
|
+
self.update_ui_for_mode()
|
73
125
|
|
74
|
-
def setup_info_layout(self):
|
75
|
-
"""
|
76
|
-
Set up the layout and widgets for the info layout.
|
77
|
-
"""
|
126
|
+
def setup_info_layout(self, parent_layout=None):
|
78
127
|
group_box = QGroupBox("Information")
|
79
128
|
layout = QVBoxLayout()
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
129
|
+
info_text = (
|
130
|
+
"This tool exports annotations to image masks for three primary use cases:<br><br>"
|
131
|
+
"<b>1. Semantic Segmentation (Integer IDs):</b> Creates masks where each class (e.g., coral, rock) "
|
132
|
+
"is represented by a unique integer value (1, 2, 3...). The background is typically 0. These are used "
|
133
|
+
"to train machine learning models.<br><br>"
|
134
|
+
"<b>2. Structure from Motion (SfM) (Binary Mask):</b> Creates masks where a foreground value "
|
135
|
+
"(e.g., 255) represents objects to keep, and a background value (e.g., 0) represents areas to "
|
136
|
+
"ignore. This is used by software like Metashape to improve 3D model reconstruction.<br><br>"
|
137
|
+
"<b>3. Visualization (RGB Colors):</b> Creates a human-readable color mask using the colors "
|
138
|
+
"assigned to each label. Ideal for reports, presentations, and qualitative analysis."
|
139
|
+
)
|
140
|
+
info_label = QLabel(info_text)
|
85
141
|
info_label.setWordWrap(True)
|
86
142
|
layout.addWidget(info_label)
|
87
|
-
|
88
143
|
group_box.setLayout(layout)
|
89
|
-
|
144
|
+
parent_layout.addWidget(group_box)
|
90
145
|
|
91
|
-
def setup_output_layout(self):
|
92
|
-
"""Setup the output directory and file format layout."""
|
146
|
+
def setup_output_layout(self, parent_layout=None):
|
93
147
|
groupbox = QGroupBox("Output Directory and File Format")
|
94
148
|
layout = QFormLayout()
|
95
149
|
|
96
|
-
# Output directory selection
|
97
150
|
output_dir_layout = QHBoxLayout()
|
98
151
|
self.output_dir_edit = QLineEdit()
|
99
152
|
self.output_dir_button = QPushButton("Browse...")
|
@@ -102,18 +155,58 @@ class ExportMaskAnnotations(QDialog):
|
|
102
155
|
output_dir_layout.addWidget(self.output_dir_button)
|
103
156
|
layout.addRow("Output Directory:", output_dir_layout)
|
104
157
|
|
105
|
-
|
106
|
-
self.output_name_edit = QLineEdit("")
|
158
|
+
self.output_name_edit = QLineEdit("masks")
|
107
159
|
layout.addRow("Folder Name:", self.output_name_edit)
|
108
160
|
|
109
161
|
groupbox.setLayout(layout)
|
110
|
-
|
162
|
+
parent_layout.addWidget(groupbox)
|
163
|
+
|
164
|
+
def setup_mask_format_layout(self, parent_layout=None):
|
165
|
+
groupbox = QGroupBox("Export Mode and Format")
|
166
|
+
main_layout = QVBoxLayout()
|
167
|
+
|
168
|
+
# Mode Selection
|
169
|
+
mode_layout = QHBoxLayout()
|
170
|
+
self.semantic_radio = QRadioButton("Semantic Segmentation (Integer IDs)")
|
171
|
+
self.sfm_radio = QRadioButton("Structure from Motion (Binary Mask)")
|
172
|
+
self.rgb_radio = QRadioButton("Visualization (RGB Colors)")
|
173
|
+
|
174
|
+
self.mode_group = QButtonGroup(self)
|
175
|
+
self.mode_group.addButton(self.semantic_radio)
|
176
|
+
self.mode_group.addButton(self.sfm_radio)
|
177
|
+
self.mode_group.addButton(self.rgb_radio)
|
178
|
+
self.mode_group.buttonClicked.connect(self.update_ui_for_mode)
|
179
|
+
|
180
|
+
mode_layout.addWidget(self.semantic_radio)
|
181
|
+
mode_layout.addWidget(self.sfm_radio)
|
182
|
+
mode_layout.addWidget(self.rgb_radio)
|
183
|
+
main_layout.addLayout(mode_layout)
|
184
|
+
|
185
|
+
# Format and Options
|
186
|
+
options_layout = QFormLayout()
|
187
|
+
|
188
|
+
# General file format
|
189
|
+
self.file_format_combo = QComboBox()
|
190
|
+
self.file_format_combo.addItems([".png", ".bmp", ".tif"])
|
191
|
+
self.file_format_combo.currentTextChanged.connect(self.update_georef_availability)
|
192
|
+
options_layout.addRow("File Format:", self.file_format_combo)
|
111
193
|
|
112
|
-
|
113
|
-
"
|
194
|
+
# Georeferencing
|
195
|
+
self.preserve_georef_checkbox = QCheckBox("Preserve georeferencing (if available)")
|
196
|
+
self.preserve_georef_checkbox.setChecked(True)
|
197
|
+
options_layout.addRow(self.preserve_georef_checkbox)
|
198
|
+
self.georef_note = QLabel("Note: Georeferencing is only supported for TIF format.")
|
199
|
+
self.georef_note.setStyleSheet("color: #666; font-style: italic;")
|
200
|
+
options_layout.addRow(self.georef_note)
|
201
|
+
|
202
|
+
main_layout.addLayout(options_layout)
|
203
|
+
groupbox.setLayout(main_layout)
|
204
|
+
parent_layout.addWidget(groupbox)
|
205
|
+
self.update_georef_availability()
|
206
|
+
|
207
|
+
def setup_image_selection_layout(self, parent_layout=None):
|
114
208
|
group_box = QGroupBox("Apply To")
|
115
209
|
layout = QVBoxLayout()
|
116
|
-
|
117
210
|
self.apply_filtered_checkbox = QCheckBox("▼ Apply to filtered images")
|
118
211
|
self.apply_prev_checkbox = QCheckBox("↑ Apply to previous images")
|
119
212
|
self.apply_next_checkbox = QCheckBox("↓ Apply to next images")
|
@@ -130,24 +223,18 @@ class ExportMaskAnnotations(QDialog):
|
|
130
223
|
self.apply_group.addButton(self.apply_next_checkbox)
|
131
224
|
self.apply_group.addButton(self.apply_all_checkbox)
|
132
225
|
self.apply_group.setExclusive(True)
|
133
|
-
|
134
226
|
group_box.setLayout(layout)
|
135
|
-
|
227
|
+
parent_layout.addWidget(group_box)
|
136
228
|
|
137
|
-
def setup_annotation_layout(self):
|
138
|
-
"""Setup the annotation types layout."""
|
229
|
+
def setup_annotation_layout(self, parent_layout=None):
|
139
230
|
groupbox = QGroupBox("Annotations to Include")
|
140
231
|
layout = QVBoxLayout()
|
141
|
-
|
142
|
-
# Annotation types checkboxes
|
143
232
|
self.patch_checkbox = QCheckBox("Patch Annotations")
|
144
233
|
self.patch_checkbox.setChecked(True)
|
145
234
|
self.rectangle_checkbox = QCheckBox("Rectangle Annotations")
|
146
235
|
self.rectangle_checkbox.setChecked(True)
|
147
236
|
self.polygon_checkbox = QCheckBox("Polygon Annotations")
|
148
237
|
self.polygon_checkbox.setChecked(True)
|
149
|
-
|
150
|
-
# Include negative samples
|
151
238
|
self.include_negative_samples_checkbox = QCheckBox("Include negative samples")
|
152
239
|
self.include_negative_samples_checkbox.setChecked(True)
|
153
240
|
|
@@ -155,438 +242,486 @@ class ExportMaskAnnotations(QDialog):
|
|
155
242
|
layout.addWidget(self.rectangle_checkbox)
|
156
243
|
layout.addWidget(self.polygon_checkbox)
|
157
244
|
layout.addWidget(self.include_negative_samples_checkbox)
|
158
|
-
|
159
245
|
groupbox.setLayout(layout)
|
160
|
-
|
246
|
+
parent_layout.addWidget(groupbox)
|
161
247
|
|
162
|
-
def setup_label_layout(self):
|
163
|
-
"
|
164
|
-
groupbox = QGroupBox("Labels to Include")
|
248
|
+
def setup_label_layout(self, parent_layout=None):
|
249
|
+
groupbox = QGroupBox("Labels to Include / Rasterization Order")
|
165
250
|
layout = QVBoxLayout()
|
166
|
-
|
167
|
-
|
168
|
-
self.
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
self.
|
179
|
-
self.
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
"
|
192
|
-
|
193
|
-
layout
|
194
|
-
|
195
|
-
# File format combo box
|
196
|
-
self.file_format_combo = QComboBox()
|
197
|
-
self.file_format_combo.addItems([".png", ".bmp", ".tif"])
|
198
|
-
self.file_format_combo.setEditable(True)
|
199
|
-
layout.addRow("File Format:", self.file_format_combo)
|
200
|
-
|
201
|
-
# Add option to preserve georeferencing
|
202
|
-
self.preserve_georef_checkbox = QCheckBox("Preserve georeferencing (if available)")
|
203
|
-
self.preserve_georef_checkbox.setChecked(True)
|
204
|
-
layout.addRow("", self.preserve_georef_checkbox)
|
205
|
-
|
206
|
-
# Connect the checkbox to update based on file format
|
207
|
-
self.file_format_combo.currentTextChanged.connect(self.update_georef_availability)
|
208
|
-
|
209
|
-
# Add a note about georeferencing
|
210
|
-
self.georef_note = QLabel("Note: Georeferencing can only be preserved with TIF format")
|
211
|
-
self.georef_note.setStyleSheet("color: #666; font-style: italic;")
|
212
|
-
layout.addRow("", self.georef_note)
|
213
|
-
|
251
|
+
self.label_table = QTableWidget()
|
252
|
+
self.label_table.setColumnCount(3)
|
253
|
+
self.label_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
254
|
+
self.label_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
255
|
+
header = self.label_table.horizontalHeader()
|
256
|
+
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
257
|
+
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
258
|
+
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
259
|
+
layout.addWidget(self.label_table)
|
260
|
+
|
261
|
+
button_layout = QHBoxLayout()
|
262
|
+
button_layout.addStretch(1)
|
263
|
+
self.move_up_button = QPushButton("▲ Move Up")
|
264
|
+
self.move_down_button = QPushButton("▼ Move Down")
|
265
|
+
self.move_up_button.clicked.connect(self.move_row_up)
|
266
|
+
self.move_down_button.clicked.connect(self.move_row_down)
|
267
|
+
button_layout.addWidget(self.move_up_button)
|
268
|
+
button_layout.addWidget(self.move_down_button)
|
269
|
+
button_layout.addStretch(1)
|
270
|
+
layout.addLayout(button_layout)
|
271
|
+
|
272
|
+
order_note = QLabel(
|
273
|
+
"<b>Layer Order is Important:</b> Labels lower in the list will be drawn on top of labels "
|
274
|
+
"higher in the list. For overlapping annotations, only the topmost class will appear."
|
275
|
+
)
|
276
|
+
order_note.setStyleSheet("color: #666;")
|
277
|
+
order_note.setWordWrap(True)
|
278
|
+
layout.addWidget(order_note)
|
279
|
+
|
214
280
|
groupbox.setLayout(layout)
|
215
|
-
|
281
|
+
parent_layout.addWidget(groupbox)
|
216
282
|
|
217
|
-
|
218
|
-
self.update_georef_availability()
|
219
|
-
|
220
|
-
def setup_buttons_layout(self):
|
221
|
-
"""Setup the buttons layout."""
|
283
|
+
def setup_buttons_layout(self, parent_layout=None):
|
222
284
|
button_layout = QHBoxLayout()
|
223
|
-
|
285
|
+
button_layout.addStretch(1)
|
224
286
|
self.export_button = QPushButton("Export")
|
225
|
-
self.export_button.clicked.connect(self.
|
287
|
+
self.export_button.clicked.connect(self.run_export_process)
|
226
288
|
self.cancel_button = QPushButton("Cancel")
|
227
289
|
self.cancel_button.clicked.connect(self.reject)
|
228
|
-
|
229
290
|
button_layout.addWidget(self.export_button)
|
230
291
|
button_layout.addWidget(self.cancel_button)
|
231
|
-
|
232
|
-
|
233
|
-
|
292
|
+
parent_layout.addLayout(button_layout)
|
293
|
+
|
294
|
+
def update_ui_for_mode(self):
|
295
|
+
"""Update the UI dynamically based on the selected export mode."""
|
296
|
+
if self.semantic_radio.isChecked():
|
297
|
+
self.mask_mode = 'semantic'
|
298
|
+
elif self.sfm_radio.isChecked():
|
299
|
+
self.mask_mode = 'sfm'
|
300
|
+
elif self.rgb_radio.isChecked():
|
301
|
+
self.mask_mode = 'rgb'
|
302
|
+
|
303
|
+
self.populate_label_table()
|
304
|
+
|
305
|
+
def populate_label_table(self):
|
306
|
+
"""Populate the label table based on the current mode."""
|
307
|
+
self.label_table.blockSignals(True)
|
308
|
+
self.label_table.setRowCount(0)
|
309
|
+
|
310
|
+
# Set table headers based on mode
|
311
|
+
headers = ["Include", "Label Name"]
|
312
|
+
if self.mask_mode in ['semantic', 'sfm']:
|
313
|
+
headers.append("Mask Value")
|
314
|
+
elif self.mask_mode == 'rgb':
|
315
|
+
headers.append("Color Preview")
|
316
|
+
self.label_table.setHorizontalHeaderLabels(headers)
|
317
|
+
|
318
|
+
# --- BACKGROUND ROW (ROW 0) ---
|
319
|
+
self.label_table.insertRow(0)
|
320
|
+
checkbox_widget = self.create_centered_checkbox(checked=True)
|
321
|
+
self.label_table.setCellWidget(0, 0, checkbox_widget)
|
322
|
+
|
323
|
+
label_item = QTableWidgetItem("Background")
|
324
|
+
label_item.setFlags(label_item.flags() & ~Qt.ItemIsEditable)
|
325
|
+
label_item.setData(Qt.UserRole, "Background")
|
326
|
+
self.label_table.setItem(0, 1, label_item)
|
327
|
+
|
328
|
+
if self.mask_mode in ['semantic', 'sfm']:
|
329
|
+
spinbox = QSpinBox()
|
330
|
+
spinbox.setRange(0, 255)
|
331
|
+
spinbox.setValue(0)
|
332
|
+
self.label_table.setCellWidget(0, 2, spinbox)
|
333
|
+
elif self.mask_mode == 'rgb':
|
334
|
+
swatch = ClickableColorSwatchWidget(self.rgb_background_color)
|
335
|
+
swatch.clicked.connect(self.pick_background_color)
|
336
|
+
# Create a container widget to center the swatch
|
337
|
+
container_widget = QWidget()
|
338
|
+
layout = QHBoxLayout(container_widget)
|
339
|
+
layout.addWidget(swatch)
|
340
|
+
layout.setAlignment(Qt.AlignCenter)
|
341
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
342
|
+
self.label_table.setCellWidget(0, 2, container_widget)
|
343
|
+
|
344
|
+
# --- LABEL ROWS ---
|
345
|
+
for i, label in enumerate(self.label_window.labels):
|
346
|
+
row = i + 1
|
347
|
+
self.label_table.insertRow(row)
|
348
|
+
|
349
|
+
# Column 0: Include Checkbox
|
350
|
+
checkbox_widget = self.create_centered_checkbox(checked=True)
|
351
|
+
self.label_table.setCellWidget(row, 0, checkbox_widget)
|
352
|
+
|
353
|
+
# Column 1: Label Name
|
354
|
+
label_item = QTableWidgetItem(label.short_label_code)
|
355
|
+
label_item.setFlags(label_item.flags() & ~Qt.ItemIsEditable)
|
356
|
+
label_item.setData(Qt.UserRole, label.short_label_code)
|
357
|
+
self.label_table.setItem(row, 1, label_item)
|
358
|
+
|
359
|
+
# Column 2: Mode-dependent widget
|
360
|
+
if self.mask_mode == 'semantic':
|
361
|
+
spinbox = QSpinBox()
|
362
|
+
spinbox.setRange(0, 255)
|
363
|
+
spinbox.setValue(i + 1)
|
364
|
+
self.label_table.setCellWidget(row, 2, spinbox)
|
365
|
+
elif self.mask_mode == 'sfm':
|
366
|
+
spinbox = QSpinBox()
|
367
|
+
spinbox.setRange(0, 255)
|
368
|
+
spinbox.setValue(255) # Default foreground value
|
369
|
+
self.label_table.setCellWidget(row, 2, spinbox)
|
370
|
+
elif self.mask_mode == 'rgb':
|
371
|
+
try:
|
372
|
+
q_color = QColor(label.color)
|
373
|
+
except Exception:
|
374
|
+
q_color = QColor("#FFFFFF") # Default to white on error
|
375
|
+
|
376
|
+
swatch = ColorSwatchWidget(q_color)
|
377
|
+
cell_widget = QWidget()
|
378
|
+
layout = QHBoxLayout(cell_widget)
|
379
|
+
layout.addWidget(swatch)
|
380
|
+
layout.setAlignment(Qt.AlignCenter)
|
381
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
382
|
+
self.label_table.setCellWidget(row, 2, cell_widget)
|
383
|
+
|
384
|
+
if self.label_table.rowCount() > 0:
|
385
|
+
self.label_table.selectRow(0)
|
386
|
+
self.label_table.blockSignals(False)
|
387
|
+
|
388
|
+
def pick_background_color(self):
|
389
|
+
color = QColorDialog.getColor(self.rgb_background_color, self, "Select Background Color")
|
390
|
+
if color.isValid():
|
391
|
+
self.rgb_background_color = color
|
392
|
+
swatch_container = self.label_table.cellWidget(0, 2)
|
393
|
+
if swatch_container:
|
394
|
+
# Find the swatch inside the container
|
395
|
+
swatch = swatch_container.findChild(ClickableColorSwatchWidget)
|
396
|
+
if swatch:
|
397
|
+
swatch.setColor(color)
|
398
|
+
|
399
|
+
def create_centered_checkbox(self, checked=True):
|
400
|
+
checkbox = QCheckBox()
|
401
|
+
checkbox.setChecked(checked)
|
402
|
+
widget = QWidget()
|
403
|
+
layout = QHBoxLayout(widget)
|
404
|
+
layout.addWidget(checkbox)
|
405
|
+
layout.setAlignment(Qt.AlignCenter)
|
406
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
407
|
+
return widget
|
408
|
+
|
234
409
|
def browse_output_dir(self):
|
235
|
-
|
236
|
-
options = QFileDialog.Options()
|
237
|
-
directory = QFileDialog.getExistingDirectory(
|
238
|
-
self, "Select Output Directory", "", options=options
|
239
|
-
)
|
410
|
+
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
|
240
411
|
if directory:
|
241
412
|
self.output_dir_edit.setText(directory)
|
242
413
|
|
243
|
-
def update_label_selection_list(self):
|
244
|
-
"""Update the label selection list with labels from the label window."""
|
245
|
-
# Clear existing checkboxes
|
246
|
-
for checkbox in self.label_checkboxes:
|
247
|
-
self.label_layout.removeWidget(checkbox)
|
248
|
-
checkbox.deleteLater()
|
249
|
-
self.label_checkboxes = []
|
250
|
-
|
251
|
-
# Create a checkbox for each label
|
252
|
-
for label in self.label_window.labels:
|
253
|
-
checkbox = QCheckBox(label.short_label_code)
|
254
|
-
checkbox.setChecked(True) # Default to checked
|
255
|
-
checkbox.setProperty("label", label) # Store the label object
|
256
|
-
self.label_checkboxes.append(checkbox)
|
257
|
-
self.label_layout.addWidget(checkbox)
|
258
|
-
|
259
|
-
def update_georef_availability(self):
|
260
|
-
"""Update georeferencing checkbox availability based on file format"""
|
261
|
-
current_format = self.file_format_combo.currentText().lower()
|
262
|
-
is_tif = '.tif' in current_format
|
263
|
-
|
264
|
-
self.preserve_georef_checkbox.setEnabled(is_tif)
|
265
|
-
if not is_tif:
|
266
|
-
self.preserve_georef_checkbox.setChecked(False)
|
267
|
-
self.georef_note.setStyleSheet("color: red; font-style: italic;")
|
268
|
-
else:
|
269
|
-
self.georef_note.setStyleSheet("color: #666; font-style: italic;")
|
270
|
-
|
271
414
|
def get_selected_image_paths(self):
|
272
|
-
"""
|
273
|
-
Get the selected image paths based on the options.
|
274
|
-
|
275
|
-
:return: List of selected image paths
|
276
|
-
"""
|
277
|
-
# Current image path showing
|
278
415
|
current_image_path = self.annotation_window.current_image_path
|
279
416
|
if not current_image_path:
|
280
417
|
return []
|
281
418
|
|
282
|
-
# Determine which images to export annotations for
|
283
419
|
if self.apply_filtered_checkbox.isChecked():
|
284
420
|
return self.image_window.table_model.filtered_paths
|
285
421
|
elif self.apply_prev_checkbox.isChecked():
|
286
422
|
if current_image_path in self.image_window.table_model.filtered_paths:
|
287
423
|
current_index = self.image_window.table_model.get_row_for_path(current_image_path)
|
288
424
|
return self.image_window.table_model.filtered_paths[:current_index + 1]
|
289
|
-
else:
|
290
|
-
return [current_image_path]
|
291
425
|
elif self.apply_next_checkbox.isChecked():
|
292
426
|
if current_image_path in self.image_window.table_model.filtered_paths:
|
293
427
|
current_index = self.image_window.table_model.get_row_for_path(current_image_path)
|
294
428
|
return self.image_window.table_model.filtered_paths[current_index:]
|
295
|
-
else:
|
296
|
-
return [current_image_path]
|
297
429
|
elif self.apply_all_checkbox.isChecked():
|
298
430
|
return self.image_window.raster_manager.image_paths
|
299
|
-
|
300
|
-
|
301
|
-
return [current_image_path]
|
431
|
+
|
432
|
+
return [current_image_path]
|
302
433
|
|
303
|
-
def
|
304
|
-
"""Export the class mapping to a JSON file."""
|
305
|
-
mapping_file = os.path.join(output_path, "class_mapping.json")
|
306
|
-
|
307
|
-
with open(mapping_file, 'w') as f:
|
308
|
-
json.dump(self.class_mapping, f, indent=4)
|
309
|
-
|
310
|
-
if not os.path.exists(mapping_file):
|
311
|
-
print(f"Warning: Failed to save class mapping to {mapping_file}")
|
312
|
-
|
313
|
-
def export_masks(self):
|
314
|
-
"""Export segmentation masks based on selected annotations and labels."""
|
315
|
-
# Validate inputs
|
434
|
+
def validate_inputs(self):
|
316
435
|
if not self.output_dir_edit.text():
|
317
|
-
QMessageBox.warning(self,
|
318
|
-
"Missing
|
436
|
+
QMessageBox.warning(self,
|
437
|
+
"Missing Input",
|
319
438
|
"Please select an output directory.")
|
320
|
-
return
|
321
|
-
|
322
|
-
|
323
|
-
if not any([self.patch_checkbox.isChecked(),
|
324
|
-
self.rectangle_checkbox.isChecked(),
|
439
|
+
return False
|
440
|
+
if not any([self.patch_checkbox.isChecked(),
|
441
|
+
self.rectangle_checkbox.isChecked(),
|
325
442
|
self.polygon_checkbox.isChecked()]):
|
326
|
-
QMessageBox.warning(self,
|
327
|
-
"
|
443
|
+
QMessageBox.warning(self,
|
444
|
+
"Missing Input",
|
328
445
|
"Please select at least one annotation type.")
|
446
|
+
return False
|
447
|
+
return True
|
448
|
+
|
449
|
+
def run_export_process(self):
|
450
|
+
if not self.validate_inputs():
|
329
451
|
return
|
330
452
|
|
331
|
-
|
332
|
-
self.
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
453
|
+
self.labels_to_render = []
|
454
|
+
self.background_value = 0
|
455
|
+
|
456
|
+
# --- Collect data from UI based on mode ---
|
457
|
+
if self.mask_mode in ['semantic', 'sfm']:
|
458
|
+
used_mask_values = {}
|
459
|
+
for i in range(self.label_table.rowCount()):
|
460
|
+
if not self.label_table.cellWidget(i, 0).findChild(QCheckBox).isChecked():
|
461
|
+
continue
|
462
|
+
|
463
|
+
label_code = self.label_table.item(i, 1).data(Qt.UserRole)
|
464
|
+
mask_value = self.label_table.cellWidget(i, 2).value()
|
465
|
+
|
466
|
+
if mask_value not in used_mask_values:
|
467
|
+
used_mask_values[mask_value] = []
|
468
|
+
used_mask_values[mask_value].append(label_code)
|
469
|
+
|
470
|
+
if label_code == "Background":
|
471
|
+
self.background_value = mask_value
|
472
|
+
else:
|
473
|
+
label = next((l for l in self.label_window.labels if l.short_label_code == label_code), None)
|
474
|
+
if label:
|
475
|
+
self.labels_to_render.append((label, mask_value))
|
476
|
+
|
477
|
+
# Check for duplicate values
|
478
|
+
duplicate_values = {v: l for v, l in used_mask_values.items() if len(l) > 1}
|
479
|
+
if duplicate_values:
|
480
|
+
msg = "Warning: The following mask values are used by multiple labels:\n" + \
|
481
|
+
"\n".join([f"Value {v}: {', '.join(l)}" for v, l in duplicate_values.items()]) + \
|
482
|
+
"\nThis may cause unexpected behavior. Continue?"
|
483
|
+
if QMessageBox.warning(self,
|
484
|
+
"Duplicate Values",
|
485
|
+
msg,
|
486
|
+
QMessageBox.Yes | QMessageBox.No) == QMessageBox.No:
|
487
|
+
return
|
488
|
+
|
489
|
+
elif self.mask_mode == 'rgb':
|
490
|
+
self.background_value = self.rgb_background_color.getRgb()[:3] # (R, G, B) tuple
|
491
|
+
for i in range(1, self.label_table.rowCount()): # Skip background
|
492
|
+
if self.label_table.cellWidget(i, 0).findChild(QCheckBox).isChecked():
|
493
|
+
label_code = self.label_table.item(i, 1).data(Qt.UserRole)
|
494
|
+
label = next((l for l in self.label_window.labels if l.short_label_code == label_code), None)
|
495
|
+
if label:
|
496
|
+
try:
|
497
|
+
# Check if label.color is already a QColor object
|
498
|
+
if isinstance(label.color, QColor):
|
499
|
+
color_tuple = label.color.getRgb()[:3] # Extract (R,G,B) and ignore alpha
|
500
|
+
else:
|
501
|
+
# Otherwise, assume it's a string (hex code) and convert it
|
502
|
+
color_tuple = ImageColor.getrgb(label.color)
|
503
|
+
self.labels_to_render.append((label, color_tuple))
|
504
|
+
except (ValueError, TypeError) as e:
|
505
|
+
print(f"Warning: Invalid color format for label "
|
506
|
+
f"'{label.short_label_code}': {label.color}. Error: {e}. Skipping.")
|
507
|
+
|
508
|
+
# --- Check if any labels are selected to be drawn ---
|
509
|
+
if not self.labels_to_render:
|
510
|
+
QMessageBox.warning(self, "No Labels Selected", "Please select at least one label to include in the masks.")
|
342
511
|
return
|
343
512
|
|
513
|
+
# --- Setup paths and progress bar ---
|
344
514
|
output_dir = self.output_dir_edit.text()
|
345
515
|
folder_name = self.output_name_edit.text().strip()
|
346
|
-
file_format = self.file_format_combo.currentText()
|
347
|
-
|
348
|
-
|
349
|
-
if not file_format.startswith('.'):
|
350
|
-
file_format = '.' + file_format
|
516
|
+
self.file_format = self.file_format_combo.currentText()
|
517
|
+
if not self.file_format.startswith('.'):
|
518
|
+
self.file_format = '.' + self.file_format
|
351
519
|
|
352
|
-
# Create output directory
|
353
520
|
output_path = os.path.join(output_dir, folder_name)
|
354
|
-
|
355
|
-
|
356
|
-
except Exception as e:
|
357
|
-
QMessageBox.critical(self,
|
358
|
-
"Error Creating Directory",
|
359
|
-
f"Failed to create output directory: {str(e)}")
|
360
|
-
return
|
361
|
-
|
362
|
-
# Get the list of images to process
|
521
|
+
os.makedirs(output_path, exist_ok=True)
|
522
|
+
|
363
523
|
images = self.get_selected_image_paths()
|
364
524
|
if not images:
|
365
|
-
QMessageBox.warning(self,
|
366
|
-
"No Images",
|
367
|
-
"No images found in the project.")
|
525
|
+
QMessageBox.warning(self, "No Images", "No images found for processing.")
|
368
526
|
return
|
369
|
-
|
370
|
-
# Collect annotation types to include
|
527
|
+
|
371
528
|
self.annotation_types = []
|
372
|
-
if self.patch_checkbox.isChecked():
|
529
|
+
if self.patch_checkbox.isChecked():
|
373
530
|
self.annotation_types.append(PatchAnnotation)
|
374
|
-
if self.rectangle_checkbox.isChecked():
|
531
|
+
if self.rectangle_checkbox.isChecked():
|
375
532
|
self.annotation_types.append(RectangleAnnotation)
|
376
533
|
if self.polygon_checkbox.isChecked():
|
377
534
|
self.annotation_types.append(PolygonAnnotation)
|
378
535
|
self.annotation_types.append(MultiPolygonAnnotation)
|
379
536
|
|
380
|
-
#
|
381
|
-
self.class_mapping = {}
|
382
|
-
|
383
|
-
for i, label in enumerate(self.selected_labels):
|
384
|
-
# Leave 0 for background
|
385
|
-
self.class_mapping[label.short_label_code] = {
|
386
|
-
"label": label.to_dict(),
|
387
|
-
"index": i + 1
|
388
|
-
}
|
389
|
-
|
390
|
-
# Make the cursor busy
|
537
|
+
# --- Run Export Loop ---
|
391
538
|
QApplication.setOverrideCursor(Qt.WaitCursor)
|
392
|
-
progress_bar = ProgressBar(self.annotation_window, "Exporting
|
539
|
+
progress_bar = ProgressBar(self.annotation_window, "Exporting Masks")
|
393
540
|
progress_bar.show()
|
394
541
|
progress_bar.start_progress(len(images))
|
395
542
|
|
396
543
|
try:
|
397
544
|
for image_path in images:
|
398
|
-
|
399
|
-
self.create_mask_for_image(image_path, output_path, file_format)
|
545
|
+
self.create_mask_for_image(image_path, output_path)
|
400
546
|
progress_bar.update_progress()
|
401
547
|
|
402
|
-
|
403
|
-
self.
|
404
|
-
|
405
|
-
QMessageBox.information(self,
|
406
|
-
"Export Complete",
|
407
|
-
"Segmentation masks have been successfully exported")
|
548
|
+
self.export_metadata(output_path)
|
549
|
+
QMessageBox.information(self, "Export Complete", "Masks exported successfully.")
|
408
550
|
self.accept()
|
409
|
-
|
410
551
|
except Exception as e:
|
411
|
-
QMessageBox.critical(self,
|
412
|
-
"Error Exporting Masks",
|
413
|
-
f"An error occurred: {str(e)}")
|
414
|
-
|
552
|
+
QMessageBox.critical(self, "Error", f"An error occurred during export: {e}")
|
415
553
|
finally:
|
416
|
-
# Make cursor normal again
|
417
554
|
QApplication.restoreOverrideCursor()
|
418
|
-
progress_bar.finish_progress()
|
419
|
-
progress_bar.stop_progress()
|
420
555
|
progress_bar.close()
|
421
556
|
|
422
|
-
def
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
# Get all annotations for this image
|
428
|
-
annotations = []
|
429
|
-
|
430
|
-
for annotation in self.annotation_window.get_image_annotations(image_path):
|
431
|
-
# Check that the annotation is of the correct type
|
432
|
-
if not isinstance(annotation, tuple(self.annotation_types)):
|
433
|
-
continue
|
434
|
-
|
435
|
-
# Check that the annotation's label is in the selected labels
|
436
|
-
if annotation.label.short_label_code not in selected_labels:
|
437
|
-
continue
|
438
|
-
|
439
|
-
# Add the annotation to the list based on its type, if selected
|
440
|
-
if self.patch_checkbox.isChecked() and isinstance(annotation, PatchAnnotation):
|
441
|
-
annotations.append(annotation)
|
442
|
-
|
443
|
-
elif self.rectangle_checkbox.isChecked() and isinstance(annotation, RectangleAnnotation):
|
444
|
-
annotations.append(annotation)
|
557
|
+
def create_mask_for_image(self, image_path, output_path):
|
558
|
+
height, width, has_georef, transform, crs = self.get_image_metadata(image_path, self.file_format)
|
559
|
+
if not height or not width:
|
560
|
+
print(f"Skipping {image_path}: could not determine dimensions.")
|
561
|
+
return
|
445
562
|
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
563
|
+
# Initialize mask based on mode
|
564
|
+
if self.mask_mode == 'rgb':
|
565
|
+
mask = np.full((height, width, 3), self.background_value, dtype=np.uint8)
|
566
|
+
else: # semantic or sfm
|
567
|
+
mask = np.full((height, width), self.background_value, dtype=np.uint8)
|
568
|
+
|
569
|
+
has_annotations = False
|
570
|
+
|
571
|
+
# Draw annotations based on mode
|
572
|
+
if self.mask_mode in ['semantic', 'sfm']:
|
573
|
+
for label, value in self.labels_to_render:
|
574
|
+
annotations = self.get_annotations_for_image(image_path, label)
|
575
|
+
if annotations:
|
576
|
+
has_annotations = True
|
577
|
+
self.draw_annotations_on_mask(mask, annotations, value)
|
578
|
+
elif self.mask_mode == 'rgb':
|
579
|
+
for label, color in self.labels_to_render:
|
580
|
+
annotations = self.get_annotations_for_image(image_path, label)
|
581
|
+
if annotations:
|
582
|
+
has_annotations = True
|
583
|
+
self.draw_annotations_on_mask(mask, annotations, color)
|
584
|
+
|
585
|
+
if not has_annotations and not self.include_negative_samples_checkbox.isChecked():
|
586
|
+
return
|
452
587
|
|
588
|
+
# Always save as PNG
|
589
|
+
filename = f"{os.path.splitext(os.path.basename(image_path))[0]}.png"
|
590
|
+
mask_path = os.path.join(output_path, filename)
|
591
|
+
|
592
|
+
if self.mask_mode == 'rgb':
|
593
|
+
# OpenCV expects BGR, so convert from RGB
|
594
|
+
mask = cv2.cvtColor(mask, cv2.COLOR_RGB2BGR)
|
595
|
+
cv2.imwrite(mask_path, mask)
|
596
|
+
|
597
|
+
def export_metadata(self, output_path):
|
598
|
+
if self.mask_mode == 'semantic':
|
599
|
+
class_mapping = {}
|
600
|
+
if self.label_table.cellWidget(0, 0).findChild(QCheckBox).isChecked():
|
601
|
+
background_label = "Background"
|
602
|
+
background_index = self.label_table.cellWidget(0, 2).value()
|
603
|
+
class_mapping[background_label] = {
|
604
|
+
"label": background_label,
|
605
|
+
"index": background_index
|
606
|
+
}
|
607
|
+
|
608
|
+
for label, value in self.labels_to_render:
|
609
|
+
class_mapping[label.short_label_code] = {"label": label.to_dict(), "index": value}
|
610
|
+
|
611
|
+
with open(os.path.join(output_path, "class_mapping.json"), 'w') as f:
|
612
|
+
json.dump(class_mapping, f, indent=4)
|
613
|
+
|
614
|
+
elif self.mask_mode == 'rgb':
|
615
|
+
color_legend = {}
|
616
|
+
if self.label_table.cellWidget(0, 0).findChild(QCheckBox).isChecked():
|
617
|
+
color_legend["Background"] = self.background_value
|
618
|
+
|
619
|
+
for label, color in self.labels_to_render:
|
620
|
+
color_legend[label.short_label_code] = color
|
621
|
+
|
622
|
+
with open(os.path.join(output_path, "color_legend.json"), 'w') as f:
|
623
|
+
json.dump(color_legend, f, indent=4)
|
624
|
+
|
625
|
+
# No metadata file needed for SfM mode
|
626
|
+
|
627
|
+
def get_annotations_for_image(self, image_path, label):
|
628
|
+
annotations = []
|
629
|
+
for ann in self.annotation_window.get_image_annotations(image_path):
|
630
|
+
if ann.label.short_label_code == label.short_label_code and isinstance(ann, tuple(self.annotation_types)):
|
631
|
+
if isinstance(ann, MultiPolygonAnnotation):
|
632
|
+
annotations.extend(ann.polygons)
|
633
|
+
else:
|
634
|
+
annotations.append(ann)
|
453
635
|
return annotations
|
454
636
|
|
637
|
+
def draw_annotations_on_mask(self, mask, annotations, value):
|
638
|
+
for ann in annotations:
|
639
|
+
if isinstance(ann, (PatchAnnotation, RectangleAnnotation)):
|
640
|
+
p1 = (int(ann.top_left.x()), int(ann.top_left.y()))
|
641
|
+
p2 = (int(ann.bottom_right.x()), int(ann.bottom_right.y()))
|
642
|
+
cv2.rectangle(mask, p1, p2, value, -1)
|
643
|
+
elif isinstance(ann, PolygonAnnotation):
|
644
|
+
points = np.array([[p.x(), p.y()] for p in ann.points], dtype=np.int32)
|
645
|
+
cv2.fillPoly(mask, [points], value)
|
646
|
+
|
455
647
|
def get_image_metadata(self, image_path, file_format):
|
456
|
-
|
457
|
-
|
458
|
-
transform = None
|
459
|
-
crs = None
|
460
|
-
has_georef = False
|
461
|
-
height = None
|
462
|
-
width = None
|
463
|
-
|
464
|
-
# Get the raster from the raster manager
|
648
|
+
transform, crs, has_georef = None, None, False
|
649
|
+
width, height = None, None
|
465
650
|
raster = self.image_window.raster_manager.get_raster(image_path)
|
466
|
-
|
467
|
-
# Only check for georeferencing if using TIF format and checkbox is checked
|
468
|
-
can_preserve_georef = self.preserve_georef_checkbox.isChecked() and file_format.lower() == '.tif'
|
651
|
+
can_preserve = self.preserve_georef_checkbox.isChecked() and file_format.lower() == '.tif'
|
469
652
|
|
470
653
|
if raster and raster.rasterio_src:
|
471
|
-
|
472
|
-
|
473
|
-
height = raster.height
|
474
|
-
|
475
|
-
# Check for georeferencing if needed
|
476
|
-
if can_preserve_georef and hasattr(raster.rasterio_src, 'transform'):
|
654
|
+
width, height = raster.width, raster.height
|
655
|
+
if can_preserve and hasattr(raster.rasterio_src, 'transform'):
|
477
656
|
transform = raster.rasterio_src.transform
|
478
657
|
if transform and not transform.is_identity:
|
479
658
|
crs = raster.rasterio_src.crs
|
480
659
|
has_georef = True
|
481
660
|
else:
|
482
|
-
# Fallback to direct file access if raster is not available
|
483
661
|
try:
|
484
|
-
if
|
662
|
+
if can_preserve:
|
485
663
|
with rasterio.open(image_path) as src:
|
486
|
-
if src.transform and not src.transform.is_identity:
|
487
|
-
transform = src.transform
|
488
|
-
crs = src.crs
|
489
|
-
has_georef = True
|
490
664
|
width, height = src.width, src.height
|
665
|
+
if src.transform and not src.transform.is_identity:
|
666
|
+
transform, crs, has_georef = src.transform, src.crs, True
|
491
667
|
else:
|
492
|
-
|
493
|
-
|
494
|
-
width, height = image.size
|
668
|
+
with Image.open(image_path) as img:
|
669
|
+
width, height = img.size
|
495
670
|
except Exception as e:
|
496
|
-
print(f"Error
|
497
|
-
|
671
|
+
print(f"Error reading metadata for {image_path}: {e}")
|
498
672
|
return height, width, has_georef, transform, crs
|
499
673
|
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
cv2.fillPoly(mask, [points], label_index)
|
537
|
-
|
538
|
-
return mask
|
539
|
-
|
540
|
-
def create_mask_for_image(self, image_path, output_path, file_format):
|
541
|
-
"""Create a segmentation mask for a single image"""
|
542
|
-
# Get the annotations for this image
|
543
|
-
annotations = self.get_annotations_for_image(image_path)
|
544
|
-
|
545
|
-
if not annotations and not self.include_negative_samples_checkbox.isChecked():
|
546
|
-
return # Skip images with no annotations if the user doesn't want negative samples
|
547
|
-
|
548
|
-
# Get the image dimensions, georeferencing
|
549
|
-
height, width, has_georef, transform, crs = self.get_image_metadata(image_path, file_format)
|
674
|
+
# --- Row Movement and UI Helpers ---
|
675
|
+
def move_row_up(self):
|
676
|
+
current_row = self.label_table.currentRow()
|
677
|
+
if current_row > 0:
|
678
|
+
self.swap_rows(current_row, current_row - 1)
|
679
|
+
self.label_table.selectRow(current_row - 1)
|
680
|
+
|
681
|
+
def move_row_down(self):
|
682
|
+
current_row = self.label_table.currentRow()
|
683
|
+
if 0 <= current_row < self.label_table.rowCount() - 1:
|
684
|
+
self.swap_rows(current_row, current_row + 1)
|
685
|
+
self.label_table.selectRow(current_row + 1)
|
686
|
+
|
687
|
+
def swap_rows(self, row1, row2):
|
688
|
+
# Because widgets are complex to swap, we just repopulate the table
|
689
|
+
# while preserving the core data (label order). This is simpler and more robust.
|
690
|
+
|
691
|
+
# Step 1: Extract the core data and checkbox state from the table
|
692
|
+
table_data = []
|
693
|
+
for r in range(self.label_table.rowCount()):
|
694
|
+
is_checked = self.label_table.cellWidget(r, 0).findChild(QCheckBox).isChecked()
|
695
|
+
label_code = self.label_table.item(r, 1).data(Qt.UserRole)
|
696
|
+
table_data.append({'code': label_code, 'checked': is_checked})
|
697
|
+
|
698
|
+
# Step 2: Swap the data for the two rows
|
699
|
+
table_data[row1], table_data[row2] = table_data[row2], table_data[row1]
|
700
|
+
|
701
|
+
# Step 3: Map old label order to new order
|
702
|
+
new_label_order = [data['code'] for data in table_data if data['code'] != 'Background']
|
703
|
+
|
704
|
+
def label_sort_key(x):
|
705
|
+
if x.short_label_code in new_label_order:
|
706
|
+
return new_label_order.index(x.short_label_code)
|
707
|
+
else:
|
708
|
+
return float('inf')
|
709
|
+
self.label_window.labels.sort(key=label_sort_key)
|
550
710
|
|
551
|
-
|
552
|
-
|
553
|
-
|
711
|
+
# Step 4: Repopulate and restore checkbox states
|
712
|
+
self.populate_label_table()
|
713
|
+
for r, data in enumerate(table_data):
|
714
|
+
self.label_table.cellWidget(r, 0).findChild(QCheckBox).setChecked(data['checked'])
|
554
715
|
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
# Save the mask
|
562
|
-
filename = os.path.basename(image_path)
|
563
|
-
name_without_ext = os.path.splitext(filename)[0]
|
564
|
-
mask_filename = f"{name_without_ext}{file_format}"
|
565
|
-
mask_path = os.path.join(output_path, mask_filename)
|
566
|
-
|
567
|
-
# If we have georeferencing and we need to preserve it, use rasterio to save
|
568
|
-
if has_georef and file_format.lower() == '.tif':
|
569
|
-
with rasterio.open(
|
570
|
-
mask_path,
|
571
|
-
'w',
|
572
|
-
driver='GTiff',
|
573
|
-
height=height,
|
574
|
-
width=width,
|
575
|
-
count=1,
|
576
|
-
dtype=mask.dtype,
|
577
|
-
crs=crs,
|
578
|
-
transform=transform,
|
579
|
-
compress='lzw',
|
580
|
-
) as dst:
|
581
|
-
dst.write(mask, 1)
|
716
|
+
def update_georef_availability(self):
|
717
|
+
is_tif = '.tif' in self.file_format_combo.currentText().lower()
|
718
|
+
self.preserve_georef_checkbox.setEnabled(is_tif)
|
719
|
+
if not is_tif:
|
720
|
+
self.preserve_georef_checkbox.setChecked(False)
|
721
|
+
self.georef_note.setStyleSheet("color: red; font-style: italic;")
|
582
722
|
else:
|
583
|
-
#
|
584
|
-
cv2.imwrite(mask_path, mask)
|
585
|
-
|
586
|
-
if not os.path.exists(mask_path):
|
587
|
-
print(f"Warning: Failed to save mask to {mask_path}")
|
723
|
+
self.georef_note.setStyleSheet("color: #666; font-style: italic;")
|
588
724
|
|
589
725
|
def closeEvent(self, event):
|
590
|
-
"""Handle the close event."""
|
591
|
-
# Clean up any resources if needed
|
592
726
|
super().closeEvent(event)
|
727
|
+
|