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.
- coralnet_toolbox/Explorer/QtDataItem.py +52 -22
- coralnet_toolbox/Explorer/QtExplorer.py +277 -1600
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +101 -15
- coralnet_toolbox/Explorer/QtViewers.py +1568 -0
- coralnet_toolbox/Explorer/transformer_models.py +59 -0
- coralnet_toolbox/Explorer/yolo_models.py +112 -0
- coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +239 -147
- coralnet_toolbox/MachineLearning/VideoInference/YOLO3D/run.py +102 -16
- coralnet_toolbox/QtAnnotationWindow.py +16 -10
- coralnet_toolbox/QtImageWindow.py +3 -7
- coralnet_toolbox/Rasters/RasterTableModel.py +20 -0
- coralnet_toolbox/SAM/QtDeployGenerator.py +1 -4
- coralnet_toolbox/SAM/QtDeployPredictor.py +1 -3
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +131 -106
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +45 -3
- coralnet_toolbox/Tools/QtPolygonTool.py +42 -3
- coralnet_toolbox/Tools/QtRectangleTool.py +30 -0
- coralnet_toolbox/__init__.py +1 -1
- coralnet_toolbox/utilities.py +21 -0
- {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/METADATA +6 -3
- {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/RECORD +25 -22
- {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.74.dist-info → coralnet_toolbox-0.0.75.dist-info}/licenses/LICENSE.txt +0 -0
- {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(
|
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:
|
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
|
-
|
818
|
+
Saves the combined collection of VPEs (imported and pre-generated from references) to disk.
|
814
819
|
"""
|
815
|
-
|
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
|
-
|
826
|
-
|
827
|
-
|
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
|
-
#
|
832
|
-
|
826
|
+
# Add imported VPEs if available
|
827
|
+
if self.imported_vpes:
|
828
|
+
all_vpes.extend(self.imported_vpes)
|
833
829
|
|
834
|
-
if
|
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
|
-
|
845
|
-
|
846
|
-
|
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
|
-
"
|
849
|
-
"
|
849
|
+
"Save VPE Collection",
|
850
|
+
"",
|
851
|
+
"PyTorch Tensor (*.pt);;All Files (*)"
|
850
852
|
)
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
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
|
1253
|
+
def _apply_model_using_vpe(self, inputs):
|
1201
1254
|
"""
|
1202
|
-
Apply the model to the inputs using
|
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
|
-
#
|
1223
|
-
if
|
1224
|
-
|
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.
|
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
|
-
#
|
1282
|
-
|
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.
|
1426
|
-
|
1427
|
-
|
1428
|
-
|
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
|
-
#
|
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
|
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
|
-
#
|
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
|
-
|
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(
|
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
|
-
|
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,
|
coralnet_toolbox/__init__.py
CHANGED
coralnet_toolbox/utilities.py
CHANGED
@@ -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.
|
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.
|
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
|
-
|
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
|
---
|