coralnet-toolbox 0.0.69__py2.py3-none-any.whl → 0.0.71__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.
@@ -262,7 +262,7 @@ class Annotation(QObject):
262
262
  # [1, 2] - Very small dots with small gaps
263
263
  # [2, 4] - Small dots with larger gaps
264
264
  # [1, 3] - Tiny dots with medium gaps
265
- pen = QPen(pen_color, 3) # Width for dotted line
265
+ pen = QPen(pen_color, 4) # Width for dotted line
266
266
  pen.setStyle(Qt.CustomDashLine)
267
267
  pen.setDashPattern([1, 2]) # Dotted pattern: 2 pixels on, 3 pixels off
268
268
  pen.setDashOffset(self._animated_line)
@@ -130,7 +130,7 @@ class EmbeddingViewer(QWidget):
130
130
  # Create a QToolButton to have both a primary action and a dropdown menu
131
131
  self.find_mislabels_button = QToolButton()
132
132
  self.find_mislabels_button.setText("Find Potential Mislabels")
133
- self.find_mislabels_button.setPopupMode(QToolButton.MenuButtonPopup) # Key change for split-button style
133
+ self.find_mislabels_button.setPopupMode(QToolButton.MenuButtonPopup) # Key change for split-button style
134
134
  self.find_mislabels_button.setToolButtonStyle(Qt.ToolButtonTextOnly)
135
135
  self.find_mislabels_button.setStyleSheet(
136
136
  "QToolButton::menu-indicator {"
@@ -186,9 +186,18 @@ class EmbeddingViewer(QWidget):
186
186
 
187
187
  uncertainty_settings_widget.parameters_changed.connect(self.uncertainty_parameters_changed.emit)
188
188
  toolbar_layout.addWidget(self.find_uncertain_button)
189
-
189
+
190
+ # Add a strech and separator
190
191
  toolbar_layout.addStretch()
191
-
192
+ toolbar_layout.addWidget(self._create_separator())
193
+
194
+ # Center on selection button
195
+ self.center_on_selection_button = QPushButton()
196
+ self.center_on_selection_button.setIcon(get_icon("target.png"))
197
+ self.center_on_selection_button.setToolTip("Center view on selected point(s)")
198
+ self.center_on_selection_button.clicked.connect(self.center_on_selection)
199
+ toolbar_layout.addWidget(self.center_on_selection_button)
200
+
192
201
  # Home button to reset view
193
202
  self.home_button = QPushButton()
194
203
  self.home_button.setIcon(get_icon("home.png"))
@@ -258,6 +267,7 @@ class EmbeddingViewer(QWidget):
258
267
 
259
268
  self.find_mislabels_button.setEnabled(points_exist)
260
269
  self.find_uncertain_button.setEnabled(points_exist and self.is_uncertainty_analysis_available)
270
+ self.center_on_selection_button.setEnabled(points_exist and selection_exists)
261
271
 
262
272
  if self.isolated_mode:
263
273
  self.isolate_button.hide()
@@ -270,12 +280,46 @@ class EmbeddingViewer(QWidget):
270
280
  def reset_view(self):
271
281
  """Reset the view to fit all embedding points."""
272
282
  self.fit_view_to_points()
283
+
284
+ def center_on_selection(self):
285
+ """Centers the view on selected point(s) or maintains the current view if no points are selected."""
286
+ selected_items = self.graphics_scene.selectedItems()
287
+ if not selected_items:
288
+ # No selection, show a message
289
+ QMessageBox.information(self, "No Selection", "Please select one or more points first.")
290
+ return
291
+
292
+ # Create a bounding rect that encompasses all selected points
293
+ selection_rect = None
294
+
295
+ for item in selected_items:
296
+ if isinstance(item, EmbeddingPointItem):
297
+ # Get the item's bounding rect in scene coordinates
298
+ item_rect = item.sceneBoundingRect()
299
+
300
+ # Add padding around the point for better visibility
301
+ padding = 50 # pixels
302
+ item_rect = item_rect.adjusted(-padding, -padding, padding, padding)
303
+
304
+ if selection_rect is None:
305
+ selection_rect = item_rect
306
+ else:
307
+ selection_rect = selection_rect.united(item_rect)
308
+
309
+ if selection_rect:
310
+ # Add extra margin for better visibility
311
+ margin = 20
312
+ selection_rect = selection_rect.adjusted(-margin, -margin, margin, margin)
313
+
314
+ # Fit the view to the selection rect
315
+ self.graphics_view.fitInView(selection_rect, Qt.KeepAspectRatio)
273
316
 
274
317
  def show_placeholder(self):
275
318
  """Show the placeholder message and hide the graphics view."""
276
319
  self.graphics_view.setVisible(False)
277
320
  self.placeholder_label.setVisible(True)
278
321
  self.home_button.setEnabled(False)
322
+ self.center_on_selection_button.setEnabled(False) # Disable center button
279
323
  self.find_mislabels_button.setEnabled(False)
280
324
  self.find_uncertain_button.setEnabled(False)
281
325
 
@@ -346,6 +390,39 @@ class EmbeddingViewer(QWidget):
346
390
 
347
391
  def mousePressEvent(self, event):
348
392
  """Handle mouse press for selection (point or rubber band) and panning."""
393
+ # Ctrl+Right-Click for context menu selection
394
+ if event.button() == Qt.RightButton and event.modifiers() == Qt.ControlModifier:
395
+ item_at_pos = self.graphics_view.itemAt(event.pos())
396
+ if isinstance(item_at_pos, EmbeddingPointItem):
397
+ # 1. Clear all selections in both viewers
398
+ self.graphics_scene.clearSelection()
399
+ item_at_pos.setSelected(True)
400
+ self.on_selection_changed() # Updates internal state and emits signals
401
+
402
+ # 2. Sync annotation viewer selection
403
+ ann_id = item_at_pos.data_item.annotation.id
404
+ self.explorer_window.annotation_viewer.render_selection_from_ids({ann_id})
405
+
406
+ # 3. Update annotation window (set image, select, center)
407
+ explorer = self.explorer_window
408
+ annotation = item_at_pos.data_item.annotation
409
+ image_path = annotation.image_path
410
+
411
+ if hasattr(explorer, 'annotation_window'):
412
+ if explorer.annotation_window.current_image_path != image_path:
413
+ if hasattr(explorer.annotation_window, 'set_image'):
414
+ explorer.annotation_window.set_image(image_path)
415
+ if hasattr(explorer.annotation_window, 'select_annotation'):
416
+ explorer.annotation_window.select_annotation(annotation)
417
+ if hasattr(explorer.annotation_window, 'center_on_annotation'):
418
+ explorer.annotation_window.center_on_annotation(annotation)
419
+
420
+ explorer.update_label_window_selection()
421
+ explorer.update_button_states()
422
+ event.accept()
423
+ return
424
+
425
+ # Handle left-click for selection or rubber band
349
426
  if event.button() == Qt.LeftButton and event.modifiers() == Qt.ControlModifier:
350
427
  item_at_pos = self.graphics_view.itemAt(event.pos())
351
428
  if isinstance(item_at_pos, EmbeddingPointItem):
@@ -701,6 +778,14 @@ class AnnotationViewer(QScrollArea):
701
778
  explorer = self.explorer_window
702
779
  image_path = widget.annotation.image_path
703
780
  annotation_to_select = widget.annotation
781
+
782
+ # ctrl+right click to only select this annotation (single selection):
783
+ self.clear_selection()
784
+ self.select_widget(widget)
785
+ changed_ids = [widget.data_item.annotation.id]
786
+
787
+ if changed_ids:
788
+ self.selection_changed.emit(changed_ids)
704
789
 
705
790
  if hasattr(explorer, 'annotation_window'):
706
791
  # Check if the image needs to be changed
@@ -710,15 +795,17 @@ class AnnotationViewer(QScrollArea):
710
795
 
711
796
  # Now, select the annotation in the annotation_window
712
797
  if hasattr(explorer.annotation_window, 'select_annotation'):
713
- # This method by default unselects other annotations
714
798
  explorer.annotation_window.select_annotation(annotation_to_select)
799
+
800
+ # Center the annotation window view on the selected annotation
801
+ if hasattr(explorer.annotation_window, 'center_on_annotation'):
802
+ explorer.annotation_window.center_on_annotation(annotation_to_select)
715
803
 
716
804
  # Also clear any existing selection in the explorer window itself
717
- explorer.annotation_viewer.clear_selection()
718
- explorer.embedding_viewer.render_selection_from_ids(set())
805
+ explorer.embedding_viewer.render_selection_from_ids({widget.data_item.annotation.id})
719
806
  explorer.update_label_window_selection()
720
807
  explorer.update_button_states()
721
-
808
+
722
809
  event.accept()
723
810
 
724
811
  @pyqtSlot()
@@ -982,6 +1069,8 @@ class AnnotationViewer(QScrollArea):
982
1069
 
983
1070
  self.recalculate_widget_positions()
984
1071
  self._update_toolbar_state()
1072
+ # Update the label window with the new annotation count
1073
+ self.explorer_window.main_window.label_window.update_annotation_count()
985
1074
 
986
1075
  def resizeEvent(self, event):
987
1076
  """On window resize, reflow the annotation widgets."""
@@ -1027,11 +1116,17 @@ class AnnotationViewer(QScrollArea):
1027
1116
  break
1028
1117
  widget = widget.parent()
1029
1118
 
1030
- # If click is outside widgets and there is a selection, clear it
1031
- if not is_on_widget and self.selected_widgets:
1032
- changed_ids = [w.data_item.annotation.id for w in self.selected_widgets]
1033
- self.clear_selection()
1034
- self.selection_changed.emit(changed_ids)
1119
+ # If click is outside widgets, clear annotation_window selection
1120
+ if not is_on_widget:
1121
+ # Clear annotation selection in the annotation_window as well
1122
+ if hasattr(self.explorer_window, 'annotation_window') and self.explorer_window.annotation_window:
1123
+ if hasattr(self.explorer_window.annotation_window, 'unselect_annotations'):
1124
+ self.explorer_window.annotation_window.unselect_annotations()
1125
+ # If there is a selection in the viewer, clear it
1126
+ if self.selected_widgets:
1127
+ changed_ids = [w.data_item.annotation.id for w in self.selected_widgets]
1128
+ self.clear_selection()
1129
+ self.selection_changed.emit(changed_ids)
1035
1130
  return
1036
1131
 
1037
1132
  elif event.modifiers() == Qt.ControlModifier:
@@ -1371,6 +1466,7 @@ class ExplorerWindow(QMainWindow):
1371
1466
 
1372
1467
  self.device = main_window.device
1373
1468
  self.loaded_model = None
1469
+ self.loaded_model_imgsz = 128
1374
1470
 
1375
1471
  self.feature_store = FeatureStore()
1376
1472
 
@@ -1724,13 +1820,15 @@ class ExplorerWindow(QMainWindow):
1724
1820
  """
1725
1821
  Identifies annotations whose label does not match the majority of its
1726
1822
  k-nearest neighbors in the high-dimensional feature space.
1823
+ Skips any annotation or neighbor with an invalid label (id == -1).
1727
1824
  """
1728
1825
  # Get parameters from the stored property instead of hardcoding
1729
1826
  K = self.mislabel_params.get('k', 5)
1730
1827
  agreement_threshold = self.mislabel_params.get('threshold', 0.6)
1731
1828
 
1732
1829
  if not self.embedding_viewer.points_by_id or len(self.embedding_viewer.points_by_id) < K:
1733
- QMessageBox.information(self, "Not Enough Data",
1830
+ QMessageBox.information(self,
1831
+ "Not Enough Data",
1734
1832
  f"This feature requires at least {K} points in the embedding viewer.")
1735
1833
  return
1736
1834
 
@@ -1741,7 +1839,6 @@ class ExplorerWindow(QMainWindow):
1741
1839
  model_info = self.model_settings_widget.get_selected_model()
1742
1840
  model_name, feature_mode = model_info if isinstance(model_info, tuple) else (model_info, "default")
1743
1841
  sanitized_model_name = os.path.basename(model_name).replace(' ', '_')
1744
- # FIX: Also replace the forward slash to handle "N/A"
1745
1842
  sanitized_feature_mode = feature_mode.replace(' ', '_').replace('/', '_')
1746
1843
  model_key = f"{sanitized_model_name}_{sanitized_feature_mode}"
1747
1844
 
@@ -1751,13 +1848,17 @@ class ExplorerWindow(QMainWindow):
1751
1848
  index = self.feature_store._get_or_load_index(model_key)
1752
1849
  faiss_idx_to_ann_id = self.feature_store.get_faiss_index_to_annotation_id_map(model_key)
1753
1850
  if index is None or not faiss_idx_to_ann_id:
1754
- QMessageBox.warning(self, "Error", "Could not find a valid feature index for the current model.")
1851
+ QMessageBox.warning(self,
1852
+ "Error",
1853
+ "Could not find a valid feature index for the current model.")
1755
1854
  return
1756
1855
 
1757
1856
  # Get the high-dimensional features for the points in the current view
1758
1857
  features_dict, _ = self.feature_store.get_features(data_items_in_view, model_key)
1759
1858
  if not features_dict:
1760
- QMessageBox.warning(self, "Error", "Could not retrieve features for the items in view.")
1859
+ QMessageBox.warning(self,
1860
+ "Error",
1861
+ "Could not retrieve features for the items in view.")
1761
1862
  return
1762
1863
 
1763
1864
  query_ann_ids = list(features_dict.keys())
@@ -1768,25 +1869,33 @@ class ExplorerWindow(QMainWindow):
1768
1869
 
1769
1870
  mislabeled_ann_ids = []
1770
1871
  for i, ann_id in enumerate(query_ann_ids):
1771
- current_label = self.data_item_cache[ann_id].effective_label.id
1772
-
1872
+ data_item = self.data_item_cache[ann_id]
1873
+ # Use preview_label if present, else effective_label
1874
+ label_obj = getattr(data_item, "preview_label", None) or data_item.effective_label
1875
+ current_label_id = getattr(label_obj, "id", "-1")
1876
+ if current_label_id == "-1":
1877
+ continue # Skip if label is invalid
1878
+
1773
1879
  # Get neighbor labels, ignoring the first result (the point itself)
1774
1880
  neighbor_faiss_indices = I[i][1:]
1775
-
1881
+
1776
1882
  neighbor_labels = []
1777
1883
  for n_idx in neighbor_faiss_indices:
1778
- # THIS IS THE CORRECTED LOGIC
1779
1884
  if n_idx in faiss_idx_to_ann_id:
1780
1885
  neighbor_ann_id = faiss_idx_to_ann_id[n_idx]
1781
- # ADD THIS CHECK to ensure the neighbor hasn't been deleted
1782
1886
  if neighbor_ann_id in self.data_item_cache:
1783
- neighbor_labels.append(self.data_item_cache[neighbor_ann_id].effective_label.id)
1887
+ neighbor_item = self.data_item_cache[neighbor_ann_id]
1888
+ neighbor_label_obj = getattr(neighbor_item, "preview_label", None)
1889
+ if neighbor_label_obj is None:
1890
+ neighbor_label_obj = neighbor_item.effective_label
1891
+ neighbor_label_id = getattr(neighbor_label_obj, "id", "-1")
1892
+ if neighbor_label_id != "-1":
1893
+ neighbor_labels.append(neighbor_label_id)
1784
1894
 
1785
1895
  if not neighbor_labels:
1786
1896
  continue
1787
1897
 
1788
- # Use the agreement threshold instead of strict majority
1789
- num_matching_neighbors = neighbor_labels.count(current_label)
1898
+ num_matching_neighbors = neighbor_labels.count(current_label_id)
1790
1899
  agreement_ratio = num_matching_neighbors / len(neighbor_labels)
1791
1900
 
1792
1901
  if agreement_ratio < agreement_threshold:
@@ -2031,16 +2140,36 @@ class ExplorerWindow(QMainWindow):
2031
2140
  print(f"Model or mode changed. Reloading {model_name} for '{feature_mode}'.")
2032
2141
  try:
2033
2142
  model = YOLO(model_name)
2143
+
2144
+ # Check if the model task is compatible with the selected feature mode
2145
+ if model.task != 'classify' and feature_mode == "Predictions":
2146
+ QMessageBox.warning(self,
2147
+ "Invalid Mode for Model",
2148
+ f"The selected model is a '{model.task}' model. "
2149
+ "The 'Predictions' feature mode is only available for 'classify' models. "
2150
+ "Reverting to 'Embed Features' mode.")
2151
+
2152
+ # Force the feature mode combo box back to "Embed Features"
2153
+ self.model_settings_widget.feature_mode_combo.setCurrentText("Embed Features")
2154
+
2155
+ # On failure, reset the model cache
2156
+ self.loaded_model = None
2157
+ self.current_feature_generating_model = None
2158
+ return None, None
2159
+
2034
2160
  # Update the cache key to the new successful combination
2035
2161
  self.current_feature_generating_model = current_run_key
2036
2162
  self.loaded_model = model
2037
- imgsz = getattr(model.model.args, 'imgsz', 128)
2163
+
2164
+ # Get the imgsz, but if it's larger than 128, default to 128
2165
+ imgsz = min(getattr(model.model.args, 'imgsz', 128), 128)
2166
+ self.loaded_model_imgsz = imgsz
2038
2167
 
2039
2168
  # Warm up the model
2040
2169
  dummy_image = np.zeros((imgsz, imgsz, 3), dtype=np.uint8)
2041
2170
  model.predict(dummy_image, imgsz=imgsz, half=True, device=self.device, verbose=False)
2042
2171
 
2043
- return model, imgsz
2172
+ return model, self.loaded_model_imgsz
2044
2173
 
2045
2174
  except Exception as e:
2046
2175
  print(f"ERROR: Could not load YOLO model '{model_name}': {e}")
@@ -2049,8 +2178,8 @@ class ExplorerWindow(QMainWindow):
2049
2178
  self.current_feature_generating_model = None
2050
2179
  return None, None
2051
2180
 
2052
- # Model already loaded and cached
2053
- return self.loaded_model, getattr(self.loaded_model.model.args, 'imgsz', 128)
2181
+ # Model already loaded and cached, return it and its image size
2182
+ return self.loaded_model, self.loaded_model_imgsz
2054
2183
 
2055
2184
  def _prepare_images_from_data_items(self, data_items, progress_bar=None):
2056
2185
  """
@@ -2488,6 +2617,10 @@ class ExplorerWindow(QMainWindow):
2488
2617
  # Reset sort options when filters change
2489
2618
  self.annotation_viewer.active_ordered_ids = []
2490
2619
  self.annotation_viewer.set_confidence_sort_availability(False)
2620
+
2621
+ # Update the annotation count in the label window
2622
+ self.label_window.update_annotation_count()
2623
+
2491
2624
  finally:
2492
2625
  QApplication.restoreOverrideCursor()
2493
2626
 
Binary file
@@ -660,6 +660,10 @@ class Base(QDialog):
660
660
  QMessageBox.warning(self, "Import Warning", "The YAML file appears to be empty or invalid.")
661
661
  return
662
662
 
663
+ # For backward compatibility, check if the old nested 'parameters' key exists.
664
+ # If not, use the whole data dictionary.
665
+ params_to_load = data.get('parameters', data)
666
+
663
667
  # Helper function to infer type from value
664
668
  def infer_type_and_value(value):
665
669
  """
@@ -673,12 +677,9 @@ class Base(QDialog):
673
677
  elif isinstance(value, float):
674
678
  return "float", value
675
679
  elif isinstance(value, str):
676
- # Check for boolean strings
677
680
  if value.lower() in ['true', 'false']:
678
681
  return "bool", value.lower() == 'true'
679
- # Check for numeric strings
680
682
  try:
681
- # Try to convert to int first
682
683
  if '.' not in value:
683
684
  return "int", int(value)
684
685
  else:
@@ -686,14 +687,13 @@ class Base(QDialog):
686
687
  except ValueError:
687
688
  return "string", value
688
689
  else:
689
- # For any other type, convert to string
690
690
  return "string", str(value)
691
691
 
692
692
  # Clear existing custom parameters before importing
693
693
  while self.custom_params:
694
694
  self.remove_parameter_pair()
695
695
 
696
- # Map parameters to UI controls
696
+ # Map standard parameters to their UI controls
697
697
  param_mapping = {
698
698
  'epochs': self.epochs_spinbox,
699
699
  'patience': self.patience_spinbox,
@@ -712,37 +712,31 @@ class Base(QDialog):
712
712
  }
713
713
 
714
714
  # Update UI controls with imported values
715
- for param_name, value in data.items():
715
+ for param_name, value in params_to_load.items():
716
716
  param_type, converted_value = infer_type_and_value(value)
717
717
 
718
718
  if param_name in param_mapping:
719
719
  widget = param_mapping[param_name]
720
720
 
721
721
  if isinstance(widget, QSpinBox):
722
- if param_type in ['int', 'float'] and isinstance(converted_value, (int, float)):
722
+ if isinstance(converted_value, (int, float)):
723
723
  widget.setValue(int(converted_value))
724
724
  elif isinstance(widget, QDoubleSpinBox):
725
- if param_type in ['int', 'float'] and isinstance(converted_value, (int, float)):
725
+ if isinstance(converted_value, (int, float)):
726
726
  widget.setValue(float(converted_value))
727
727
  elif isinstance(widget, QComboBox):
728
728
  if param_name in ['multi_scale', 'save', 'weighted', 'val', 'verbose']:
729
- # Boolean parameters
730
- if param_type == 'bool':
731
- widget.setCurrentText("True" if converted_value else "False")
732
- else:
733
- # String parameters like optimizer
734
- if str(converted_value) in [widget.itemText(i) for i in range(widget.count())]:
735
- widget.setCurrentText(str(converted_value))
729
+ widget.setCurrentText("True" if converted_value else "False")
730
+ elif str(converted_value) in [widget.itemText(i) for i in range(widget.count())]:
731
+ widget.setCurrentText(str(converted_value))
736
732
  else:
737
- # Add as custom parameter using inferred type
733
+ # Add as a custom parameter
738
734
  self.add_parameter_pair()
739
- param_widgets = self.custom_params[-1]
740
- param_name_widget, param_value_widget, param_type_widget = param_widgets
735
+ param_name_widget, param_value_widget, param_type_widget = self.custom_params[-1]
741
736
 
742
737
  param_name_widget.setText(param_name)
743
738
  param_type_widget.setCurrentText(param_type)
744
739
 
745
- # Set value based on type
746
740
  if param_type == "bool":
747
741
  param_value_widget.setText("True" if converted_value else "False")
748
742
  else:
@@ -750,14 +744,14 @@ class Base(QDialog):
750
744
 
751
745
  QMessageBox.information(self,
752
746
  "Import Success",
753
- "Parameters successfully imported with automatic type inference")
747
+ "Parameters successfully imported with automatic type inference.")
754
748
 
755
749
  except Exception as e:
756
750
  QMessageBox.critical(self, "Import Error", f"Failed to import parameters: {str(e)}")
757
751
 
758
752
  def export_parameters(self):
759
753
  """
760
- Export current parameters to a YAML file with explicit type information.
754
+ Export current parameters to a flat YAML file.
761
755
  """
762
756
  file_path, _ = QFileDialog.getSaveFileName(self,
763
757
  "Export Parameters to YAML",
@@ -767,69 +761,56 @@ class Base(QDialog):
767
761
  return
768
762
 
769
763
  try:
770
- # Structure: types section followed by parameters section
771
- export_data = {
772
- 'types': {},
773
- 'parameters': {}
774
- }
775
-
776
- # Standard parameters with their types
777
- standard_params = {
778
- 'epochs': ('int', self.epochs_spinbox.value()),
779
- 'patience': ('int', self.patience_spinbox.value()),
780
- 'imgsz': ('int', self.imgsz_spinbox.value()),
781
- 'batch': ('int', self.batch_spinbox.value()),
782
- 'workers': ('int', self.workers_spinbox.value()),
783
- 'save_period': ('int', self.save_period_spinbox.value()),
784
- 'freeze_layers': ('float', self.freeze_layers_spinbox.value()),
785
- 'dropout': ('float', self.dropout_spinbox.value()),
786
- 'multi_scale': ('bool', self.multi_scale_combo.currentText() == "True"),
787
- 'save': ('bool', self.save_combo.currentText() == "True"),
788
- 'weighted': ('bool', self.weighted_combo.currentText() == "True"),
789
- 'val': ('bool', self.val_combo.currentText() == "True"),
790
- 'verbose': ('bool', self.verbose_combo.currentText() == "True"),
791
- 'optimizer': ('string', self.optimizer_combo.currentText())
792
- }
793
-
794
- # Add standard parameters
795
- for param_name, (param_type, value) in standard_params.items():
796
- export_data['types'][param_name] = param_type
797
- export_data['parameters'][param_name] = value
764
+ # Use a single flat dictionary for export
765
+ export_data = {}
766
+
767
+ # Standard parameters
768
+ export_data['epochs'] = self.epochs_spinbox.value()
769
+ export_data['patience'] = self.patience_spinbox.value()
770
+ export_data['imgsz'] = self.imgsz_spinbox.value()
771
+ export_data['batch'] = self.batch_spinbox.value()
772
+ export_data['workers'] = self.workers_spinbox.value()
773
+ export_data['save_period'] = self.save_period_spinbox.value()
774
+ export_data['freeze_layers'] = self.freeze_layers_spinbox.value()
775
+ export_data['dropout'] = self.dropout_spinbox.value()
776
+ export_data['multi_scale'] = self.multi_scale_combo.currentText() == "True"
777
+ export_data['save'] = self.save_combo.currentText() == "True"
778
+ export_data['weighted'] = self.weighted_combo.currentText() == "True"
779
+ export_data['val'] = self.val_combo.currentText() == "True"
780
+ export_data['verbose'] = self.verbose_combo.currentText() == "True"
781
+ export_data['optimizer'] = self.optimizer_combo.currentText()
798
782
 
799
783
  # Custom parameters
800
784
  for param_info in self.custom_params:
801
- param_name, param_value, param_type = param_info
802
- name = param_name.text().strip()
803
- value = param_value.text().strip()
804
- type_name = param_type.currentText()
785
+ param_name_widget, param_value_widget, param_type_widget = param_info
786
+ name = param_name_widget.text().strip()
787
+ value_str = param_value_widget.text().strip()
788
+ type_name = param_type_widget.currentText()
805
789
 
806
- if name and value:
807
- export_data['types'][name] = type_name
808
-
809
- if type_name == "bool":
810
- export_data['parameters'][name] = value.lower() == "true"
811
- elif type_name == "int":
812
- try:
813
- export_data['parameters'][name] = int(value)
814
- except ValueError:
815
- export_data['parameters'][name] = value
816
- export_data['types'][name] = "string" # Fallback to string
817
- elif type_name == "float":
818
- try:
819
- export_data['parameters'][name] = float(value)
820
- except ValueError:
821
- export_data['parameters'][name] = value
822
- export_data['types'][name] = "string" # Fallback to string
823
- else: # string type
824
- export_data['parameters'][name] = value
825
-
826
- # Write to YAML file
790
+ if name and value_str:
791
+ # Convert value to the correct type before exporting
792
+ try:
793
+ if type_name == "bool":
794
+ value = value_str.lower() == "true"
795
+ elif type_name == "int":
796
+ value = int(value_str)
797
+ elif type_name == "float":
798
+ value = float(value_str)
799
+ else: # string type
800
+ value = value_str
801
+ export_data[name] = value
802
+ except ValueError:
803
+ # If conversion fails, save it as a string
804
+ print(f"Warning: Could not convert '{value_str}' to {type_name} for parameter '{name}'. Saving as string.")
805
+ export_data[name] = value_str
806
+
807
+ # Write the flat dictionary to the YAML file
827
808
  with open(file_path, 'w') as f:
828
- yaml.dump(export_data, f, default_flow_style=False, indent=2)
809
+ yaml.dump(export_data, f, default_flow_style=False, sort_keys=False, indent=2)
829
810
 
830
811
  QMessageBox.information(self,
831
812
  "Export Success",
832
- "Parameters successfully exported")
813
+ "Parameters successfully exported.")
833
814
 
834
815
  except Exception as e:
835
816
  QMessageBox.critical(self,
@@ -448,7 +448,7 @@ class AnnotationWindow(QGraphicsView):
448
448
  # Center the view on the work area's center
449
449
  self.centerOn(work_area_center)
450
450
 
451
- def center_on_annotation(self, annotation):
451
+ def just_center_on_annotation(self, annotation):
452
452
  """Center the view on the specified annotation."""
453
453
  # Create graphics item if it doesn't exist
454
454
  if not annotation.graphics_item:
@@ -460,6 +460,33 @@ class AnnotationWindow(QGraphicsView):
460
460
 
461
461
  # Center the view on the annotation's center
462
462
  self.centerOn(annotation_center)
463
+
464
+ def center_on_annotation(self, annotation):
465
+ """Center and zoom in to focus on the specified annotation."""
466
+ # Create graphics item if it doesn't exist
467
+ if not annotation.graphics_item:
468
+ annotation.create_graphics_item(self.scene)
469
+
470
+ # Get the bounding rect of the annotation in scene coordinates
471
+ annotation_rect = annotation.graphics_item.boundingRect()
472
+
473
+ # Add some padding around the annotation (20% on each side)
474
+ padding_x = annotation_rect.width() * 0.2
475
+ padding_y = annotation_rect.height() * 0.2
476
+ padded_rect = annotation_rect.adjusted(-padding_x, -padding_y, padding_x, padding_y)
477
+
478
+ # Fit the padded annotation rect in the view
479
+ self.fitInView(padded_rect, Qt.KeepAspectRatio)
480
+
481
+ # Update the zoom factor based on the new view transformation
482
+ # We can calculate this by comparing the viewport size to the scene rect size
483
+ view_rect = self.viewport().rect()
484
+ zoom_x = view_rect.width() / padded_rect.width()
485
+ zoom_y = view_rect.height() / padded_rect.height()
486
+ self.zoom_factor = min(zoom_x, zoom_y)
487
+
488
+ # Signal that the view has changed
489
+ self.viewChanged.emit(*self.get_image_dimensions())
463
490
 
464
491
  def cycle_annotations(self, direction):
465
492
  """Cycle through annotations in the specified direction."""
@@ -1398,6 +1398,9 @@ class MainWindow(QMainWindow):
1398
1398
 
1399
1399
  def update_label_transparency(self, value):
1400
1400
  """Update the label transparency value in LabelWindow, AnnotationWindow and the Slider"""
1401
+ if self.explorer_window:
1402
+ return # Do not update transparency if explorer window is open
1403
+
1401
1404
  if self.all_labels_button.isChecked():
1402
1405
  # Set transparency for all labels in LabelWindow, AnnotationWindow
1403
1406
  self.label_window.set_all_labels_transparency(value)
@@ -1677,6 +1680,9 @@ class MainWindow(QMainWindow):
1677
1680
 
1678
1681
  try:
1679
1682
  self.untoggle_all_tools()
1683
+ # Set the transparency value ahead of time
1684
+ self.update_transparency_slider(0)
1685
+
1680
1686
  # Recreate the explorer window, passing the main window instance
1681
1687
  self.explorer_window = ExplorerWindow(self)
1682
1688
 
@@ -1688,8 +1694,7 @@ class MainWindow(QMainWindow):
1688
1694
  # Disable all main window widgets except select few
1689
1695
  self.set_main_window_enabled_state(
1690
1696
  enable_list=[self.annotation_window,
1691
- self.label_window,
1692
- self.transparency_widget],
1697
+ self.label_window],
1693
1698
  disable_list=[self.toolbar,
1694
1699
  self.menu_bar,
1695
1700
  self.image_window,
@@ -1708,7 +1713,7 @@ class MainWindow(QMainWindow):
1708
1713
  self.explorer_window = None
1709
1714
  # Re-enable everything if there was an error
1710
1715
  self.set_main_window_enabled_state()
1711
-
1716
+
1712
1717
  def explorer_closed(self):
1713
1718
  """Handle the explorer window being closed."""
1714
1719
  if self.explorer_window:
@@ -1,6 +1,6 @@
1
1
  """Top-level package for CoralNet-Toolbox."""
2
2
 
3
- __version__ = "0.0.69"
3
+ __version__ = "0.0.71"
4
4
  __author__ = "Jordan Pierce"
5
5
  __email__ = "jordan.pierce@noaa.gov"
6
6
  __credits__ = "National Center for Coastal and Ocean Sciences (NCCOS)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coralnet-toolbox
3
- Version: 0.0.69
3
+ Version: 0.0.71
4
4
  Summary: Tools for annotating and developing ML models for benthic imagery
5
5
  Author-email: Jordan Pierce <jordan.pierce@noaa.gov>
6
6
  License: MIT License
@@ -94,10 +94,12 @@ coralnet-toolbox
94
94
 
95
95
  ## 📚 Guides
96
96
 
97
- For further instructions please see the following guides:
97
+ For further information and instructions, please see the following guides:
98
+ - [Overview](https://jordan-pierce.github.io/CoralNet-Toolbox/overview)
98
99
  - [Installation](https://jordan-pierce.github.io/CoralNet-Toolbox/installation)
99
100
  - [Usage](https://jordan-pierce.github.io/CoralNet-Toolbox/usage)
100
- - [Patch-based Image Classifier](https://jordan-pierce.github.io/CoralNet-Toolbox/classify)
101
+ - Community Provided:
102
+ - [Patch-based Image Classifier](https://jordan-pierce.github.io/CoralNet-Toolbox/classify)
101
103
 
102
104
  <details open>
103
105
  <summary><h2><b>🎥 Watch the Video Demos</b></h2></summary>
@@ -177,7 +179,7 @@ Enhance your CoralNet experience with these tools:
177
179
  - 🚀 Optimize: Productionize models for faster inferencing
178
180
  - ⚙️ Batch Inference: Perform predictions on multiple images, automatically
179
181
  - 🎞️ Video Inference: Perform predictions on a video in real-time, record the output and analytics
180
- - 🔮 Explorer: Cluster, view, and re-label annotations using embeddings, mapped from feature-space
182
+ - 🔮 [Explorer](https://youtu.be/68eZt5l_7nA): Cluster, view, and re-label annotations using embeddings, mapped from feature-space
181
183
  - ↔️ I/O: Import and Export annotations from / to CoralNet, Viscore, and TagLab
182
184
  - Export annotations as [GeoJSONs](https://datatracker.ietf.org/doc/html/rfc7946), segmentation masks
183
185
  - 📸 YOLO: Import and Export YOLO datasets for machine learning
@@ -1,16 +1,16 @@
1
- coralnet_toolbox/QtAnnotationWindow.py,sha256=ohYkvUdUAdNmZJXqKtCGnnXQ3cPtnSkFEtwH_mWnn3c,37808
1
+ coralnet_toolbox/QtAnnotationWindow.py,sha256=3pGy_81qaOYOrD_356DSiyXzmuoQnHl5GCp3o-9ay48,39096
2
2
  coralnet_toolbox/QtConfidenceWindow.py,sha256=L5hR23uW91GpqnsNS9R1XF3zCTe2aU7w0iDoQMV0oyE,16190
3
3
  coralnet_toolbox/QtEventFilter.py,sha256=KKC9de3e66PvGVgiML8P7MZ9-r7vvHidPJJYpcbTwyM,6696
4
4
  coralnet_toolbox/QtImageWindow.py,sha256=vLziMSEWFfVRSBN0nUNkosgk3LiNxZDqPwbinz9ZivQ,49356
5
5
  coralnet_toolbox/QtLabelWindow.py,sha256=-4GCk4pTY9g4ADH1iE__4xwqT-7UR_7VCT8v-bJzerk,50869
6
- coralnet_toolbox/QtMainWindow.py,sha256=r4pNozDqpaoyB1veiPLbUlbdacAwyHQQVHEy8kqJAS0,112034
6
+ coralnet_toolbox/QtMainWindow.py,sha256=W86kc-ZFmZHcSKcNTWN5RFCvt7sysvTCYZrPWZPWd6E,112220
7
7
  coralnet_toolbox/QtPatchSampling.py,sha256=Ehj06auBGfQwIruLNYQjF8eFOCpl8G72p42UXXb2mUo,29013
8
8
  coralnet_toolbox/QtProgressBar.py,sha256=pnozUOcVjfO_yTS9z8wOMPcrrrOtG_FeCknTcdI6eyk,6250
9
9
  coralnet_toolbox/QtWorkArea.py,sha256=YXRvHQKpWUtWyv_o9lZ8rmxfm28dUOG9pmMUeimDhQ4,13578
10
- coralnet_toolbox/__init__.py,sha256=ShjZP-Gm_sxpQicWuT6DKcnGnAt8YWuUH08qUGKAPbU,207
10
+ coralnet_toolbox/__init__.py,sha256=K-BGYPTYU1ySXgZWS4vmpHfRksFJb3SAyX73veK1hdE,207
11
11
  coralnet_toolbox/main.py,sha256=6j2B_1reC_KDmqvq1C0fB-UeSEm8eeJOozp2f4XXMLQ,1573
12
12
  coralnet_toolbox/utilities.py,sha256=eUkxXuWaNFH83LSW-KniwujkXKJ2rK04czx3k3OPiAY,27115
13
- coralnet_toolbox/Annotations/QtAnnotation.py,sha256=I2Givo3p92gfyVYnnFHBjxso-hmQCCXM531Pygiebfw,29242
13
+ coralnet_toolbox/Annotations/QtAnnotation.py,sha256=-3ASbjl1dXw9U731vyCgwyiyZT9zOD5Mvp1jt-7bCnA,29242
14
14
  coralnet_toolbox/Annotations/QtMultiPolygonAnnotation.py,sha256=ErAT31gw-zhEVNxkPRpyB9uw-NSpPh-ShCBxpscXdRw,15579
15
15
  coralnet_toolbox/Annotations/QtPatchAnnotation.py,sha256=67fNnK_-muyhGZdGB0kBDx-JGuflv1TM6q5ikfW_zOk,20076
16
16
  coralnet_toolbox/Annotations/QtPolygonAnnotation.py,sha256=1EkZEJlO4VZ4so01Sat2T8LeO1LNs7HbGJLO-G2_73Q,26886
@@ -36,7 +36,7 @@ coralnet_toolbox/CoralNet/QtAuthenticate.py,sha256=Y__iY0Kcosz6AOV7dlJBwiB6Hte40
36
36
  coralnet_toolbox/CoralNet/QtDownload.py,sha256=HBb8TpZRIEFirGIaIAV1v8qg3fL4cP6Bf-hUiqXoiLE,48516
37
37
  coralnet_toolbox/CoralNet/__init__.py,sha256=ILkAZh6mlAK1UaCCZjCB9JZxd-oY4cIgfnIC8UgjjIU,188
38
38
  coralnet_toolbox/Explorer/QtDataItem.py,sha256=-O2Tneh9wYbAZarqwb_Cvy5cP1F_zQH2IAw9c-rHy1Y,13572
39
- coralnet_toolbox/Explorer/QtExplorer.py,sha256=rfJr29z3Nduh_NXxVJ-BZCLRxpfXhfeAuAae-2uV878,115844
39
+ coralnet_toolbox/Explorer/QtExplorer.py,sha256=IATLlZi57m5XU1KIG-oGNGqhcZEpjxQeilmKGrKMIFo,122968
40
40
  coralnet_toolbox/Explorer/QtFeatureStore.py,sha256=kMn--vuBed6wZS-BQhHt_KBA5z-tL1ydFgFkkIoGiB4,6742
41
41
  coralnet_toolbox/Explorer/QtSettingsWidgets.py,sha256=hIMj2lzqGKBoFWKYolH7bEPm5ePAIzYcGjYRQv2uWFE,27656
42
42
  coralnet_toolbox/Explorer/__init__.py,sha256=wZPhf2oaUUyIQ2WK48Aj-4q1ENIZG2dGl1HF_mjhI6w,116
@@ -89,6 +89,7 @@ coralnet_toolbox/Icons/rocket.png,sha256=iMlRGlrNBS_dNBD2XIpN4RSrphCGbw_Ds1AYJ01
89
89
  coralnet_toolbox/Icons/select.png,sha256=twnMIO9ylQYjvyGnAR28V6K3ds6xpArZQTrvf0uxS6g,1896
90
90
  coralnet_toolbox/Icons/settings.png,sha256=rklROt3oKrfEk_qwN9J-JwvKok08iOkZy3OD4oNsLJQ,1376
91
91
  coralnet_toolbox/Icons/snake.png,sha256=cwcekSkXwDi_fhtTU48u7FN4bIybbY53cWK0n7-IN9A,2361
92
+ coralnet_toolbox/Icons/target.png,sha256=jzb-S_sXWT8MfbvefhDNsuTdAZgV2nGf1ieawaCkByM,1702
92
93
  coralnet_toolbox/Icons/tile.png,sha256=WiXKBpWVBfPv7gC8dnkc_gW3wuLQmLUyxYMWEM-G9ZU,382
93
94
  coralnet_toolbox/Icons/transparent.png,sha256=ZkuGkVzh6zLVNau1Wj166-TtUlbCRqJObGt4vxMxnLk,1098
94
95
  coralnet_toolbox/Icons/turtle.png,sha256=55OG5atmEs8nIUiN2B5hW-Jx1fpuY9QI-zolQoUOKWw,1971
@@ -147,7 +148,7 @@ coralnet_toolbox/MachineLearning/MergeDatasets/QtClassify.py,sha256=FI4WdxJ4-vtn
147
148
  coralnet_toolbox/MachineLearning/MergeDatasets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
148
149
  coralnet_toolbox/MachineLearning/OptimizeModel/QtBase.py,sha256=06irheL8aKvtwKBQLLJUohvWvrMqKFC-jhEEoVqIYdg,8890
149
150
  coralnet_toolbox/MachineLearning/OptimizeModel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
- coralnet_toolbox/MachineLearning/TrainModel/QtBase.py,sha256=lLGmgDSvjQFYD2RdF7eC6U5xU5in2rC26RQ3lmKvfLY,37460
151
+ coralnet_toolbox/MachineLearning/TrainModel/QtBase.py,sha256=f__pgcG8yyV4IQbICED4q4YWO6GnNtD5b5UGpWuv4c0,36580
151
152
  coralnet_toolbox/MachineLearning/TrainModel/QtClassify.py,sha256=ss5ppGbrpULzUPmeRmfqZjiqZPp7XbdUZ4BzSX0ehu0,3267
152
153
  coralnet_toolbox/MachineLearning/TrainModel/QtDetect.py,sha256=uxopZkrNkl3tImMNSDwC2ENpFAxdG0NLiwRwqNnbep0,4467
153
154
  coralnet_toolbox/MachineLearning/TrainModel/QtSegment.py,sha256=y8bpNS24SQxyg967RSi6TraqHSmlJYj8kbvC_5HMBIM,3597
@@ -215,9 +216,9 @@ coralnet_toolbox/Tools/QtTool.py,sha256=2MCjT151gYBN8KbsK0GX4WOrEg1uw3oeSkp7Elw1
215
216
  coralnet_toolbox/Tools/QtWorkAreaTool.py,sha256=-CDrEPenOdSI3sf5wn19Cip4alE1ef7WsRDxQFDkHlc,22162
216
217
  coralnet_toolbox/Tools/QtZoomTool.py,sha256=F9CAoABv1jxcUS7dyIh1FYjgjOXYRI1xtBPNIR1g62o,4041
217
218
  coralnet_toolbox/Tools/__init__.py,sha256=218iQ8IFXIkKXiUDVYtXk9e08UY9-LhHjcryaJAanQ0,797
218
- coralnet_toolbox-0.0.69.dist-info/licenses/LICENSE.txt,sha256=AURacZ_G_PZKqqPQ9VB9Sqegblk67RNgWSGAYKwXXMY,521
219
- coralnet_toolbox-0.0.69.dist-info/METADATA,sha256=tEMfVdY4gbof1mKK4dlo5JyGWtTInnpiP83p9suRO34,18007
220
- coralnet_toolbox-0.0.69.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
221
- coralnet_toolbox-0.0.69.dist-info/entry_points.txt,sha256=oEeMoDlJ_2lq95quOeDHIx9hZpubUlSo80OLtgbcrbM,63
222
- coralnet_toolbox-0.0.69.dist-info/top_level.txt,sha256=SMWPh4_9JfB8zVpPOOvjucV2_B_hvWW7bNWmMjG0LsY,17
223
- coralnet_toolbox-0.0.69.dist-info/RECORD,,
219
+ coralnet_toolbox-0.0.71.dist-info/licenses/LICENSE.txt,sha256=AURacZ_G_PZKqqPQ9VB9Sqegblk67RNgWSGAYKwXXMY,521
220
+ coralnet_toolbox-0.0.71.dist-info/METADATA,sha256=LltT45S41MrUplIQGHLJ_1rjuFT8PccNnbRCUw2ODRQ,18152
221
+ coralnet_toolbox-0.0.71.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
222
+ coralnet_toolbox-0.0.71.dist-info/entry_points.txt,sha256=oEeMoDlJ_2lq95quOeDHIx9hZpubUlSo80OLtgbcrbM,63
223
+ coralnet_toolbox-0.0.71.dist-info/top_level.txt,sha256=SMWPh4_9JfB8zVpPOOvjucV2_B_hvWW7bNWmMjG0LsY,17
224
+ coralnet_toolbox-0.0.71.dist-info/RECORD,,