coralnet-toolbox 0.0.76__py2.py3-none-any.whl → 0.0.77__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/Common/QtGraphicsUtility.py +18 -8
- coralnet_toolbox/IO/QtExportMaskAnnotations.py +45 -6
- coralnet_toolbox/IO/QtImportImages.py +7 -15
- coralnet_toolbox/IO/QtOpenProject.py +15 -19
- coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +33 -8
- coralnet_toolbox/QtAnnotationWindow.py +4 -0
- coralnet_toolbox/QtEventFilter.py +1 -1
- coralnet_toolbox/QtImageWindow.py +4 -0
- coralnet_toolbox/Rasters/RasterManager.py +5 -2
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +312 -170
- coralnet_toolbox/Tools/QtPatchTool.py +6 -2
- coralnet_toolbox/Tools/QtPolygonTool.py +5 -3
- coralnet_toolbox/Tools/QtRectangleTool.py +17 -9
- coralnet_toolbox/Tools/QtSAMTool.py +4 -0
- coralnet_toolbox/Tools/QtSeeAnythingTool.py +4 -0
- coralnet_toolbox/Tools/QtTool.py +79 -3
- coralnet_toolbox/Tools/QtWorkAreaTool.py +4 -0
- coralnet_toolbox/Transformers/Models/QtBase.py +2 -1
- coralnet_toolbox/__init__.py +1 -1
- {coralnet_toolbox-0.0.76.dist-info → coralnet_toolbox-0.0.77.dist-info}/METADATA +1 -1
- {coralnet_toolbox-0.0.76.dist-info → coralnet_toolbox-0.0.77.dist-info}/RECORD +25 -25
- {coralnet_toolbox-0.0.76.dist-info → coralnet_toolbox-0.0.77.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.76.dist-info → coralnet_toolbox-0.0.77.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.76.dist-info → coralnet_toolbox-0.0.77.dist-info}/licenses/LICENSE.txt +0 -0
- {coralnet_toolbox-0.0.76.dist-info → coralnet_toolbox-0.0.77.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,17 @@
|
|
1
1
|
import warnings
|
2
2
|
|
3
|
-
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
4
|
-
warnings.filterwarnings("ignore", category=UserWarning)
|
5
|
-
|
6
|
-
import os
|
7
3
|
|
4
|
+
from PyQt5.QtCore import Qt
|
5
|
+
from PyQt5.QtGui import QPen, QColor
|
8
6
|
from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGroupBox, QFormLayout,
|
9
7
|
QDoubleSpinBox, QComboBox, QSpinBox, QHBoxLayout,
|
10
8
|
QWidget, QStackedWidget, QGridLayout, QMessageBox,
|
11
9
|
QDialog, QListWidget, QPushButton, QFileDialog,
|
12
|
-
QGraphicsView)
|
13
|
-
|
10
|
+
QGraphicsView, QGraphicsLineItem)
|
11
|
+
|
14
12
|
|
15
|
-
|
13
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
14
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
16
15
|
|
17
16
|
|
18
17
|
# ----------------------------------------------------------------------------------------------------------------------
|
@@ -161,4 +160,15 @@ class GraphicsUtility:
|
|
161
160
|
|
162
161
|
view_width = round(extent.width())
|
163
162
|
view_height = round(extent.height())
|
164
|
-
return max(2, min(5, max(view_width, view_height) // 1000))
|
163
|
+
return max(2, min(5, max(view_width, view_height) // 1000))
|
164
|
+
|
165
|
+
@staticmethod
|
166
|
+
def create_guide_line(start_point, end_point):
|
167
|
+
"""Create a semi-transparent guide line for crosshairs."""
|
168
|
+
line = QGraphicsLineItem(start_point.x(), start_point.y(), end_point.x(), end_point.y())
|
169
|
+
pen = QPen(QColor(255, 255, 255, 180)) # Semi-transparent white
|
170
|
+
pen.setWidth(1)
|
171
|
+
pen.setStyle(Qt.DashLine)
|
172
|
+
line.setPen(pen)
|
173
|
+
line.setZValue(1000) # Ensure it's drawn on top of other elements
|
174
|
+
return line
|
@@ -585,14 +585,53 @@ class ExportMaskAnnotations(QDialog):
|
|
585
585
|
if not has_annotations and not self.include_negative_samples_checkbox.isChecked():
|
586
586
|
return
|
587
587
|
|
588
|
-
#
|
589
|
-
filename = f"{os.path.splitext(os.path.basename(image_path))[0]}.
|
588
|
+
# Use the selected file format
|
589
|
+
filename = f"{os.path.splitext(os.path.basename(image_path))[0]}{self.file_format}"
|
590
590
|
mask_path = os.path.join(output_path, filename)
|
591
591
|
|
592
|
-
if
|
593
|
-
|
594
|
-
|
595
|
-
|
592
|
+
# Check if we need to preserve georeferencing
|
593
|
+
use_georef = has_georef and self.preserve_georef_checkbox.isChecked() and self.file_format.lower() == '.tif'
|
594
|
+
|
595
|
+
if use_georef:
|
596
|
+
# Save with georeferencing using rasterio
|
597
|
+
if self.mask_mode == 'rgb':
|
598
|
+
# For RGB, we need to convert to the expected channel order for rasterio
|
599
|
+
# rasterio expects (bands, height, width) with R,G,B channel order
|
600
|
+
mask_transposed = np.transpose(mask, (2, 0, 1))
|
601
|
+
with rasterio.open(
|
602
|
+
mask_path,
|
603
|
+
'w',
|
604
|
+
driver='GTiff',
|
605
|
+
height=height,
|
606
|
+
width=width,
|
607
|
+
count=3,
|
608
|
+
dtype=mask.dtype,
|
609
|
+
crs=crs,
|
610
|
+
transform=transform
|
611
|
+
) as dst:
|
612
|
+
dst.write(mask_transposed)
|
613
|
+
else:
|
614
|
+
# For single-channel masks
|
615
|
+
with rasterio.open(
|
616
|
+
mask_path,
|
617
|
+
'w',
|
618
|
+
driver='GTiff',
|
619
|
+
height=height,
|
620
|
+
width=width,
|
621
|
+
count=1,
|
622
|
+
dtype=mask.dtype,
|
623
|
+
crs=crs,
|
624
|
+
transform=transform
|
625
|
+
) as dst:
|
626
|
+
dst.write(mask, 1)
|
627
|
+
else:
|
628
|
+
# Use cv2 for non-georeferenced output
|
629
|
+
if self.mask_mode == 'rgb':
|
630
|
+
# OpenCV expects BGR, so convert from RGB
|
631
|
+
mask = cv2.cvtColor(mask, cv2.COLOR_RGB2BGR)
|
632
|
+
|
633
|
+
# Save using the appropriate format
|
634
|
+
cv2.imwrite(mask_path, mask)
|
596
635
|
|
597
636
|
def export_metadata(self, output_path):
|
598
637
|
if self.mask_mode == 'semantic':
|
@@ -50,7 +50,6 @@ class ImportImages:
|
|
50
50
|
|
51
51
|
def _process_image_files(self, file_names):
|
52
52
|
"""Helper method to process a list of image files with progress tracking."""
|
53
|
-
# Make the cursor busy
|
54
53
|
QApplication.setOverrideCursor(Qt.WaitCursor)
|
55
54
|
|
56
55
|
progress_bar = ProgressBar(self.image_window, title="Importing Images")
|
@@ -58,30 +57,24 @@ class ImportImages:
|
|
58
57
|
progress_bar.start_progress(len(file_names))
|
59
58
|
|
60
59
|
try:
|
61
|
-
# Keep track of successfully imported images
|
62
60
|
imported_paths = []
|
63
61
|
|
64
|
-
# Add images to the
|
62
|
+
# Add images directly to the manager without emitting signals
|
65
63
|
for file_name in file_names:
|
66
|
-
# Check if the image is already in the raster manager
|
67
64
|
if file_name not in self.image_window.raster_manager.image_paths:
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
except Exception as e:
|
73
|
-
print(f"Warning: Could not import image {file_name}. Error: {e}")
|
65
|
+
# Call the manager directly to add the raster silently,
|
66
|
+
# bypassing ImageWindow.add_image and its signal handlers.
|
67
|
+
if self.image_window.raster_manager.add_raster(file_name, emit_signal=False):
|
68
|
+
imported_paths.append(file_name)
|
74
69
|
else:
|
75
|
-
# Image already exists
|
76
70
|
imported_paths.append(file_name)
|
77
71
|
|
78
|
-
# Update the progress bar
|
79
72
|
progress_bar.update_progress()
|
80
73
|
|
81
|
-
#
|
74
|
+
# After the silent loop, manually update the UI exactly once.
|
75
|
+
self.image_window.update_search_bars()
|
82
76
|
self.image_window.filter_images()
|
83
77
|
|
84
|
-
# Show the last imported image if any were imported
|
85
78
|
if imported_paths:
|
86
79
|
self.image_window.load_image_by_path(imported_paths[-1])
|
87
80
|
|
@@ -89,7 +82,6 @@ class ImportImages:
|
|
89
82
|
except Exception as e:
|
90
83
|
self._show_error_message(str(e))
|
91
84
|
finally:
|
92
|
-
# Restore the cursor to the default cursor
|
93
85
|
QApplication.restoreOverrideCursor()
|
94
86
|
progress_bar.stop_progress()
|
95
87
|
progress_bar.close()
|
@@ -163,46 +163,42 @@ class OpenProject(QDialog):
|
|
163
163
|
# Exit
|
164
164
|
self.accept()
|
165
165
|
|
166
|
+
# In: QtOpenProject.py
|
167
|
+
|
166
168
|
def import_images(self, images_data, legacy_workareas=None):
|
167
169
|
"""Import images, states, and work areas from the given data."""
|
168
170
|
if not images_data:
|
169
171
|
return
|
170
172
|
|
171
|
-
# Determine if the format is old (list of strings) or new (list of dicts)
|
172
173
|
is_new_format = isinstance(images_data[0], dict)
|
173
|
-
|
174
174
|
image_paths = [img['path'] for img in images_data] if is_new_format else images_data
|
175
175
|
|
176
176
|
if not all([os.path.exists(path) for path in image_paths]):
|
177
177
|
image_paths, self.updated_paths = UpdateImagePaths.update_paths(image_paths)
|
178
178
|
|
179
|
-
# Start progress bar
|
180
179
|
total_images = len(image_paths)
|
181
180
|
progress_bar = ProgressBar(self.image_window, title="Importing Images")
|
182
181
|
progress_bar.show()
|
183
182
|
progress_bar.start_progress(total_images)
|
184
183
|
|
185
184
|
try:
|
186
|
-
# Create a map for quick data lookup if using the new format
|
187
185
|
image_data_map = {img['path']: img for img in images_data} if is_new_format else {}
|
188
186
|
|
189
|
-
# Add images to the
|
187
|
+
# Add images directly to the manager without emitting signals
|
190
188
|
for path in image_paths:
|
191
|
-
|
189
|
+
# Call the manager directly to add the raster silently
|
190
|
+
self.image_window.raster_manager.add_raster(path, emit_signal=False)
|
191
|
+
|
192
192
|
raster = self.image_window.raster_manager.get_raster(path)
|
193
193
|
if not raster:
|
194
|
+
progress_bar.update_progress()
|
194
195
|
continue
|
195
196
|
|
196
|
-
# If using the new format, apply saved state and work areas
|
197
197
|
if is_new_format and path in image_data_map:
|
198
198
|
data = image_data_map[path]
|
199
199
|
state = data.get('state', {})
|
200
200
|
work_areas_list = data.get('work_areas', [])
|
201
|
-
|
202
|
-
# Apply raster state
|
203
201
|
raster.checkbox_state = state.get('checkbox_state', False)
|
204
|
-
|
205
|
-
# Import work areas for this image
|
206
202
|
for work_area_data in work_areas_list:
|
207
203
|
try:
|
208
204
|
work_area = WorkArea.from_dict(work_area_data, path)
|
@@ -210,10 +206,8 @@ class OpenProject(QDialog):
|
|
210
206
|
except Exception as e:
|
211
207
|
print(f"Warning: Could not import work area {work_area_data}: {str(e)}")
|
212
208
|
|
213
|
-
# Update the progress bar
|
214
209
|
progress_bar.update_progress()
|
215
|
-
|
216
|
-
# Handle backward compatibility for old, top-level work areas
|
210
|
+
|
217
211
|
if legacy_workareas:
|
218
212
|
for image_path, work_areas_list in legacy_workareas.items():
|
219
213
|
current_path = self.updated_paths.get(image_path, image_path)
|
@@ -223,16 +217,18 @@ class OpenProject(QDialog):
|
|
223
217
|
work_area = WorkArea.from_dict(work_area_data, current_path)
|
224
218
|
raster.add_work_area(work_area)
|
225
219
|
|
226
|
-
# Show the last image if any were imported
|
227
|
-
if self.image_window.raster_manager.image_paths:
|
228
|
-
self.image_window.load_image_by_path(self.image_window.raster_manager.image_paths[-1])
|
229
|
-
|
230
220
|
except Exception as e:
|
231
221
|
QMessageBox.warning(self.annotation_window,
|
232
222
|
"Error Importing Image(s)",
|
233
223
|
f"An error occurred while importing image(s): {str(e)}")
|
234
224
|
finally:
|
235
|
-
#
|
225
|
+
# Manually perform the UI updates ONCE for all imported images
|
226
|
+
self.image_window.update_search_bars()
|
227
|
+
self.image_window.filter_images()
|
228
|
+
|
229
|
+
if self.image_window.raster_manager.image_paths:
|
230
|
+
self.image_window.load_image_by_path(self.image_window.raster_manager.image_paths[-1])
|
231
|
+
|
236
232
|
progress_bar.stop_progress()
|
237
233
|
progress_bar.close()
|
238
234
|
|
@@ -346,6 +346,8 @@ class Base(QDialog):
|
|
346
346
|
self.button_box.rejected.connect(self.reject)
|
347
347
|
self.layout.addWidget(self.button_box)
|
348
348
|
|
349
|
+
# In: QtBase.py
|
350
|
+
|
349
351
|
def browse_data_yaml(self):
|
350
352
|
"""Open a file dialog to select the data YAML file and populate advanced options."""
|
351
353
|
options = QFileDialog.Options()
|
@@ -358,22 +360,40 @@ class Base(QDialog):
|
|
358
360
|
try:
|
359
361
|
with open(file_path, 'r') as file:
|
360
362
|
data = yaml.safe_load(file)
|
361
|
-
|
362
|
-
|
363
|
-
|
363
|
+
|
364
|
+
names_data = data.get('names')
|
365
|
+
if not names_data:
|
366
|
+
QMessageBox.warning(self, "Warning", "Could not find a 'names' entry in the selected YAML file.")
|
364
367
|
return
|
365
368
|
|
369
|
+
# Handle both dictionary and list formats for class names
|
370
|
+
names_to_display = []
|
371
|
+
if isinstance(names_data, dict):
|
372
|
+
# If it's a dictionary (e.g., {0: 'coral'}), extract the values, sorting by key
|
373
|
+
# to preserve the intended class order.
|
374
|
+
names_to_display = [str(names_data[key]) for key in sorted(names_data.keys())]
|
375
|
+
elif isinstance(names_data, list):
|
376
|
+
# If it's already a list, use it directly.
|
377
|
+
names_to_display = [str(name) for name in names_data]
|
378
|
+
else:
|
379
|
+
# Handle any other unexpected format.
|
380
|
+
QMessageBox.warning(self, "Format Error",
|
381
|
+
f"The 'names' entry in the YAML has an unexpected format: {type(names_data)}.")
|
382
|
+
return
|
383
|
+
|
366
384
|
self.yaml_path_label.setText(file_path)
|
367
385
|
yaml_dir = os.path.dirname(file_path)
|
368
386
|
self.output_dir_label.setText(yaml_dir)
|
369
387
|
self.output_folder_name.setText("data")
|
370
388
|
|
389
|
+
# Clear any existing checkboxes before adding new ones
|
371
390
|
for checkbox in self.class_checkboxes:
|
372
391
|
self.class_layout.removeWidget(checkbox)
|
373
392
|
checkbox.deleteLater()
|
374
393
|
self.class_checkboxes.clear()
|
375
394
|
|
376
|
-
|
395
|
+
# Create checkboxes using the processed list of names
|
396
|
+
for name in names_to_display:
|
377
397
|
checkbox = QCheckBox(name)
|
378
398
|
checkbox.setChecked(True)
|
379
399
|
self.class_layout.addWidget(checkbox)
|
@@ -504,8 +524,11 @@ class Base(QDialog):
|
|
504
524
|
added_paths = []
|
505
525
|
progress_bar.set_title(f"Adding {len(image_paths)} images...")
|
506
526
|
progress_bar.start_progress(len(image_paths))
|
527
|
+
|
507
528
|
for path in image_paths:
|
508
|
-
|
529
|
+
# Call the manager directly to add the raster silently,
|
530
|
+
# bypassing ImageWindow.add_image and its signal handlers.
|
531
|
+
if self.image_window.raster_manager.add_raster(path, emit_signal=False):
|
509
532
|
added_paths.append(path)
|
510
533
|
progress_bar.update_progress()
|
511
534
|
|
@@ -535,19 +558,21 @@ class Base(QDialog):
|
|
535
558
|
self.main_window.get_transparency_value())
|
536
559
|
|
537
560
|
self.annotation_window.add_annotation_to_dict(annotation)
|
538
|
-
newly_created_annotations.append(annotation)
|
561
|
+
newly_created_annotations.append(annotation)
|
539
562
|
|
540
563
|
progress_bar.update_progress()
|
541
564
|
|
542
|
-
# --- Call the restored, correct export function ---
|
543
565
|
progress_bar.set_title("Exporting annotations.json...")
|
544
566
|
self._export_annotations_to_json(newly_created_annotations, self.output_folder)
|
545
567
|
|
546
568
|
progress_bar.finish_progress()
|
547
569
|
progress_bar.stop_progress()
|
548
570
|
progress_bar.close()
|
549
|
-
|
571
|
+
|
572
|
+
# Manually perform a full UI update exactly once.
|
573
|
+
self.image_window.update_search_bars()
|
550
574
|
self.image_window.filter_images()
|
575
|
+
|
551
576
|
if added_paths:
|
552
577
|
self.image_window.load_image_by_path(added_paths[-1])
|
553
578
|
self.image_window.update_image_annotations(added_paths[-1])
|
@@ -401,6 +401,10 @@ class AnnotationWindow(QGraphicsView):
|
|
401
401
|
self.current_image_path = image_path
|
402
402
|
self.active_image = True
|
403
403
|
|
404
|
+
# Automatically mark this image as checked when viewed
|
405
|
+
raster.checkbox_state = True
|
406
|
+
self.main_window.image_window.table_model.update_raster_data(image_path)
|
407
|
+
|
404
408
|
self.tools["zoom"].reset_zoom()
|
405
409
|
self.scene.addItem(QGraphicsPixmapItem(self.pixmap_image))
|
406
410
|
self.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)
|
@@ -13,7 +13,7 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
13
13
|
|
14
14
|
class GlobalEventFilter(QObject):
|
15
15
|
def __init__(self, main_window):
|
16
|
-
super().__init__()
|
16
|
+
super().__init__(main_window)
|
17
17
|
self.main_window = main_window
|
18
18
|
self.label_window = main_window.label_window
|
19
19
|
self.annotation_window = main_window.annotation_window
|
@@ -667,6 +667,10 @@ class ImageWindow(QWidget):
|
|
667
667
|
# Get the raster
|
668
668
|
raster = self.raster_manager.get_raster(image_path)
|
669
669
|
|
670
|
+
# Mark as checked when viewed
|
671
|
+
raster.checkbox_state = True
|
672
|
+
self.table_model.update_raster_data(image_path)
|
673
|
+
|
670
674
|
# Update selection
|
671
675
|
self.selected_image_path = image_path
|
672
676
|
self.table_model.set_selected_path(image_path)
|
@@ -31,7 +31,7 @@ class RasterManager(QObject):
|
|
31
31
|
self.rasters: Dict[str, Raster] = {}
|
32
32
|
self.image_paths: List[str] = []
|
33
33
|
|
34
|
-
def add_raster(self, image_path: str) -> bool:
|
34
|
+
def add_raster(self, image_path: str, emit_signal: bool = True) -> bool:
|
35
35
|
"""
|
36
36
|
Add a new raster to the manager.
|
37
37
|
|
@@ -51,7 +51,10 @@ class RasterManager(QObject):
|
|
51
51
|
|
52
52
|
self.rasters[image_path] = raster
|
53
53
|
self.image_paths.append(image_path)
|
54
|
-
|
54
|
+
|
55
|
+
if emit_signal:
|
56
|
+
self.rasterAdded.emit(image_path)
|
57
|
+
|
55
58
|
return True
|
56
59
|
|
57
60
|
except Exception as e:
|