coralnet-toolbox 0.0.74__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.
Files changed (49) hide show
  1. coralnet_toolbox/Annotations/QtPolygonAnnotation.py +57 -12
  2. coralnet_toolbox/Annotations/QtRectangleAnnotation.py +44 -14
  3. coralnet_toolbox/Explorer/QtDataItem.py +52 -22
  4. coralnet_toolbox/Explorer/QtExplorer.py +277 -1600
  5. coralnet_toolbox/Explorer/QtSettingsWidgets.py +101 -15
  6. coralnet_toolbox/Explorer/QtViewers.py +1568 -0
  7. coralnet_toolbox/Explorer/transformer_models.py +70 -0
  8. coralnet_toolbox/Explorer/yolo_models.py +112 -0
  9. coralnet_toolbox/IO/QtExportMaskAnnotations.py +538 -403
  10. coralnet_toolbox/Icons/system_monitor.png +0 -0
  11. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +239 -147
  12. coralnet_toolbox/MachineLearning/VideoInference/YOLO3D/run.py +102 -16
  13. coralnet_toolbox/QtAnnotationWindow.py +16 -10
  14. coralnet_toolbox/QtEventFilter.py +4 -4
  15. coralnet_toolbox/QtImageWindow.py +3 -7
  16. coralnet_toolbox/QtMainWindow.py +104 -64
  17. coralnet_toolbox/QtProgressBar.py +1 -0
  18. coralnet_toolbox/QtSystemMonitor.py +370 -0
  19. coralnet_toolbox/Rasters/RasterTableModel.py +20 -0
  20. coralnet_toolbox/Results/ConvertResults.py +14 -8
  21. coralnet_toolbox/Results/ResultsProcessor.py +3 -2
  22. coralnet_toolbox/SAM/QtDeployGenerator.py +2 -5
  23. coralnet_toolbox/SAM/QtDeployPredictor.py +11 -3
  24. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +146 -116
  25. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +55 -9
  26. coralnet_toolbox/Tile/QtTileBatchInference.py +4 -4
  27. coralnet_toolbox/Tools/QtPolygonTool.py +42 -3
  28. coralnet_toolbox/Tools/QtRectangleTool.py +30 -0
  29. coralnet_toolbox/Tools/QtSAMTool.py +140 -91
  30. coralnet_toolbox/Transformers/Models/GroundingDINO.py +72 -0
  31. coralnet_toolbox/Transformers/Models/OWLViT.py +72 -0
  32. coralnet_toolbox/Transformers/Models/OmDetTurbo.py +68 -0
  33. coralnet_toolbox/Transformers/Models/QtBase.py +120 -0
  34. coralnet_toolbox/{AutoDistill → Transformers}/Models/__init__.py +1 -1
  35. coralnet_toolbox/{AutoDistill → Transformers}/QtBatchInference.py +15 -15
  36. coralnet_toolbox/{AutoDistill → Transformers}/QtDeployModel.py +18 -16
  37. coralnet_toolbox/{AutoDistill → Transformers}/__init__.py +1 -1
  38. coralnet_toolbox/__init__.py +1 -1
  39. coralnet_toolbox/utilities.py +21 -15
  40. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.76.dist-info}/METADATA +13 -10
  41. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.76.dist-info}/RECORD +45 -40
  42. coralnet_toolbox/AutoDistill/Models/GroundingDINO.py +0 -81
  43. coralnet_toolbox/AutoDistill/Models/OWLViT.py +0 -76
  44. coralnet_toolbox/AutoDistill/Models/OmDetTurbo.py +0 -75
  45. coralnet_toolbox/AutoDistill/Models/QtBase.py +0 -112
  46. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.76.dist-info}/WHEEL +0 -0
  47. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.76.dist-info}/entry_points.txt +0 -0
  48. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.76.dist-info}/licenses/LICENSE.txt +0 -0
  49. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.76.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,8 @@ import ujson as json
10
10
  from PyQt5.QtCore import Qt, QPointF, QObject, QThread, pyqtSignal
11
11
  from PyQt5.QtWidgets import (QFileDialog, QApplication, QMessageBox, QVBoxLayout, QGroupBox,
12
12
  QLabel, QLineEdit, QDialog, QPushButton, QDialogButtonBox,
13
- QGridLayout)
13
+ QGridLayout, QScrollArea, QFrame, QCheckBox, QRadioButton,
14
+ QToolButton)
14
15
 
15
16
  from coralnet_toolbox.Annotations.QtPolygonAnnotation import PolygonAnnotation
16
17
  from coralnet_toolbox.Annotations.QtRectangleAnnotation import RectangleAnnotation
@@ -39,35 +40,43 @@ class DatasetProcessor(QObject):
39
40
  error = pyqtSignal(str)
40
41
  finished = pyqtSignal()
41
42
 
42
- def __init__(self, yaml_path, output_folder, task, import_as, rename_on_conflict=False, parent=None):
43
+ def __init__(self, yaml_path, output_folder, task, import_as, rename_on_conflict=False,
44
+ excluded_classes=None, image_import_policy='annotated_only', parent=None):
43
45
  super().__init__(parent)
44
46
  self.yaml_path = yaml_path
45
47
  self.output_folder = output_folder
46
48
  self.task = task # 'detect' or 'segment' (source format)
47
49
  self.import_as = import_as # 'rectangle' or 'polygon' (target format)
48
50
  self.rename_on_conflict = rename_on_conflict
51
+ self.excluded_classes = excluded_classes if excluded_classes is not None else set()
52
+ self.image_import_policy = image_import_policy
49
53
  self.is_running = True
50
54
  self.parsing_errors = [] # To collect errors instead of printing
51
55
 
52
56
  def stop(self):
53
57
  self.is_running = False
54
58
 
59
+ # In class DatasetProcessor, update this method:
60
+
55
61
  def run(self):
56
62
  """Main processing method executed in the thread."""
57
63
  try:
58
64
  # Step 1: Read YAML and discover files
59
- self.status_changed.emit("Discovering and copying files...", 0)
60
65
  with open(self.yaml_path, 'r') as file:
61
66
  data = yaml.safe_load(file)
62
67
  class_names = data.get('names', [])
63
68
 
64
- # Step 2: Find and copy image/label pairs to output folder
65
- image_label_paths = self._find_and_copy_files(self.output_folder)
66
- if not image_label_paths:
69
+ source_image_label_map = self._find_source_files()
70
+
71
+ if not source_image_label_map:
67
72
  self.error.emit("No valid image/label pairs found in the dataset.")
68
73
  self.finished.emit()
69
74
  return
70
75
 
76
+ # --- Step 2: Copy files with progress reporting ---
77
+ self.status_changed.emit("Copying image files...", len(source_image_label_map))
78
+ image_label_paths = self._copy_files_with_progress(source_image_label_map)
79
+
71
80
  if not self.is_running:
72
81
  self.finished.emit()
73
82
  return
@@ -80,57 +89,67 @@ class DatasetProcessor(QObject):
80
89
  self.finished.emit()
81
90
  return
82
91
 
83
- # Step 4: Emit results for GUI to consume
92
+ # Step 4 (REMOVED): The JSON export is no longer done here.
93
+
94
+ # Step 5: Emit results for GUI to consume
84
95
  image_paths = list(image_label_paths.keys())
85
96
  self.processing_complete.emit(raw_annotations, image_paths, self.parsing_errors)
86
97
 
87
98
  except Exception as e:
88
- # Catch-all for any error during processing
89
99
  self.error.emit(f"An error occurred during processing: {str(e)}")
90
-
91
100
  finally:
92
- # Always emit finished signal
93
101
  self.finished.emit()
94
102
 
95
- def _find_and_copy_files(self, output_folder):
96
- """
97
- Finds, copies, and optionally renames image files.
98
- Returns a mapping of output image paths to original label paths.
99
- """
100
- img_out_dir = os.path.join(output_folder, "images")
101
- os.makedirs(img_out_dir, exist_ok=True)
102
-
103
+ def _find_source_files(self):
104
+ """Finds all source image and label paths based on the import policy."""
103
105
  dir_path = os.path.dirname(self.yaml_path)
104
- # Find all images and labels recursively
105
106
  image_paths = glob.glob(f"{dir_path}/**/images/*.*", recursive=True)
106
107
  label_paths = glob.glob(f"{dir_path}/**/labels/*.txt", recursive=True)
107
- # Map from base name (no extension) to image path
108
- image_basenames_map = {os.path.splitext(os.path.basename(p))[0]: p for p in image_paths}
109
108
 
110
- image_label_map = {}
111
- for label_path in label_paths:
109
+ source_map = {}
110
+ if self.image_import_policy == 'all':
111
+ # Policy: Find all images, and match labels to them if they exist
112
+ label_basenames_map = {os.path.splitext(os.path.basename(p))[0]: p for p in label_paths}
113
+ for img_path in image_paths:
114
+ img_basename = os.path.splitext(os.path.basename(img_path))[0]
115
+ label_path = label_basenames_map.get(img_basename, None) # Label can be None
116
+ source_map[img_path] = label_path
117
+ else:
118
+ # Policy: Only find images that have a corresponding label file
119
+ image_basenames_map = {os.path.splitext(os.path.basename(p))[0]: p for p in image_paths}
120
+ for label_path in label_paths:
121
+ label_basename_no_ext = os.path.splitext(os.path.basename(label_path))[0]
122
+ if label_basename_no_ext in image_basenames_map:
123
+ src_image_path = image_basenames_map[label_basename_no_ext]
124
+ source_map[src_image_path] = label_path
125
+ return source_map
126
+
127
+ def _copy_files_with_progress(self, source_image_label_map):
128
+ """Copies files and reports progress for each file."""
129
+ img_out_dir = os.path.join(self.output_folder, "images")
130
+ os.makedirs(img_out_dir, exist_ok=True)
131
+
132
+ dest_label_map = {}
133
+ for i, (src_image_path, label_path) in enumerate(source_image_label_map.items()):
112
134
  if not self.is_running:
113
135
  break
114
- label_basename_no_ext = os.path.splitext(
115
- os.path.basename(label_path))[0]
116
- if label_basename_no_ext in image_basenames_map:
117
- src_image_path = image_basenames_map[label_basename_no_ext]
118
- original_img_basename = os.path.basename(src_image_path)
119
-
120
- # Optionally rename to avoid conflicts
121
- if self.rename_on_conflict:
122
- base, ext = os.path.splitext(original_img_basename)
123
- unique_id = str(uuid.uuid4())[:8]
124
- new_img_basename = f"{base}_{unique_id}{ext}"
125
- else:
126
- new_img_basename = original_img_basename
127
-
128
- dest_image_path = os.path.join(img_out_dir, new_img_basename)
129
- shutil.copy(src_image_path, dest_image_path)
130
- # Store mapping with normalized (forward slash) paths - output image path to original label path
131
- image_label_map[dest_image_path.replace("\\", "/")] = label_path.replace("\\", "/")
132
-
133
- return image_label_map
136
+
137
+ original_img_basename = os.path.basename(src_image_path)
138
+
139
+ if self.rename_on_conflict:
140
+ base, ext = os.path.splitext(original_img_basename)
141
+ unique_id = str(uuid.uuid4())[:8]
142
+ new_img_basename = f"{base}_{unique_id}{ext}"
143
+ else:
144
+ new_img_basename = original_img_basename
145
+
146
+ dest_image_path = os.path.join(img_out_dir, new_img_basename)
147
+ shutil.copy(src_image_path, dest_image_path)
148
+
149
+ dest_label_map[dest_image_path.replace("\\", "/")] = label_path
150
+ self.progress_updated.emit(i + 1)
151
+
152
+ return dest_label_map
134
153
 
135
154
  def _create_raw_annotations(self, image_label_paths, class_names):
136
155
  """
@@ -142,7 +161,11 @@ class DatasetProcessor(QObject):
142
161
  if not self.is_running:
143
162
  break
144
163
 
145
- # Get image dimensions for denormalizing coordinates
164
+ # If there's no label file for this image (e.g., 'import all' policy), skip to progress update
165
+ if not label_path:
166
+ self.progress_updated.emit(i + 1)
167
+ continue
168
+
146
169
  image_height, image_width = rasterio_open(image_path).shape
147
170
  with open(label_path, 'r') as file:
148
171
  lines = file.readlines()
@@ -151,9 +174,13 @@ class DatasetProcessor(QObject):
151
174
  try:
152
175
  parts = list(map(float, line.split()))
153
176
  class_id = int(parts[0])
154
- raw_ann_data = {"image_path": image_path, "class_name": class_names[class_id]}
155
177
 
156
- # --- Step 1: Parse the source data based on the original task type ---
178
+ class_name = class_names[class_id]
179
+ if class_name in self.excluded_classes:
180
+ continue # Skip this annotation if its class was unchecked
181
+
182
+ raw_ann_data = {"image_path": image_path, "class_name": class_name}
183
+
157
184
  parsed_data = {}
158
185
  if self.task == 'detect': # Source is bbox: class, x_c, y_c, w, h
159
186
  _, x_c, y_c, w, h = parts
@@ -165,47 +192,45 @@ class DatasetProcessor(QObject):
165
192
  parsed_data['bottom_right'] = (x + width / 2, y + height / 2)
166
193
  else: # Source is polygon: class, x1, y1, x2, y2, ...
167
194
  points_norm = parts[1:]
195
+ # Convert normalized coordinates to pixel coordinates
196
+ # Extract x and y coordinates from the flattened list
197
+ x_coords = points_norm[::2] # Every even index (0, 2, 4...)
198
+ y_coords = points_norm[1::2] # Every odd index (1, 3, 5...)
199
+
200
+ # Scale coordinates by image dimensions
168
201
  points = []
169
- for x, y in zip(points_norm[::2], points_norm[1::2]):
170
- points.append((x * image_width, y * image_height))
202
+ for x, y in zip(x_coords, y_coords):
203
+ pixel_x = x * image_width
204
+ pixel_y = y * image_height
205
+ points.append((pixel_x, pixel_y))
171
206
  parsed_data['points'] = points
172
207
 
173
- # --- Step 2: Convert to the target format if necessary ---
174
208
  if self.import_as == 'rectangle':
175
209
  raw_ann_data["type"] = "RectangleAnnotation"
176
- if 'top_left' in parsed_data: # Already a rectangle
210
+ if 'top_left' in parsed_data:
177
211
  raw_ann_data.update(parsed_data)
178
- else: # Convert polygon to rectangle (bounding box)
212
+ else:
179
213
  points = parsed_data['points']
180
214
  x_coords = [p[0] for p in points]
181
215
  y_coords = [p[1] for p in points]
182
216
  raw_ann_data["top_left"] = (min(x_coords), min(y_coords))
183
217
  raw_ann_data["bottom_right"] = (max(x_coords), max(y_coords))
184
-
185
218
  elif self.import_as == 'polygon':
186
219
  raw_ann_data["type"] = "PolygonAnnotation"
187
- if 'points' in parsed_data: # Already a polygon
220
+ if 'points' in parsed_data:
188
221
  raw_ann_data.update(parsed_data)
189
- else: # Convert rectangle to polygon
190
- tl = parsed_data['top_left']
191
- br = parsed_data['bottom_right']
192
- raw_ann_data["points"] = [
193
- (tl[0], tl[1]), (br[0], tl[1]),
194
- (br[0], br[1]), (tl[0], br[1])
195
- ]
196
-
222
+ else:
223
+ tl, br = parsed_data['top_left'], parsed_data['bottom_right']
224
+ raw_ann_data["points"] = [(tl[0], tl[1]), (br[0], tl[1]), (br[0], br[1]), (tl[0], br[1])]
197
225
  all_raw_annotations.append(raw_ann_data)
198
226
  except (ValueError, IndexError) as e:
199
- # Log the malformed line error instead of printing
200
227
  error_msg = (f"In file '{os.path.basename(label_path)}' on line {line_num + 1}:\n"
201
228
  f"Skipped malformed content: '{line.strip()}'\nReason: {e}\n")
202
229
  self.parsing_errors.append(error_msg)
203
230
 
204
-
205
- # Update progress after each image
206
231
  self.progress_updated.emit(i + 1)
207
232
  return all_raw_annotations
208
-
233
+
209
234
 
210
235
  # ----------------------------------------------------------------------------------------------------------------------
211
236
  # Dialog Classes
@@ -221,13 +246,14 @@ class Base(QDialog):
221
246
 
222
247
  self.setWindowIcon(get_icon("coral.png"))
223
248
  self.setWindowTitle("Import Dataset")
224
- self.resize(500, 200) # Increased height for new widget
249
+ self.resize(500, 350)
225
250
 
226
251
  self.task = None
227
252
  self.progress_bar = None
228
253
  self.thread = None
229
254
  self.worker = None
230
255
  self.output_folder = None
256
+ self.class_checkboxes = []
231
257
 
232
258
  self.layout = QVBoxLayout(self)
233
259
  self.setup_info_layout()
@@ -235,12 +261,14 @@ class Base(QDialog):
235
261
  self.setup_output_layout()
236
262
  self.setup_buttons_layout()
237
263
 
264
+ self.advanced_options_toggle.setEnabled(False)
265
+ self.advanced_options_frame.setVisible(False)
266
+
238
267
  def setup_info_layout(self):
239
268
  raise NotImplementedError("Subclasses must implement method.")
240
269
 
241
270
  def setup_yaml_layout(self):
242
271
  """Set up the layout for selecting the data YAML file."""
243
- # Group box for YAML file selection
244
272
  group_box = QGroupBox("Data YAML File")
245
273
  layout = QGridLayout()
246
274
  layout.addWidget(QLabel("File:"), 0, 0)
@@ -255,8 +283,7 @@ class Base(QDialog):
255
283
  self.layout.addWidget(group_box)
256
284
 
257
285
  def setup_output_layout(self):
258
- """Set up the layout for output directory and folder name selection."""
259
- # Group box for output directory and folder name
286
+ """Set up the layout for output directory and the advanced options accordion."""
260
287
  group_box = QGroupBox("Output Settings")
261
288
  layout = QGridLayout()
262
289
  layout.addWidget(QLabel("Directory:"), 0, 0)
@@ -273,41 +300,94 @@ class Base(QDialog):
273
300
  group_box.setLayout(layout)
274
301
  self.layout.addWidget(group_box)
275
302
 
303
+ self.advanced_options_toggle = QToolButton()
304
+ self.advanced_options_toggle.setText("Advanced Options")
305
+ self.advanced_options_toggle.setCheckable(True)
306
+ self.advanced_options_toggle.setStyleSheet("QToolButton { border: none; }")
307
+ self.advanced_options_toggle.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
308
+ self.advanced_options_toggle.setArrowType(Qt.RightArrow)
309
+ self.advanced_options_toggle.toggled.connect(self.toggle_advanced_options)
310
+ self.layout.addWidget(self.advanced_options_toggle)
311
+
312
+ self.advanced_options_frame = QFrame()
313
+ self.advanced_options_frame.setFrameShape(QFrame.StyledPanel)
314
+ advanced_layout = QVBoxLayout(self.advanced_options_frame)
315
+
316
+ image_rule_box = QGroupBox("Image Import Rule")
317
+ image_rule_layout = QVBoxLayout()
318
+ self.import_annotated_images_radio = QRadioButton("Import only images with annotations")
319
+ self.import_all_images_radio = QRadioButton("Import all images found in dataset")
320
+ self.import_annotated_images_radio.setChecked(True)
321
+ image_rule_layout.addWidget(self.import_annotated_images_radio)
322
+ image_rule_layout.addWidget(self.import_all_images_radio)
323
+ image_rule_box.setLayout(image_rule_layout)
324
+ advanced_layout.addWidget(image_rule_box)
325
+
326
+ class_filter_box = QGroupBox("Classes to Import")
327
+ class_filter_layout = QVBoxLayout()
328
+ self.class_scroll_area = QScrollArea()
329
+ self.class_scroll_area.setWidgetResizable(True)
330
+ self.class_widget = QFrame()
331
+ self.class_layout = QVBoxLayout(self.class_widget)
332
+ self.class_scroll_area.setWidget(self.class_widget)
333
+ class_filter_layout.addWidget(self.class_scroll_area)
334
+ class_filter_box.setLayout(class_filter_layout)
335
+ advanced_layout.addWidget(class_filter_box)
336
+ self.layout.addWidget(self.advanced_options_frame)
337
+
338
+ def toggle_advanced_options(self, checked):
339
+ self.advanced_options_toggle.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
340
+ self.advanced_options_frame.setVisible(checked)
341
+
276
342
  def setup_buttons_layout(self):
277
343
  """Set up the OK/Cancel button box."""
278
- # Dialog button box for OK and Cancel actions
279
344
  self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
280
345
  self.button_box.accepted.connect(self.start_processing)
281
346
  self.button_box.rejected.connect(self.reject)
282
347
  self.layout.addWidget(self.button_box)
283
348
 
284
349
  def browse_data_yaml(self):
285
- """Open a file dialog to select the data YAML file."""
286
- # File dialog for selecting YAML file
350
+ """Open a file dialog to select the data YAML file and populate advanced options."""
287
351
  options = QFileDialog.Options()
288
352
  file_path, _ = QFileDialog.getOpenFileName(
289
- self,
290
- "Select Data YAML",
291
- "",
292
- "YAML Files (*.yaml);;All Files (*)",
293
- options=options
353
+ self, "Select Data YAML", "", "YAML Files (*.yaml);;All Files (*)", options=options
294
354
  )
295
- if file_path:
355
+ if not file_path:
356
+ return
357
+
358
+ try:
359
+ with open(file_path, 'r') as file:
360
+ data = yaml.safe_load(file)
361
+ class_names = data.get('names', [])
362
+ if not class_names:
363
+ QMessageBox.warning(self, "Warning", "Could not find a 'names' list in the selected YAML file.")
364
+ return
365
+
296
366
  self.yaml_path_label.setText(file_path)
297
- # Auto-fill output directory to be the PARENT of the yaml's directory
298
- if not self.output_dir_label.text():
299
- parent_dir = os.path.dirname(os.path.dirname(file_path))
300
- self.output_dir_label.setText(parent_dir)
301
- if not self.output_folder_name.text():
302
- # Suggest a folder name based on the yaml file's parent folder
303
- project_name = os.path.basename(os.path.dirname(file_path))
304
- self.output_folder_name.setText(f"{project_name}_imported")
367
+ yaml_dir = os.path.dirname(file_path)
368
+ self.output_dir_label.setText(yaml_dir)
369
+ self.output_folder_name.setText("data")
370
+
371
+ for checkbox in self.class_checkboxes:
372
+ self.class_layout.removeWidget(checkbox)
373
+ checkbox.deleteLater()
374
+ self.class_checkboxes.clear()
375
+
376
+ for name in class_names:
377
+ checkbox = QCheckBox(name)
378
+ checkbox.setChecked(True)
379
+ self.class_layout.addWidget(checkbox)
380
+ self.class_checkboxes.append(checkbox)
381
+
382
+ self.advanced_options_toggle.setEnabled(True)
383
+
384
+ except Exception as e:
385
+ QMessageBox.critical(self, "Error", f"Failed to read or parse YAML file:\n{e}")
386
+ self.advanced_options_toggle.setEnabled(False)
305
387
 
306
388
  def browse_output_dir(self):
307
389
  """Open a dialog to select the output directory."""
308
- # Directory dialog for selecting output directory
309
- dir_path = QFileDialog.getExistingDirectory(
310
- self, "Select Output Directory")
390
+ dir_path = QFileDialog.getExistingDirectory(self, "Select Output Directory")
311
391
  if dir_path:
312
392
  self.output_dir_label.setText(dir_path)
313
393
 
@@ -316,28 +396,22 @@ class Base(QDialog):
316
396
  if not all([self.yaml_path_label.text(), self.output_dir_label.text(), self.output_folder_name.text()]):
317
397
  QMessageBox.warning(self, "Error", "Please fill in all fields.")
318
398
  return
319
-
320
- # This check for existing output is still relevant
399
+
321
400
  self.output_folder = os.path.join(self.output_dir_label.text(), self.output_folder_name.text())
322
401
  if os.path.exists(self.output_folder) and os.listdir(self.output_folder):
323
- reply = QMessageBox.question(self,
324
- 'Directory Not Empty',
325
- f"The directory '{self.output_folder}' is not empty. Continue?",
402
+ reply = QMessageBox.question(self,
403
+ 'Directory Not Empty',
404
+ f"The directory '{self.output_folder}' is not empty. Continue?",
326
405
  QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
327
- if reply == QMessageBox.No:
328
- return
406
+ if reply == QMessageBox.No: return
329
407
 
330
- # Pre-scan for duplicates
331
- yaml_path = self.yaml_path_label.text()
332
- dir_path = os.path.dirname(yaml_path)
333
408
  QApplication.setOverrideCursor(Qt.WaitCursor)
334
409
  try:
335
- image_paths = glob.glob(f"{dir_path}/**/images/*.*", recursive=True)
410
+ image_paths = glob.glob(f"{os.path.dirname(self.yaml_path_label.text())}/**/images/*.*", recursive=True)
336
411
  finally:
337
412
  QApplication.restoreOverrideCursor()
338
413
 
339
- basenames = set()
340
- duplicates_exist = False
414
+ basenames, duplicates_exist = set(), False
341
415
  for path in image_paths:
342
416
  basename_no_ext = os.path.splitext(os.path.basename(path))[0]
343
417
  if basename_no_ext in basenames:
@@ -345,7 +419,6 @@ class Base(QDialog):
345
419
  break
346
420
  basenames.add(basename_no_ext)
347
421
 
348
- # Default behavior is to overwrite, but we will confirm with the user if conflicts exist.
349
422
  rename_files = False
350
423
  if duplicates_exist:
351
424
  msg_box = QMessageBox(self)
@@ -357,51 +430,63 @@ class Base(QDialog):
357
430
  )
358
431
  msg_box.setInformativeText("How would you like to handle these conflicts?")
359
432
 
360
- # Add custom buttons for each action
361
- rename_button = msg_box.addButton("Rename Files (Safe)", QMessageBox.AcceptRole)
362
- overwrite_button = msg_box.addButton("Overwrite", QMessageBox.DestructiveRole)
363
- cancel_button = msg_box.addButton("Cancel", QMessageBox.RejectRole)
433
+ # Add buttons with proper line breaks
434
+ rename_button = msg_box.addButton(
435
+ "Rename Files (Safe)",
436
+ QMessageBox.AcceptRole
437
+ )
438
+ overwrite_button = msg_box.addButton(
439
+ "Overwrite",
440
+ QMessageBox.DestructiveRole
441
+ )
442
+ cancel_button = msg_box.addButton(
443
+ "Cancel",
444
+ QMessageBox.RejectRole
445
+ )
364
446
 
365
447
  msg_box.setDefaultButton(rename_button)
366
448
  msg_box.exec_()
367
449
 
368
450
  clicked_button = msg_box.clickedButton()
369
-
370
451
  if clicked_button == cancel_button:
371
- return # Stop the process
452
+ return
372
453
  elif clicked_button == rename_button:
373
454
  rename_files = True
374
455
  elif clicked_button == overwrite_button:
375
456
  rename_files = False
376
- else: # User closed the dialog
457
+ else:
377
458
  return
378
-
459
+
460
+ excluded_classes = set()
461
+ if self.advanced_options_toggle.isEnabled():
462
+ for checkbox in self.class_checkboxes:
463
+ if not checkbox.isChecked():
464
+ excluded_classes.add(checkbox.text())
465
+ image_import_policy = 'all' if self.import_all_images_radio.isChecked() else 'annotated_only'
466
+
379
467
  self.button_box.setEnabled(False)
380
468
  QApplication.setOverrideCursor(Qt.WaitCursor)
381
-
382
469
  self.progress_bar = ProgressBar(self, title="Preparing to Import...")
383
470
  self.progress_bar.show()
384
471
 
385
- # Get the selected import format from the combobox
386
- import_as_text = self.import_as_combo.currentText()
387
- import_as = 'polygon' if 'Polygon' in import_as_text else 'rectangle'
472
+ import_as = 'polygon' if 'Polygon' in self.import_as_combo.currentText() else 'rectangle'
388
473
 
389
474
  self.thread = QThread()
390
475
  self.worker = DatasetProcessor(
391
476
  yaml_path=self.yaml_path_label.text(),
392
477
  output_folder=self.output_folder,
393
478
  task=self.task,
394
- import_as=import_as, # Pass the user's choice
395
- rename_on_conflict=rename_files # Pass the user's final decision
479
+ import_as=import_as,
480
+ rename_on_conflict=rename_files,
481
+ excluded_classes=excluded_classes,
482
+ image_import_policy=image_import_policy
396
483
  )
397
484
  self.worker.moveToThread(self.thread)
398
-
399
485
  self.thread.started.connect(self.worker.run)
400
486
  self.worker.finished.connect(self.on_worker_finished)
401
487
  self.worker.error.connect(self.on_error)
402
488
  self.worker.status_changed.connect(self.on_status_changed)
403
489
  self.worker.progress_updated.connect(self.on_progress_update)
404
- # Connect to the updated signal
405
490
  self.worker.processing_complete.connect(self.on_processing_complete)
406
491
  self.thread.start()
407
492
 
@@ -413,65 +498,72 @@ class Base(QDialog):
413
498
  self.progress_bar.set_value(value)
414
499
 
415
500
  def on_processing_complete(self, raw_annotations, image_paths, parsing_errors):
501
+ progress_bar = ProgressBar(self, title="Adding Data to Project...")
502
+ progress_bar.show()
503
+
416
504
  added_paths = []
505
+ progress_bar.set_title(f"Adding {len(image_paths)} images...")
506
+ progress_bar.start_progress(len(image_paths))
417
507
  for path in image_paths:
418
508
  if self.image_window.add_image(path):
419
509
  added_paths.append(path)
510
+ progress_bar.update_progress()
420
511
 
421
512
  newly_created_annotations = []
513
+ progress_bar.set_title(f"Adding {len(raw_annotations)} annotations...")
514
+ progress_bar.start_progress(len(raw_annotations))
422
515
  for raw_ann in raw_annotations:
423
- label = self.main_window.label_window.add_label_if_not_exists(
424
- raw_ann["class_name"])
516
+ label = self.main_window.label_window.add_label_if_not_exists(raw_ann["class_name"])
425
517
  if raw_ann["type"] == "RectangleAnnotation":
426
518
  tl, br = raw_ann["top_left"], raw_ann["bottom_right"]
427
519
  annotation = RectangleAnnotation(QPointF(tl[0], tl[1]),
428
- QPointF(br[0], br[1]),
520
+ QPointF(br[0], br[1]),
429
521
  label.short_label_code,
430
522
  label.long_label_code,
431
- label.color,
523
+ label.color,
432
524
  raw_ann["image_path"],
433
525
  label.id,
434
526
  self.main_window.get_transparency_value())
435
- else: # PolygonAnnotation
527
+ else:
436
528
  points = [QPointF(p[0], p[1]) for p in raw_ann["points"]]
437
- annotation = PolygonAnnotation(points,
438
- label.short_label_code,
439
- label.long_label_code,
440
- label.color,
529
+ annotation = PolygonAnnotation(points,
530
+ label.short_label_code,
531
+ label.long_label_code,
532
+ label.color,
441
533
  raw_ann["image_path"],
442
534
  label.id,
443
535
  self.main_window.get_transparency_value())
444
-
536
+
445
537
  self.annotation_window.add_annotation_to_dict(annotation)
446
- newly_created_annotations.append(annotation) # Add to our list
538
+ newly_created_annotations.append(annotation) # Collect created objects
539
+
540
+ progress_bar.update_progress()
447
541
 
448
- # --- Now, export the fully created objects to JSON ---
449
- self.progress_bar.set_title("Exporting to annotations.json...")
450
- self.export_annotations_to_json(newly_created_annotations, self.output_folder)
542
+ # --- Call the restored, correct export function ---
543
+ progress_bar.set_title("Exporting annotations.json...")
544
+ self._export_annotations_to_json(newly_created_annotations, self.output_folder)
545
+
546
+ progress_bar.finish_progress()
547
+ progress_bar.stop_progress()
548
+ progress_bar.close()
451
549
 
452
550
  self.image_window.filter_images()
453
-
454
551
  if added_paths:
455
552
  self.image_window.load_image_by_path(added_paths[-1])
456
553
  self.image_window.update_image_annotations(added_paths[-1])
457
554
  self.annotation_window.load_annotations()
458
555
 
459
- # --- Display a summary message, including any parsing errors ---
460
556
  summary_message = "Dataset has been successfully imported."
461
557
  if parsing_errors:
462
- # If there were errors, show a more detailed dialog
463
558
  QMessageBox.warning(self,
464
- "Import Complete with Warnings",
465
- f"{summary_message}\n\nHowever, {len(parsing_errors)} issue(s) were found "
466
- "in the label files. Please review them below.",
559
+ "Import Complete with Warnings",
560
+ f"{summary_message}\n\nHowever, {len(parsing_errors)} issue(s) were found. "
561
+ "Please review them below.",
467
562
  details='\n'.join(parsing_errors))
468
563
  else:
469
- # Otherwise, show a simple info box
470
- QMessageBox.information(self,
471
- "Dataset Imported",
472
- summary_message)
473
-
474
- def export_annotations_to_json(self, annotations_list, output_dir):
564
+ QMessageBox.information(self, "Dataset Imported", summary_message)
565
+
566
+ def _export_annotations_to_json(self, annotations_list, output_dir):
475
567
  """
476
568
  Merges the list of annotation objects into an existing annotations.json file,
477
569
  or creates a new one if it doesn't exist.
@@ -527,7 +619,7 @@ class Base(QDialog):
527
619
  file.flush()
528
620
  except Exception as e:
529
621
  QMessageBox.critical(self, "Export Error", f"Failed to write annotations.json:\n{e}")
530
-
622
+
531
623
  def on_error(self, message):
532
624
  QMessageBox.warning(self, "Error", message)
533
625
 
@@ -554,4 +646,4 @@ class Base(QDialog):
554
646
 
555
647
  def closeEvent(self, event):
556
648
  self.reject()
557
- super().closeEvent(event)
649
+ super().closeEvent(event)