coralnet-toolbox 0.0.74__py2.py3-none-any.whl → 0.0.75__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 (25) hide show
  1. coralnet_toolbox/Explorer/QtDataItem.py +52 -22
  2. coralnet_toolbox/Explorer/QtExplorer.py +277 -1600
  3. coralnet_toolbox/Explorer/QtSettingsWidgets.py +101 -15
  4. coralnet_toolbox/Explorer/QtViewers.py +1568 -0
  5. coralnet_toolbox/Explorer/transformer_models.py +59 -0
  6. coralnet_toolbox/Explorer/yolo_models.py +112 -0
  7. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +239 -147
  8. coralnet_toolbox/MachineLearning/VideoInference/YOLO3D/run.py +102 -16
  9. coralnet_toolbox/QtAnnotationWindow.py +16 -10
  10. coralnet_toolbox/QtImageWindow.py +3 -7
  11. coralnet_toolbox/Rasters/RasterTableModel.py +20 -0
  12. coralnet_toolbox/SAM/QtDeployGenerator.py +1 -4
  13. coralnet_toolbox/SAM/QtDeployPredictor.py +1 -3
  14. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +131 -106
  15. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +45 -3
  16. coralnet_toolbox/Tools/QtPolygonTool.py +42 -3
  17. coralnet_toolbox/Tools/QtRectangleTool.py +30 -0
  18. coralnet_toolbox/__init__.py +1 -1
  19. coralnet_toolbox/utilities.py +21 -0
  20. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/METADATA +6 -3
  21. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/RECORD +25 -22
  22. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/WHEEL +0 -0
  23. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/entry_points.txt +0 -0
  24. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/licenses/LICENSE.txt +0 -0
  25. {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/top_level.txt +0 -0
@@ -416,7 +416,7 @@ class DeployGeneratorDialog(QDialog):
416
416
 
417
417
  # Image size control
418
418
  self.imgsz_spinbox = QSpinBox()
419
- self.imgsz_spinbox.setRange(512, 65536)
419
+ self.imgsz_spinbox.setRange(1024, 65536)
420
420
  self.imgsz_spinbox.setSingleStep(1024)
421
421
  self.imgsz_spinbox.setValue(self.imgsz)
422
422
  layout.addRow("Image Size (imgsz):", self.imgsz_spinbox)
@@ -502,8 +502,13 @@ class DeployGeneratorDialog(QDialog):
502
502
 
503
503
  main_layout.addLayout(button_row)
504
504
 
505
- # Second row: Save VPE button + Show VPE button side by side
505
+ # Second row: VPE action buttons
506
506
  vpe_row = QHBoxLayout()
507
+
508
+ generate_vpe_button = QPushButton("Generate VPEs")
509
+ generate_vpe_button.clicked.connect(self.generate_vpes_from_references)
510
+ vpe_row.addWidget(generate_vpe_button)
511
+
507
512
  save_vpe_button = QPushButton("Save VPE")
508
513
  save_vpe_button.clicked.connect(self.save_vpe)
509
514
  vpe_row.addWidget(save_vpe_button)
@@ -810,80 +815,75 @@ class DeployGeneratorDialog(QDialog):
810
815
 
811
816
  def save_vpe(self):
812
817
  """
813
- Save the combined collection of VPEs (imported and reference-generated) to disk.
818
+ Saves the combined collection of VPEs (imported and pre-generated from references) to disk.
814
819
  """
815
- # Always sync with the live UI selection before saving.
816
- self.update_stashed_references_from_ui()
817
-
818
- # Create a list to hold all VPEs
819
- all_vpes = []
820
-
821
- # Add imported VPEs if available
822
- if self.imported_vpes:
823
- all_vpes.extend(self.imported_vpes)
820
+ QApplication.setOverrideCursor(Qt.WaitCursor)
824
821
 
825
- # Check if we should generate new VPEs from reference images
826
- references_dict = self._get_references()
827
- if references_dict:
828
- # Reload the model to ensure clean state
829
- self.reload_model()
822
+ try:
823
+ # Create a list to hold all VPEs to be saved
824
+ all_vpes = []
830
825
 
831
- # Convert references to VPEs without updating self.reference_vpes yet
832
- new_vpes = self.references_to_vpe(references_dict, update_reference_vpes=False)
826
+ # Add imported VPEs if available
827
+ if self.imported_vpes:
828
+ all_vpes.extend(self.imported_vpes)
833
829
 
834
- if new_vpes:
835
- # Add new VPEs to collection
836
- all_vpes.extend(new_vpes)
837
- # Update reference_vpes with the new ones
838
- self.reference_vpes = new_vpes
839
- else:
840
- # Include existing reference VPEs if we have them
830
+ # Add pre-generated reference VPEs if available
841
831
  if self.reference_vpes:
842
832
  all_vpes.extend(self.reference_vpes)
843
-
844
- # Check if we have any VPEs to save
845
- if not all_vpes:
846
- QMessageBox.warning(
833
+
834
+ # Check if we have any VPEs to save
835
+ if not all_vpes:
836
+ QApplication.restoreOverrideCursor()
837
+ QMessageBox.warning(
838
+ self,
839
+ "No VPEs Available",
840
+ "No VPEs available to save. "
841
+ "Please either load a VPE file or generate VPEs from reference images first."
842
+ )
843
+ return
844
+
845
+ QApplication.restoreOverrideCursor()
846
+
847
+ file_path, _ = QFileDialog.getSaveFileName(
847
848
  self,
848
- "No VPEs Available",
849
- "No VPEs available to save. Please either load a VPE file or select reference images."
849
+ "Save VPE Collection",
850
+ "",
851
+ "PyTorch Tensor (*.pt);;All Files (*)"
850
852
  )
851
- return
852
-
853
- # Open file dialog to select save location
854
- file_path, _ = QFileDialog.getSaveFileName(
855
- self,
856
- "Save VPE Collection",
857
- "",
858
- "PyTorch Tensor (*.pt);;All Files (*)"
859
- )
860
-
861
- if not file_path:
862
- return # User canceled the dialog
863
-
864
- # Add .pt extension if not present
865
- if not file_path.endswith('.pt'):
866
- file_path += '.pt'
867
-
868
- try:
869
- # Move tensors to CPU before saving
853
+
854
+ if not file_path:
855
+ return
856
+
857
+ QApplication.setOverrideCursor(Qt.WaitCursor)
858
+
859
+ if not file_path.endswith('.pt'):
860
+ file_path += '.pt'
861
+
870
862
  vpe_list_cpu = [vpe.cpu() for vpe in all_vpes]
871
863
 
872
- # Save the list of tensors
873
864
  torch.save(vpe_list_cpu, file_path)
874
865
 
875
866
  self.status_bar.setText(f"Saved {len(all_vpes)} VPE tensors to {os.path.basename(file_path)}")
867
+
868
+ QApplication.restoreOverrideCursor()
876
869
  QMessageBox.information(
877
870
  self,
878
871
  "VPE Saved",
879
872
  f"Saved {len(all_vpes)} VPE tensors to {file_path}"
880
873
  )
874
+
881
875
  except Exception as e:
876
+ QApplication.restoreOverrideCursor()
882
877
  QMessageBox.critical(
883
878
  self,
884
879
  "Error Saving VPE",
885
880
  f"Failed to save VPE: {str(e)}"
886
881
  )
882
+ finally:
883
+ try:
884
+ QApplication.restoreOverrideCursor()
885
+ except:
886
+ pass
887
887
 
888
888
  def load_model(self):
889
889
  """
@@ -1156,6 +1156,59 @@ class DeployGeneratorDialog(QDialog):
1156
1156
 
1157
1157
  return [[combined_results]]
1158
1158
 
1159
+ def generate_vpes_from_references(self):
1160
+ """
1161
+ Calculates VPEs from the currently highlighted reference images and
1162
+ stores them in self.reference_vpes, overwriting any previous ones.
1163
+ """
1164
+ if not self.loaded_model:
1165
+ QMessageBox.warning(self, "No Model Loaded", "A model must be loaded before generating VPEs.")
1166
+ return
1167
+
1168
+ # Always sync with the live UI selection before generating.
1169
+ self.update_stashed_references_from_ui()
1170
+ references_dict = self._get_references()
1171
+
1172
+ if not references_dict:
1173
+ QMessageBox.information(
1174
+ self,
1175
+ "No References Selected",
1176
+ "Please highlight one or more reference images in the table to generate VPEs."
1177
+ )
1178
+ return
1179
+
1180
+ QApplication.setOverrideCursor(Qt.WaitCursor)
1181
+ progress_bar = ProgressBar(self, title="Generating VPEs")
1182
+ progress_bar.show()
1183
+
1184
+ try:
1185
+ # Make progress bar busy
1186
+ progress_bar.set_busy_mode("Generating VPEs...")
1187
+ # Reload the model to ensure a clean state for VPE generation
1188
+ self.reload_model()
1189
+
1190
+ # The references_to_vpe method will calculate and update self.reference_vpes
1191
+ new_vpes = self.references_to_vpe(references_dict, update_reference_vpes=True)
1192
+
1193
+ if new_vpes:
1194
+ num_vpes = len(new_vpes)
1195
+ num_images = len(references_dict)
1196
+ message = f"Successfully generated {num_vpes} VPEs from {num_images} reference image(s)."
1197
+ self.status_bar.setText(message)
1198
+ QMessageBox.information(self, "VPEs Generated", message)
1199
+ else:
1200
+ message = "Could not generate VPEs. Ensure annotations are valid."
1201
+ self.status_bar.setText(message)
1202
+ QMessageBox.warning(self, "Generation Failed", message)
1203
+
1204
+ except Exception as e:
1205
+ QMessageBox.critical(self, "Error Generating VPEs", f"An unexpected error occurred: {str(e)}")
1206
+ self.status_bar.setText("Error during VPE generation.")
1207
+ finally:
1208
+ QApplication.restoreOverrideCursor()
1209
+ progress_bar.stop_progress()
1210
+ progress_bar.close()
1211
+
1159
1212
  def references_to_vpe(self, reference_dict, update_reference_vpes=True):
1160
1213
  """
1161
1214
  Converts the contents of a reference dictionary to VPEs (Visual Prompt Embeddings).
@@ -1197,14 +1250,13 @@ class DeployGeneratorDialog(QDialog):
1197
1250
 
1198
1251
  return vpe_list
1199
1252
 
1200
- def _apply_model_using_vpe(self, inputs, references_dict):
1253
+ def _apply_model_using_vpe(self, inputs):
1201
1254
  """
1202
- Apply the model to the inputs using combined VPEs from both imported files
1203
- and reference annotations.
1255
+ Apply the model to the inputs using pre-calculated VPEs from imported files
1256
+ and/or generated from reference annotations.
1204
1257
 
1205
1258
  Args:
1206
1259
  inputs (list): List of input images.
1207
- references_dict (dict): Dictionary containing reference annotations for each image.
1208
1260
 
1209
1261
  Returns:
1210
1262
  list: List of prediction results.
@@ -1219,23 +1271,17 @@ class DeployGeneratorDialog(QDialog):
1219
1271
  if self.imported_vpes:
1220
1272
  combined_vpes.extend(self.imported_vpes)
1221
1273
 
1222
- # Process reference images to VPEs if any exist
1223
- if references_dict:
1224
- # Only update reference_vpes if references_dict is not empty
1225
- reference_vpes = self.references_to_vpe(references_dict, update_reference_vpes=True)
1226
- if reference_vpes:
1227
- combined_vpes.extend(reference_vpes)
1228
- else:
1229
- # Use existing reference_vpes if we have them
1230
- if self.reference_vpes:
1231
- combined_vpes.extend(self.reference_vpes)
1274
+ # Add pre-generated reference VPEs if available
1275
+ if self.reference_vpes:
1276
+ combined_vpes.extend(self.reference_vpes)
1232
1277
 
1233
1278
  # Check if we have any VPEs to use
1234
1279
  if not combined_vpes:
1235
1280
  QMessageBox.warning(
1236
1281
  self,
1237
1282
  "No VPEs Available",
1238
- "No VPEs available for prediction. Please either load a VPE file or select reference images."
1283
+ "No VPEs are available for prediction. "
1284
+ "Please either load a VPE file or generate VPEs from reference images."
1239
1285
  )
1240
1286
  return []
1241
1287
 
@@ -1260,7 +1306,7 @@ class DeployGeneratorDialog(QDialog):
1260
1306
  retina_masks=self.task == "segment")
1261
1307
 
1262
1308
  return [results]
1263
-
1309
+
1264
1310
  def _apply_model(self, inputs):
1265
1311
  """
1266
1312
  Apply the model to the target inputs. This method handles both image-based
@@ -1278,21 +1324,9 @@ class DeployGeneratorDialog(QDialog):
1278
1324
 
1279
1325
  # Check if the user is using VPE or Reference Images
1280
1326
  if self.reference_method_combo_box.currentText() == "VPE":
1281
- # Check if we have any VPEs available (imported or reference-generated)
1282
- has_vpes = bool(self.imported_vpes or self.reference_vpes)
1283
-
1284
- # If we have reference images selected but no imported VPEs yet,
1285
- # warn the user only if we also don't have any reference images
1286
- if not has_vpes and not references_dict:
1287
- QMessageBox.warning(
1288
- self,
1289
- "No VPEs Available",
1290
- "No VPEs available for prediction. Please either load a VPE file or select reference images."
1291
- )
1292
- return []
1293
-
1294
- # Use the VPE method, which will combine imported and reference VPEs
1295
- results = self._apply_model_using_vpe(inputs, references_dict)
1327
+ # The VPE method will use pre-loaded/pre-generated VPEs.
1328
+ # The internal checks for whether any VPEs exist are now inside _apply_model_using_vpe.
1329
+ results = self._apply_model_using_vpe(inputs)
1296
1330
  else:
1297
1331
  # Use Reference Images method - requires reference images
1298
1332
  if not references_dict:
@@ -1405,16 +1439,9 @@ class DeployGeneratorDialog(QDialog):
1405
1439
 
1406
1440
  def show_vpe(self):
1407
1441
  """
1408
- Show a visualization of the VPEs using PyQtGraph.
1409
- This method now always recalculates VPEs from the currently highlighted reference images.
1442
+ Show a visualization of the currently stored VPEs using PyQtGraph.
1410
1443
  """
1411
- # Set cursor to busy while loading VPEs
1412
- QApplication.setOverrideCursor(Qt.WaitCursor)
1413
-
1414
1444
  try:
1415
- # Always sync with the live UI selection before visualizing.
1416
- self.update_stashed_references_from_ui()
1417
-
1418
1445
  vpes_with_source = []
1419
1446
 
1420
1447
  # 1. Add any VPEs that were loaded from a file
@@ -1422,37 +1449,35 @@ class DeployGeneratorDialog(QDialog):
1422
1449
  for vpe in self.imported_vpes:
1423
1450
  vpes_with_source.append((vpe, "Import"))
1424
1451
 
1425
- # 2. Get the currently selected reference images from the stashed list
1426
- references_dict = self._get_references()
1427
-
1428
- # 3. If there are reference images, calculate their VPEs and add with source type
1429
- if references_dict:
1430
- self.reload_model()
1431
- new_reference_vpes = self.references_to_vpe(references_dict, update_reference_vpes=True)
1432
- if new_reference_vpes:
1433
- for vpe in new_reference_vpes:
1434
- vpes_with_source.append((vpe, "Reference"))
1452
+ # 2. Add any pre-generated VPEs from reference images
1453
+ if self.reference_vpes:
1454
+ for vpe in self.reference_vpes:
1455
+ vpes_with_source.append((vpe, "Reference"))
1435
1456
 
1436
- # 4. Check if there is anything to visualize
1457
+ # 3. Check if there is anything to visualize
1437
1458
  if not vpes_with_source:
1438
1459
  QMessageBox.warning(
1439
1460
  self,
1440
1461
  "No VPEs Available",
1441
- "No VPEs available to visualize. Please either load a VPE file or select reference images."
1462
+ "No VPEs available to visualize. Please load a VPE file or generate VPEs from references first."
1442
1463
  )
1443
1464
  return
1444
1465
 
1445
- # 5. Create the visualization dialog, passing the list of tuples
1466
+ # 4. Create the visualization dialog
1446
1467
  all_vpe_tensors = [vpe for vpe, source in vpes_with_source]
1447
1468
  averaged_vpe = torch.cat(all_vpe_tensors).mean(dim=0, keepdim=True)
1448
1469
  final_vpe = torch.nn.functional.normalize(averaged_vpe, p=2, dim=-1)
1449
1470
 
1471
+ QApplication.setOverrideCursor(Qt.WaitCursor)
1472
+
1450
1473
  dialog = VPEVisualizationDialog(vpes_with_source, final_vpe, self)
1474
+ QApplication.restoreOverrideCursor()
1475
+
1451
1476
  dialog.exec_()
1452
1477
 
1453
- finally:
1454
- # Always restore cursor, even if an exception occurs
1478
+ except Exception as e:
1455
1479
  QApplication.restoreOverrideCursor()
1480
+ QMessageBox.critical(self, "Error Visualizing VPE", f"An error occurred: {str(e)}")
1456
1481
 
1457
1482
  def deactivate_model(self):
1458
1483
  """
@@ -171,7 +171,7 @@ class DeployPredictorDialog(QDialog):
171
171
  # Image size control
172
172
  self.imgsz_spinbox = QSpinBox()
173
173
  self.imgsz_spinbox.setRange(512, 65536)
174
- self.imgsz_spinbox.setSingleStep(256)
174
+ self.imgsz_spinbox.setSingleStep(1024)
175
175
  self.imgsz_spinbox.setValue(self.imgsz)
176
176
  layout.addRow("Image Size (imgsz)", self.imgsz_spinbox)
177
177
 
@@ -322,6 +322,7 @@ class DeployPredictorDialog(QDialog):
322
322
  def is_sam_model_deployed(self):
323
323
  """
324
324
  Check if the SAM model is deployed and update the checkbox state accordingly.
325
+ If SAM is enabled for polygons, sync and disable the imgsz spinbox.
325
326
 
326
327
  :return: Boolean indicating whether the SAM model is deployed
327
328
  """
@@ -334,9 +335,48 @@ class DeployPredictorDialog(QDialog):
334
335
  self.use_sam_dropdown.setCurrentText("False")
335
336
  QMessageBox.critical(self, "Error", "Please deploy the SAM model first.")
336
337
  return False
338
+
339
+ # Check if SAM polygons are enabled
340
+ if self.use_sam_dropdown.currentText() == "True":
341
+ # Sync the imgsz spinbox with SAM's value
342
+ self.imgsz_spinbox.setValue(self.sam_dialog.imgsz_spinbox.value())
343
+ # Disable the spinbox
344
+ self.imgsz_spinbox.setEnabled(False)
345
+
346
+ # Connect SAM's imgsz_spinbox valueChanged signal to update our value
347
+ # First disconnect any existing connection to avoid duplicates
348
+ try:
349
+ self.sam_dialog.imgsz_spinbox.valueChanged.disconnect(self.update_from_sam_imgsz)
350
+ except TypeError:
351
+ # No connection exists yet
352
+ pass
353
+
354
+ # Connect the signal
355
+ self.sam_dialog.imgsz_spinbox.valueChanged.connect(self.update_from_sam_imgsz)
356
+ else:
357
+ # Re-enable the spinbox when SAM polygons are disabled
358
+ self.imgsz_spinbox.setEnabled(True)
359
+
360
+ # Disconnect the signal when SAM is disabled
361
+ try:
362
+ self.sam_dialog.imgsz_spinbox.valueChanged.disconnect(self.update_from_sam_imgsz)
363
+ except TypeError:
364
+ # No connection exists
365
+ pass
337
366
 
338
367
  return True
339
368
 
369
+ def update_from_sam_imgsz(self, value):
370
+ """
371
+ Update the SeeAnything image size when SAM's image size changes.
372
+ Only takes effect when SAM polygons are enabled.
373
+
374
+ Args:
375
+ value (int): The new image size value from SAM dialog
376
+ """
377
+ if self.use_sam_dropdown.currentText() == "True":
378
+ self.imgsz_spinbox.setValue(value)
379
+
340
380
  def load_model(self):
341
381
  """
342
382
  Load the selected model.
@@ -372,7 +412,9 @@ class DeployPredictorDialog(QDialog):
372
412
  imgsz=640,
373
413
  conf=0.99,
374
414
  )
375
-
415
+ # Finish the progress bar
416
+ progress_bar.finish_progress()
417
+ # Update the status bar
376
418
  self.status_bar.setText(f"Loaded ({self.model_path}")
377
419
  QMessageBox.information(self.annotation_window, "Model Loaded", "Model loaded successfully")
378
420
 
@@ -388,7 +430,7 @@ class DeployPredictorDialog(QDialog):
388
430
  progress_bar.stop_progress()
389
431
  progress_bar.close()
390
432
  progress_bar = None
391
-
433
+
392
434
  def resize_image(self, image):
393
435
  """
394
436
  Resize the image to the specified size.
@@ -133,12 +133,51 @@ class PolygonTool(Tool):
133
133
  return None
134
134
 
135
135
  # Create the annotation with current points
136
- # The polygon simplification is now handled inside the PolygonAnnotation class
137
136
  if finished and len(self.points) > 2:
138
137
  # Close the polygon
139
138
  self.points.append(self.points[0])
140
-
141
- # Create the annotation - will be simplified in the constructor
139
+
140
+ # --- Validation for polygon size and shape ---
141
+ # Step 1: Remove duplicate or near-duplicate points
142
+ filtered_points = []
143
+ MIN_DISTANCE = 2.0 # Minimum distance between points in pixels
144
+
145
+ for i, point in enumerate(self.points):
146
+ # Skip if this point is too close to the previous one
147
+ if i > 0:
148
+ prev_point = filtered_points[-1]
149
+ distance = ((point.x() - prev_point.x())**2 + (point.y() - prev_point.y())**2)**0.5
150
+ if distance < MIN_DISTANCE:
151
+ continue
152
+ filtered_points.append(point)
153
+
154
+ # Step 2: Ensure we have enough points for a valid polygon
155
+ if len(filtered_points) < 4: # Need at least 3 + 1 closing point
156
+ # Create a small triangle/square if we don't have enough points
157
+ if len(filtered_points) > 0:
158
+ center_x = sum(p.x() for p in filtered_points) / len(filtered_points)
159
+ center_y = sum(p.y() for p in filtered_points) / len(filtered_points)
160
+
161
+ # Create a small polygon centered on the average of existing points
162
+ MIN_SIZE = 5.0
163
+ filtered_points = [
164
+ QPointF(center_x - MIN_SIZE, center_y - MIN_SIZE),
165
+ QPointF(center_x + MIN_SIZE, center_y - MIN_SIZE),
166
+ QPointF(center_x + MIN_SIZE, center_y + MIN_SIZE),
167
+ QPointF(center_x - MIN_SIZE, center_y + MIN_SIZE),
168
+ QPointF(center_x - MIN_SIZE, center_y - MIN_SIZE) # Close the polygon
169
+ ]
170
+
171
+ QMessageBox.information(
172
+ self.annotation_window,
173
+ "Polygon Adjusted",
174
+ "The polygon had too few unique points and has been adjusted to a minimum size."
175
+ )
176
+
177
+ # Use the filtered points list instead of the original
178
+ self.points = filtered_points
179
+
180
+ # Create the annotation with validated points
142
181
  annotation = PolygonAnnotation(self.points,
143
182
  self.annotation_window.selected_label.short_label_code,
144
183
  self.annotation_window.selected_label.long_label_code,
@@ -113,6 +113,36 @@ class RectangleTool(Tool):
113
113
  # Ensure top_left and bottom_right are correctly calculated
114
114
  top_left = QPointF(min(self.start_point.x(), end_point.x()), min(self.start_point.y(), end_point.y()))
115
115
  bottom_right = QPointF(max(self.start_point.x(), end_point.x()), max(self.start_point.y(), end_point.y()))
116
+
117
+ # Calculate width and height of the rectangle
118
+ width = bottom_right.x() - top_left.x()
119
+ height = bottom_right.y() - top_left.y()
120
+
121
+ # Define minimum dimensions for a valid rectangle (e.g., 3x3 pixels)
122
+ MIN_DIMENSION = 3.0
123
+
124
+ # If rectangle is too small and we're finalizing it, enforce minimum size
125
+ if finished and (width < MIN_DIMENSION or height < MIN_DIMENSION):
126
+ if width < MIN_DIMENSION:
127
+ # Expand width while maintaining center
128
+ center_x = (top_left.x() + bottom_right.x()) / 2
129
+ top_left.setX(center_x - MIN_DIMENSION / 2)
130
+ bottom_right.setX(center_x + MIN_DIMENSION / 2)
131
+
132
+ if height < MIN_DIMENSION:
133
+ # Expand height while maintaining center
134
+ center_y = (top_left.y() + bottom_right.y()) / 2
135
+ top_left.setY(center_y - MIN_DIMENSION / 2)
136
+ bottom_right.setY(center_y + MIN_DIMENSION / 2)
137
+
138
+ # Show a message if we had to adjust a very small rectangle
139
+ if width < 1 or height < 1:
140
+ QMessageBox.information(
141
+ self.annotation_window,
142
+ "Rectangle Adjusted",
143
+ f"The rectangle was too small and has been adjusted to a minimum size of "
144
+ f"{MIN_DIMENSION}x{MIN_DIMENSION} pixels."
145
+ )
116
146
 
117
147
  # Create the rectangle annotation
118
148
  annotation = RectangleAnnotation(top_left,
@@ -1,6 +1,6 @@
1
1
  """Top-level package for CoralNet-Toolbox."""
2
2
 
3
- __version__ = "0.0.74"
3
+ __version__ = "0.0.75"
4
4
  __author__ = "Jordan Pierce"
5
5
  __email__ = "jordan.pierce@noaa.gov"
6
6
  __credits__ = "National Center for Coastal and Ocean Sciences (NCCOS)"
@@ -571,6 +571,27 @@ def pixmap_to_numpy(pixmap):
571
571
  return numpy_array
572
572
 
573
573
 
574
+ def pixmap_to_pil(pixmap):
575
+ """
576
+ Convert a QPixmap to a PIL Image.
577
+
578
+ :param pixmap: QPixmap to convert
579
+ :return: PIL Image in RGB format
580
+ """
581
+ from PIL import Image
582
+
583
+ # Convert pixmap to numpy array first
584
+ image_np = pixmap_to_numpy(pixmap)
585
+
586
+ # Convert numpy array to PIL Image
587
+ if len(image_np.shape) == 2: # Grayscale
588
+ pil_image = Image.fromarray(image_np, mode='L').convert('RGB')
589
+ else: # RGB
590
+ pil_image = Image.fromarray(image_np, mode='RGB')
591
+
592
+ return pil_image
593
+
594
+
574
595
  def scale_pixmap(pixmap, max_size):
575
596
  """Scale pixmap and graphic if they exceed max dimension while preserving aspect ratio"""
576
597
  width = pixmap.width()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coralnet-toolbox
3
- Version: 0.0.74
3
+ Version: 0.0.75
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
@@ -27,7 +27,8 @@ Requires-Dist: pycocotools
27
27
  Requires-Dist: ujson
28
28
  Requires-Dist: timm==0.9.2
29
29
  Requires-Dist: autodistill
30
- Requires-Dist: transformers>=4.5.0
30
+ Requires-Dist: transformers>=4.55.4
31
+ Requires-Dist: hf_xet
31
32
  Requires-Dist: x-segment-anything>=0.0.8
32
33
  Requires-Dist: yolo-tiling>=0.0.19
33
34
  Requires-Dist: roboflow
@@ -308,7 +309,9 @@ uv pip install -U coralnet-toolbox==[latest_version]
308
309
  ### 🏗️ **Repository Structure**
309
310
 
310
311
  <div align="center">
311
- ![Visualization of the codebase](./diagram.svg)
312
+ <a href="https://raw.githubusercontent.com/Jordan-Pierce/CoralNet-Toolbox/refs/heads/main/diagram.svg">
313
+ <img src="https://raw.githubusercontent.com/Jordan-Pierce/CoralNet-Toolbox/refs/heads/main/diagram.svg" alt="Visualization of the codebase" width="80%">
314
+ </a>
312
315
  </div>
313
316
 
314
317
  ---