pingmapper 5.4.9__tar.gz → 5.5.1__tar.gz
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.
- {pingmapper-5.4.9 → pingmapper-5.5.1}/PKG-INFO +1 -1
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/class_mapSubstrateObj.py +1 -1
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/class_portstarObj.py +9 -3
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/class_rectObj.py +5 -5
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/class_sonObj.py +1 -1
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/class_sonObj_nadirgaptest.py +1 -1
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/default_params.json +8 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/gui_main.py +92 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/main_readFiles.py +616 -0
- pingmapper-5.5.1/pingmapper/version.py +1 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper.egg-info/PKG-INFO +1 -1
- pingmapper-5.4.9/pingmapper/version.py +0 -1
- {pingmapper-5.4.9 → pingmapper-5.5.1}/LICENSE +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/README.md +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/__init__.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/__main__.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/doWork.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/funcs_common.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/funcs_model.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/funcs_rectify.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/main_mapSubstrate.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/main_rectify.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/nonGUI_batch_main.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/nonGui_main.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/scratch/main.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/scratch/main_batchDirectory.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/self_check.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/test_PINGMapper.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/test_cli_self_check.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/test_dq_filter.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/test_time.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/export_coverage.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/main_mosaic_transects.py +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper.egg-info/SOURCES.txt +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper.egg-info/dependency_links.txt +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper.egg-info/requires.txt +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper.egg-info/top_level.txt +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/pyproject.toml +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/setup.cfg +0 -0
- {pingmapper-5.4.9 → pingmapper-5.5.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pingmapper
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.5.1
|
|
4
4
|
Summary: Open-source interface for processing recreation-grade side scan sonar datasets and reproducibly mapping benthic habitat
|
|
5
5
|
Author: Cameron Bodine, Daniel Buscombe, Iury T Simoes-Sousa
|
|
6
6
|
Author-email: bodine.cs@gmail.email
|
|
@@ -880,7 +880,7 @@ class mapSubObj(rectObj):
|
|
|
880
880
|
ax.imshow(son, cmap='gray')
|
|
881
881
|
|
|
882
882
|
# Prepare color map
|
|
883
|
-
color_map = plt.
|
|
883
|
+
color_map = plt.get_cmap('viridis')
|
|
884
884
|
|
|
885
885
|
im = ax.imshow(c, cmap=color_map, alpha=0.5, vmin=minSoft, vmax=maxSoft)
|
|
886
886
|
ax.axis('off')
|
|
@@ -3185,7 +3185,11 @@ class portstarObj(object):
|
|
|
3185
3185
|
|
|
3186
3186
|
for feature in dst_layer:
|
|
3187
3187
|
subID = str(int(feature.GetField('Substrate')))
|
|
3188
|
-
subName = MY_CLASS_NAMES
|
|
3188
|
+
subName = MY_CLASS_NAMES.get(subID)
|
|
3189
|
+
if subName is None:
|
|
3190
|
+
print("\n\tWARNING: Unexpected substrate value '{}' found in raster. "
|
|
3191
|
+
"This value is not in the model config and will be removed from output.".format(subID))
|
|
3192
|
+
subName = 'Unknown'
|
|
3189
3193
|
feature.SetField('Name', subName)
|
|
3190
3194
|
dst_layer.SetFeature(feature)
|
|
3191
3195
|
|
|
@@ -3204,10 +3208,12 @@ class portstarObj(object):
|
|
|
3204
3208
|
feature.SetField("Area_m", area)
|
|
3205
3209
|
dst_layer.SetFeature(feature)
|
|
3206
3210
|
|
|
3207
|
-
# Delete NoData
|
|
3211
|
+
# Delete NoData and any unexpected-value polygons
|
|
3208
3212
|
# https://gis.stackexchange.com/questions/254444/deleting-selected-features-from-vector-ogr-in-gdal-python
|
|
3213
|
+
valid_sub_ids = [int(k) for k in MY_CLASS_NAMES.keys() if int(k) != 0]
|
|
3214
|
+
invalid_filter = "Substrate NOT IN ({})".format(','.join(str(v) for v in valid_sub_ids))
|
|
3209
3215
|
layer = dst_ds.GetLayer()
|
|
3210
|
-
layer.SetAttributeFilter(
|
|
3216
|
+
layer.SetAttributeFilter(invalid_filter)
|
|
3211
3217
|
|
|
3212
3218
|
for feat in layer:
|
|
3213
3219
|
layer.DeleteFeature(feat.GetFID())
|
|
@@ -1968,7 +1968,7 @@ class rectObj(sonObj):
|
|
|
1968
1968
|
use_uint8_rgb = self._export_colormap_as_uint8()
|
|
1969
1969
|
if source_scale_bounds is not None:
|
|
1970
1970
|
norm_data = self._normalize_with_bounds(sonRect_raw16, source_scale_bounds[0], source_scale_bounds[1])
|
|
1971
|
-
cmap = plt.
|
|
1971
|
+
cmap = plt.get_cmap(self.son_colorMap_name)(norm_data)
|
|
1972
1972
|
if use_uint8_rgb:
|
|
1973
1973
|
sonRect_out = np.clip(cmap[:, :, :3] * 255.0, 0, 255).astype(np.uint8)
|
|
1974
1974
|
else:
|
|
@@ -2769,9 +2769,9 @@ class rectObj(sonObj):
|
|
|
2769
2769
|
if source_scale_bounds is not None:
|
|
2770
2770
|
norm_data = self._normalize_with_bounds(out16_raw, source_scale_bounds[0], source_scale_bounds[1])
|
|
2771
2771
|
if use_uint8_rgb:
|
|
2772
|
-
out16 = np.clip(plt.
|
|
2772
|
+
out16 = np.clip(plt.get_cmap(self.son_colorMap_name)(norm_data)[:, :, :3] * 255.0, 0, 255).astype(np.uint8)
|
|
2773
2773
|
else:
|
|
2774
|
-
out16 = np.clip(plt.
|
|
2774
|
+
out16 = np.clip(plt.get_cmap(self.son_colorMap_name)(norm_data)[:, :, :3] * 65535.0, 0, 65535).astype(np.uint16)
|
|
2775
2775
|
valid_mask = out16_raw > 0
|
|
2776
2776
|
out16[valid_mask] = np.maximum(out16[valid_mask], 1)
|
|
2777
2777
|
else:
|
|
@@ -2914,9 +2914,9 @@ class rectObj(sonObj):
|
|
|
2914
2914
|
if source_scale_bounds is not None:
|
|
2915
2915
|
norm_data = self._normalize_with_bounds(out16_raw, source_scale_bounds[0], source_scale_bounds[1])
|
|
2916
2916
|
if use_uint8_rgb:
|
|
2917
|
-
out16 = np.clip(plt.
|
|
2917
|
+
out16 = np.clip(plt.get_cmap(self.son_colorMap_name)(norm_data)[:, :, :3] * 255.0, 0, 255).astype(np.uint8)
|
|
2918
2918
|
else:
|
|
2919
|
-
out16 = np.clip(plt.
|
|
2919
|
+
out16 = np.clip(plt.get_cmap(self.son_colorMap_name)(norm_data)[:, :, :3] * 65535.0, 0, 65535).astype(np.uint16)
|
|
2920
2920
|
valid_mask = out16_raw > 0
|
|
2921
2921
|
out16[valid_mask] = np.maximum(out16[valid_mask], 1)
|
|
2922
2922
|
else:
|
|
@@ -1872,7 +1872,7 @@ class sonObj(object):
|
|
|
1872
1872
|
return out
|
|
1873
1873
|
|
|
1874
1874
|
def _colorize_array_batched(self, norm_data, valid_mask, cmap_name, scale_max, out_dtype):
|
|
1875
|
-
cmap = plt.
|
|
1875
|
+
cmap = plt.get_cmap(cmap_name)
|
|
1876
1876
|
height, width = norm_data.shape
|
|
1877
1877
|
out = np.zeros((height, width, 3), dtype=out_dtype)
|
|
1878
1878
|
|
|
@@ -1500,7 +1500,7 @@ class sonObj(object):
|
|
|
1500
1500
|
# plt.savefig(outfile)
|
|
1501
1501
|
|
|
1502
1502
|
norm_data = data / 255.0
|
|
1503
|
-
colored_data = plt.
|
|
1503
|
+
colored_data = plt.get_cmap(self.sonogram_colorMap)(norm_data)
|
|
1504
1504
|
colored_data = (colored_data[:, :, :3] * 255).astype('uint8')
|
|
1505
1505
|
data = colored_data
|
|
1506
1506
|
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
"wcm":true,
|
|
30
30
|
"wcr":false,
|
|
31
31
|
"wco":false,
|
|
32
|
+
"waterfall_ss_image":false,
|
|
33
|
+
"waterfall_ss_video":false,
|
|
34
|
+
"waterfall_di_image":false,
|
|
35
|
+
"waterfall_di_video":false,
|
|
36
|
+
"waterfall_video_fps":10,
|
|
37
|
+
"waterfall_video_resolution":"1080p",
|
|
38
|
+
"waterfall_mode_selection":"Auto (enabled exports)",
|
|
39
|
+
"waterfall_window_stride":64,
|
|
32
40
|
"export_16bit":false,
|
|
33
41
|
"export_colormap_uint8":true,
|
|
34
42
|
"tileFile":".jpg",
|
|
@@ -382,6 +382,84 @@ def gui(batch: bool):
|
|
|
382
382
|
layout.append([text_tile])
|
|
383
383
|
layout.append([col_tile_1, sg.VerticalSeparator(), col_tile_2, sg.VerticalSeparator(), col_tile_3])
|
|
384
384
|
|
|
385
|
+
############################
|
|
386
|
+
# Waterfall Image/Video Exports
|
|
387
|
+
|
|
388
|
+
text_waterfall = sg.Text('Waterfall Image/Video Exports\n', font=("Helvetica", 14, "underline"))
|
|
389
|
+
|
|
390
|
+
tip_wf_ss_img = ml_tip('Export combined side-scan waterfall image (port+star), arranged for top-to-bottom scrolling.')
|
|
391
|
+
tip_wf_ss_vid = ml_tip('Export side-scan scrolling video (top to bottom) from combined port+star waterfall.')
|
|
392
|
+
tip_wf_di_img = ml_tip('Export down-imaging waterfall image for each down-looking beam.')
|
|
393
|
+
tip_wf_di_vid = ml_tip('Export down-imaging scrolling video (right to left) for each down-looking beam.')
|
|
394
|
+
tip_wf_fps = ml_tip('Waterfall video frame rate (frames per second).')
|
|
395
|
+
tip_wf_res = ml_tip('Video output resolution preset for waterfall videos.')
|
|
396
|
+
tip_wf_mode = ml_tip('Tile mode(s) used for waterfall export. Auto uses currently enabled tile exports.')
|
|
397
|
+
tip_wf_stride = ml_tip('Window step in pixels per frame (larger = faster scrolling).')
|
|
398
|
+
|
|
399
|
+
check_wf_ss_img = sg.Checkbox(
|
|
400
|
+
'Side-scan Waterfall Image (Port+Star Combined)',
|
|
401
|
+
key='waterfall_ss_image',
|
|
402
|
+
default=default_params.get('waterfall_ss_image', False),
|
|
403
|
+
tooltip=tip_wf_ss_img,
|
|
404
|
+
)
|
|
405
|
+
check_wf_ss_vid = sg.Checkbox(
|
|
406
|
+
'Side-scan Waterfall Video (Top -> Bottom)',
|
|
407
|
+
key='waterfall_ss_video',
|
|
408
|
+
default=default_params.get('waterfall_ss_video', False),
|
|
409
|
+
tooltip=tip_wf_ss_vid,
|
|
410
|
+
)
|
|
411
|
+
check_wf_di_img = sg.Checkbox(
|
|
412
|
+
'Down-imaging Waterfall Image',
|
|
413
|
+
key='waterfall_di_image',
|
|
414
|
+
default=default_params.get('waterfall_di_image', False),
|
|
415
|
+
tooltip=tip_wf_di_img,
|
|
416
|
+
)
|
|
417
|
+
check_wf_di_vid = sg.Checkbox(
|
|
418
|
+
'Down-imaging Waterfall Video (Right -> Left)',
|
|
419
|
+
key='waterfall_di_video',
|
|
420
|
+
default=default_params.get('waterfall_di_video', False),
|
|
421
|
+
tooltip=tip_wf_di_vid,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
text_wf_fps = sg.Text('Video FPS', size=(20,1))
|
|
425
|
+
in_wf_fps = sg.Input(
|
|
426
|
+
key='waterfall_video_fps',
|
|
427
|
+
default_text=default_params.get('waterfall_video_fps', 10),
|
|
428
|
+
size=(10,1),
|
|
429
|
+
tooltip=tip_wf_fps,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
text_wf_res = sg.Text('Video Resolution', size=(20,1))
|
|
433
|
+
combo_wf_res = sg.Combo(
|
|
434
|
+
['4K', '1080p', '720p', '4xxp'],
|
|
435
|
+
key='waterfall_video_resolution',
|
|
436
|
+
default_value=default_params.get('waterfall_video_resolution', '1080p'),
|
|
437
|
+
tooltip=tip_wf_res,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
text_wf_mode = sg.Text('Waterfall Mode(s)', size=(20,1))
|
|
441
|
+
combo_wf_mode = sg.Combo(
|
|
442
|
+
['Auto (enabled exports)', 'WCP', 'SRC', 'WCP+SRC'],
|
|
443
|
+
key='waterfall_mode_selection',
|
|
444
|
+
default_value=default_params.get('waterfall_mode_selection', 'Auto (enabled exports)'),
|
|
445
|
+
tooltip=tip_wf_mode,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
text_wf_stride = sg.Text('Window Stride [px]', size=(20,1))
|
|
449
|
+
in_wf_stride = sg.Input(
|
|
450
|
+
key='waterfall_window_stride',
|
|
451
|
+
default_text=default_params.get('waterfall_window_stride', 64),
|
|
452
|
+
size=(10,1),
|
|
453
|
+
tooltip=tip_wf_stride,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
col_wf_1 = sg.Column([[check_wf_ss_img], [check_wf_ss_vid], [check_wf_di_img], [check_wf_di_vid]], pad=0)
|
|
457
|
+
col_wf_2 = sg.Column([[text_wf_fps, in_wf_fps], [text_wf_res, combo_wf_res], [text_wf_mode, combo_wf_mode], [text_wf_stride, in_wf_stride]], pad=0)
|
|
458
|
+
|
|
459
|
+
layout.append([sg.HorizontalSeparator()])
|
|
460
|
+
layout.append([text_waterfall])
|
|
461
|
+
layout.append([col_wf_1, sg.VerticalSeparator(), col_wf_2])
|
|
462
|
+
|
|
385
463
|
########################
|
|
386
464
|
# Depth & Shadow Removal
|
|
387
465
|
|
|
@@ -695,6 +773,12 @@ def gui(batch: bool):
|
|
|
695
773
|
map_mosaic = int(map_mosaic)
|
|
696
774
|
|
|
697
775
|
|
|
776
|
+
wf_mode = values['waterfall_mode_selection']
|
|
777
|
+
if wf_mode == 'Auto (enabled exports)':
|
|
778
|
+
wf_mode = 'auto'
|
|
779
|
+
else:
|
|
780
|
+
wf_mode = str(wf_mode).strip().lower()
|
|
781
|
+
|
|
698
782
|
params = {
|
|
699
783
|
# 'humFile':values[0],
|
|
700
784
|
# 'projDir':os.path.join(values[1], values[2]),
|
|
@@ -753,6 +837,14 @@ def gui(batch: bool):
|
|
|
753
837
|
'map_mosaic':map_mosaic,
|
|
754
838
|
'banklines':values['banklines'],
|
|
755
839
|
'coverage':values['coverage']
|
|
840
|
+
, 'waterfall_ss_image':values['waterfall_ss_image']
|
|
841
|
+
, 'waterfall_ss_video':values['waterfall_ss_video']
|
|
842
|
+
, 'waterfall_di_image':values['waterfall_di_image']
|
|
843
|
+
, 'waterfall_di_video':values['waterfall_di_video']
|
|
844
|
+
, 'waterfall_video_fps':int(values['waterfall_video_fps'])
|
|
845
|
+
, 'waterfall_video_resolution':values['waterfall_video_resolution']
|
|
846
|
+
, 'waterfall_mode_selection':wf_mode
|
|
847
|
+
, 'waterfall_window_stride':int(values['waterfall_window_stride'])
|
|
756
848
|
}
|
|
757
849
|
|
|
758
850
|
globals().update(params)
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
# SOFTWARE.
|
|
31
31
|
|
|
32
32
|
import os, sys
|
|
33
|
+
import re
|
|
33
34
|
|
|
34
35
|
# Add 'pingmapper' to the path, may not need after pypi package...
|
|
35
36
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -105,6 +106,576 @@ def _sidescan_group_key(beam_name):
|
|
|
105
106
|
return beam_name
|
|
106
107
|
|
|
107
108
|
|
|
109
|
+
def _extract_chunk_id_from_path(path):
|
|
110
|
+
name = os.path.splitext(os.path.basename(path))[0]
|
|
111
|
+
match = re.search(r'_(\d+)$', name)
|
|
112
|
+
if match:
|
|
113
|
+
return int(match.group(1))
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _sort_tile_paths(paths):
|
|
118
|
+
def _key(path):
|
|
119
|
+
chunk_id = _extract_chunk_id_from_path(path)
|
|
120
|
+
if chunk_id is None:
|
|
121
|
+
return (1, os.path.basename(path))
|
|
122
|
+
return (0, chunk_id)
|
|
123
|
+
|
|
124
|
+
return sorted(paths, key=_key)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _to_bgr8(img):
|
|
128
|
+
if img is None:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
if img.dtype == np.uint16:
|
|
132
|
+
img = (img / 257.0).astype(np.uint8)
|
|
133
|
+
elif img.dtype != np.uint8:
|
|
134
|
+
arr = np.asarray(img, dtype=np.float32)
|
|
135
|
+
finite = np.isfinite(arr)
|
|
136
|
+
if finite.any():
|
|
137
|
+
lo = arr[finite].min()
|
|
138
|
+
hi = arr[finite].max()
|
|
139
|
+
if hi > lo:
|
|
140
|
+
arr = (arr - lo) / (hi - lo)
|
|
141
|
+
else:
|
|
142
|
+
arr = np.zeros_like(arr)
|
|
143
|
+
else:
|
|
144
|
+
arr = np.zeros_like(arr)
|
|
145
|
+
img = (arr * 255.0).clip(0, 255).astype(np.uint8)
|
|
146
|
+
|
|
147
|
+
if img.ndim == 2:
|
|
148
|
+
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
|
149
|
+
|
|
150
|
+
if img.ndim == 3 and img.shape[2] == 4:
|
|
151
|
+
return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
|
|
152
|
+
|
|
153
|
+
return img
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resize_height(img, target_h):
|
|
157
|
+
if img.shape[0] == target_h:
|
|
158
|
+
return img
|
|
159
|
+
|
|
160
|
+
h, w = img.shape[:2]
|
|
161
|
+
if h <= 0 or w <= 0 or target_h <= 0:
|
|
162
|
+
return img
|
|
163
|
+
|
|
164
|
+
new_w = max(1, int(round(w * (target_h / float(h)))))
|
|
165
|
+
return cv2.resize(img, (new_w, target_h), interpolation=cv2.INTER_AREA)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _resize_width(img, target_w):
|
|
169
|
+
if img.shape[1] == target_w:
|
|
170
|
+
return img
|
|
171
|
+
|
|
172
|
+
h, w = img.shape[:2]
|
|
173
|
+
if h <= 0 or w <= 0 or target_w <= 0:
|
|
174
|
+
return img
|
|
175
|
+
|
|
176
|
+
new_h = max(1, int(round(h * (target_w / float(w)))))
|
|
177
|
+
return cv2.resize(img, (target_w, new_h), interpolation=cv2.INTER_AREA)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _downscale_if_needed(img, max_dim=20000):
|
|
181
|
+
h, w = img.shape[:2]
|
|
182
|
+
max_hw = max(h, w)
|
|
183
|
+
if max_hw <= max_dim:
|
|
184
|
+
return img
|
|
185
|
+
|
|
186
|
+
scale = float(max_dim) / float(max_hw)
|
|
187
|
+
new_w = max(1, int(round(w * scale)))
|
|
188
|
+
new_h = max(1, int(round(h * scale)))
|
|
189
|
+
print(f"\n\tDownscaling waterfall from {w}x{h} to {new_w}x{new_h} to control memory usage.")
|
|
190
|
+
return cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _fit_width(img, max_w):
|
|
194
|
+
h, w = img.shape[:2]
|
|
195
|
+
if w <= max_w:
|
|
196
|
+
return img
|
|
197
|
+
scale = float(max_w) / float(w)
|
|
198
|
+
new_h = max(1, int(round(h * scale)))
|
|
199
|
+
return cv2.resize(img, (max_w, new_h), interpolation=cv2.INTER_AREA)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _collect_mode_tile_paths(son, mode):
|
|
203
|
+
mode_dir = os.path.join(son.projDir, son.beamName, mode)
|
|
204
|
+
if not os.path.exists(mode_dir):
|
|
205
|
+
return []
|
|
206
|
+
|
|
207
|
+
exts = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff']
|
|
208
|
+
paths = []
|
|
209
|
+
for ext in exts:
|
|
210
|
+
paths.extend(glob(os.path.join(mode_dir, ext)))
|
|
211
|
+
|
|
212
|
+
return _sort_tile_paths(paths)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _get_chunk_range_map(son):
|
|
216
|
+
chunk_range = {}
|
|
217
|
+
try:
|
|
218
|
+
meta_path = getattr(son, 'sonMetaFile', None)
|
|
219
|
+
if not meta_path or not os.path.exists(meta_path):
|
|
220
|
+
return chunk_range
|
|
221
|
+
|
|
222
|
+
df = pd.read_csv(meta_path, usecols=['chunk_id', 'ping_cnt', 'pixM'])
|
|
223
|
+
if len(df) == 0:
|
|
224
|
+
return chunk_range
|
|
225
|
+
|
|
226
|
+
df['chunk_id'] = pd.to_numeric(df['chunk_id'], errors='coerce')
|
|
227
|
+
df['ping_cnt'] = pd.to_numeric(df['ping_cnt'], errors='coerce')
|
|
228
|
+
df['pixM'] = pd.to_numeric(df['pixM'], errors='coerce')
|
|
229
|
+
df = df.dropna(subset=['chunk_id', 'ping_cnt', 'pixM'])
|
|
230
|
+
if len(df) == 0:
|
|
231
|
+
return chunk_range
|
|
232
|
+
|
|
233
|
+
# Approximate chunk range in meters from ping count and pixel size.
|
|
234
|
+
df['range_m'] = df['ping_cnt'] * df['pixM']
|
|
235
|
+
grouped = df.groupby('chunk_id', as_index=False)['range_m'].median()
|
|
236
|
+
for _, row in grouped.iterrows():
|
|
237
|
+
cid = int(row['chunk_id'])
|
|
238
|
+
rng = float(row['range_m'])
|
|
239
|
+
if np.isfinite(rng) and rng > 0:
|
|
240
|
+
chunk_range[cid] = rng
|
|
241
|
+
except Exception:
|
|
242
|
+
return {}
|
|
243
|
+
|
|
244
|
+
return chunk_range
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _range_reference_m(chunk_range_map):
|
|
248
|
+
if not chunk_range_map:
|
|
249
|
+
return None
|
|
250
|
+
vals = [float(v) for v in chunk_range_map.values() if np.isfinite(v) and float(v) > 0]
|
|
251
|
+
if len(vals) == 0:
|
|
252
|
+
return None
|
|
253
|
+
return float(np.median(vals))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _pad_to_height(img, target_h):
|
|
257
|
+
h, w = img.shape[:2]
|
|
258
|
+
if h >= target_h:
|
|
259
|
+
return img
|
|
260
|
+
pad_h = target_h - h
|
|
261
|
+
return cv2.copyMakeBorder(img, 0, pad_h, 0, 0, cv2.BORDER_CONSTANT, value=0)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _pad_to_height_center(img, target_h):
|
|
265
|
+
h, w = img.shape[:2]
|
|
266
|
+
if h >= target_h:
|
|
267
|
+
return img
|
|
268
|
+
pad_h = target_h - h
|
|
269
|
+
top = pad_h // 2
|
|
270
|
+
bottom = pad_h - top
|
|
271
|
+
return cv2.copyMakeBorder(img, top, bottom, 0, 0, cv2.BORDER_CONSTANT, value=0)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _pad_to_width(img, target_w):
|
|
275
|
+
h, w = img.shape[:2]
|
|
276
|
+
if w >= target_w:
|
|
277
|
+
return img
|
|
278
|
+
pad_w = target_w - w
|
|
279
|
+
return cv2.copyMakeBorder(img, 0, 0, 0, pad_w, cv2.BORDER_CONSTANT, value=0)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _scale_tile_by_range(img, tile_path, chunk_range_map=None, ref_range_m=None, base_h=None):
|
|
283
|
+
if img is None:
|
|
284
|
+
return None
|
|
285
|
+
if not (chunk_range_map and ref_range_m and base_h):
|
|
286
|
+
return img
|
|
287
|
+
|
|
288
|
+
chunk_id = _extract_chunk_id_from_path(tile_path)
|
|
289
|
+
if chunk_id is None:
|
|
290
|
+
return img
|
|
291
|
+
|
|
292
|
+
rng = chunk_range_map.get(chunk_id, None)
|
|
293
|
+
if rng is None or (not np.isfinite(rng)) or rng <= 0:
|
|
294
|
+
return img
|
|
295
|
+
|
|
296
|
+
scale = float(rng) / float(ref_range_m)
|
|
297
|
+
scale = max(0.25, min(4.0, scale))
|
|
298
|
+
target_h = max(1, int(round(base_h * scale)))
|
|
299
|
+
return _resize_height(img, target_h)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _build_strip_from_tiles(tile_paths, chunk_range_map=None, ref_range_m=None):
|
|
303
|
+
if len(tile_paths) == 0:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
imgs = []
|
|
307
|
+
base_h = None
|
|
308
|
+
if ref_range_m is None:
|
|
309
|
+
ref_range_m = _range_reference_m(chunk_range_map)
|
|
310
|
+
|
|
311
|
+
for path in tile_paths:
|
|
312
|
+
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
|
|
313
|
+
if img is None:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
if base_h is None:
|
|
317
|
+
base_h = img.shape[0]
|
|
318
|
+
|
|
319
|
+
if chunk_range_map and ref_range_m and base_h:
|
|
320
|
+
chunk_id = _extract_chunk_id_from_path(path)
|
|
321
|
+
rng = chunk_range_map.get(chunk_id, None)
|
|
322
|
+
if rng is not None and np.isfinite(rng) and rng > 0:
|
|
323
|
+
scale = float(rng) / float(ref_range_m)
|
|
324
|
+
scale = max(0.25, min(4.0, scale))
|
|
325
|
+
target_h = max(1, int(round(base_h * scale)))
|
|
326
|
+
img = _resize_height(img, target_h)
|
|
327
|
+
|
|
328
|
+
imgs.append(img)
|
|
329
|
+
|
|
330
|
+
if len(imgs) == 0:
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
# Preserve range-driven vertical scaling by padding each tile to common max height.
|
|
334
|
+
max_h = max(img.shape[0] for img in imgs)
|
|
335
|
+
imgs = [_pad_to_height(img, max_h) for img in imgs]
|
|
336
|
+
strip = cv2.hconcat(imgs)
|
|
337
|
+
return strip
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _build_sidescan_combined(port_tiles, star_tiles, port_range_map=None, star_range_map=None, ref_range_m=None):
|
|
341
|
+
if len(port_tiles) == 0 or len(star_tiles) == 0:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def _map_by_chunk(paths):
|
|
345
|
+
mapped = {}
|
|
346
|
+
for p in paths:
|
|
347
|
+
cid = _extract_chunk_id_from_path(p)
|
|
348
|
+
if cid is not None:
|
|
349
|
+
mapped[int(cid)] = p
|
|
350
|
+
return mapped
|
|
351
|
+
|
|
352
|
+
port_by_chunk = _map_by_chunk(port_tiles)
|
|
353
|
+
star_by_chunk = _map_by_chunk(star_tiles)
|
|
354
|
+
|
|
355
|
+
# Newest chunk first so newer data sits at the top of the stitched waterfall,
|
|
356
|
+
# while the oldest chunk remains at the bottom for the initial video frame.
|
|
357
|
+
common = sorted(set(port_by_chunk.keys()) & set(star_by_chunk.keys()), reverse=True)
|
|
358
|
+
if len(common) == 0:
|
|
359
|
+
pair_paths = list(zip(port_tiles, star_tiles))
|
|
360
|
+
pair_paths = list(reversed(pair_paths))
|
|
361
|
+
else:
|
|
362
|
+
pair_paths = [(port_by_chunk[c], star_by_chunk[c]) for c in common]
|
|
363
|
+
|
|
364
|
+
# Baseline heights for range-based scaling.
|
|
365
|
+
base_h_port = None
|
|
366
|
+
base_h_star = None
|
|
367
|
+
for p_path, s_path in pair_paths:
|
|
368
|
+
if base_h_port is None:
|
|
369
|
+
p_img = cv2.imread(p_path, cv2.IMREAD_UNCHANGED)
|
|
370
|
+
if p_img is not None:
|
|
371
|
+
base_h_port = p_img.shape[0]
|
|
372
|
+
if base_h_star is None:
|
|
373
|
+
s_img = cv2.imread(s_path, cv2.IMREAD_UNCHANGED)
|
|
374
|
+
if s_img is not None:
|
|
375
|
+
base_h_star = s_img.shape[0]
|
|
376
|
+
if (base_h_port is not None) and (base_h_star is not None):
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
rows = []
|
|
380
|
+
for p_path, s_path in pair_paths:
|
|
381
|
+
p_img = cv2.imread(p_path, cv2.IMREAD_UNCHANGED)
|
|
382
|
+
s_img = cv2.imread(s_path, cv2.IMREAD_UNCHANGED)
|
|
383
|
+
if p_img is None or s_img is None:
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
p_img = _scale_tile_by_range(p_img, p_path, port_range_map, ref_range_m, base_h_port)
|
|
387
|
+
s_img = _scale_tile_by_range(s_img, s_path, star_range_map, ref_range_m, base_h_star)
|
|
388
|
+
|
|
389
|
+
# Apply side-scan waterfall geometry: rotate both CCW, then flip port to place nadir at center.
|
|
390
|
+
p_img = cv2.rotate(p_img, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
391
|
+
s_img = cv2.rotate(s_img, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
392
|
+
p_img = cv2.flip(p_img, 1)
|
|
393
|
+
|
|
394
|
+
p_img = _to_bgr8(p_img)
|
|
395
|
+
s_img = _to_bgr8(s_img)
|
|
396
|
+
|
|
397
|
+
row_h = max(p_img.shape[0], s_img.shape[0])
|
|
398
|
+
# Align each chunk pair on the vertical centerline.
|
|
399
|
+
p_img = _pad_to_height_center(p_img, row_h)
|
|
400
|
+
s_img = _pad_to_height_center(s_img, row_h)
|
|
401
|
+
|
|
402
|
+
# Keep chunk row widths comparable before vertical stacking.
|
|
403
|
+
row = cv2.hconcat([p_img, s_img])
|
|
404
|
+
rows.append(row)
|
|
405
|
+
|
|
406
|
+
if len(rows) == 0:
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
max_w = max(r.shape[1] for r in rows)
|
|
410
|
+
rows = [_pad_to_width(r, max_w) for r in rows]
|
|
411
|
+
return cv2.vconcat(rows)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _window_positions(length, window_size, stride, reverse=False):
|
|
415
|
+
if length <= window_size:
|
|
416
|
+
positions = [0]
|
|
417
|
+
else:
|
|
418
|
+
positions = list(range(0, length - window_size + 1, stride))
|
|
419
|
+
if positions[-1] != (length - window_size):
|
|
420
|
+
positions.append(length - window_size)
|
|
421
|
+
|
|
422
|
+
if reverse:
|
|
423
|
+
positions = list(reversed(positions))
|
|
424
|
+
|
|
425
|
+
return positions
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _export_scrolling_video(
|
|
429
|
+
img,
|
|
430
|
+
out_video,
|
|
431
|
+
axis='x',
|
|
432
|
+
reverse=False,
|
|
433
|
+
fps=10,
|
|
434
|
+
target_size=(1920, 1080),
|
|
435
|
+
stride=64,
|
|
436
|
+
):
|
|
437
|
+
os.makedirs(os.path.dirname(out_video), exist_ok=True)
|
|
438
|
+
|
|
439
|
+
h, w = img.shape[:2]
|
|
440
|
+
target_w, target_h = int(target_size[0]), int(target_size[1])
|
|
441
|
+
target_w = max(64, target_w)
|
|
442
|
+
target_h = max(64, target_h)
|
|
443
|
+
|
|
444
|
+
# Derive crop window from target resolution while preserving aspect ratio.
|
|
445
|
+
target_aspect = target_w / float(target_h)
|
|
446
|
+
win_w = min(w, target_w)
|
|
447
|
+
win_h = min(h, target_h)
|
|
448
|
+
if win_w <= 0 or win_h <= 0:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
cur_aspect = win_w / float(win_h)
|
|
452
|
+
if cur_aspect > target_aspect:
|
|
453
|
+
win_w = max(1, int(round(win_h * target_aspect)))
|
|
454
|
+
else:
|
|
455
|
+
win_h = max(1, int(round(win_w / target_aspect)))
|
|
456
|
+
|
|
457
|
+
# If image is larger than target, prefer at least target-sized crop on scroll axis.
|
|
458
|
+
if axis == 'y' and h >= target_h:
|
|
459
|
+
win_h = target_h
|
|
460
|
+
win_w = min(w, max(1, int(round(win_h * target_aspect))))
|
|
461
|
+
elif axis != 'y' and w >= target_w:
|
|
462
|
+
win_w = target_w
|
|
463
|
+
win_h = min(h, max(1, int(round(win_w / target_aspect))))
|
|
464
|
+
|
|
465
|
+
# Ensure stride is valid and not too tiny for large windows.
|
|
466
|
+
stride = int(stride)
|
|
467
|
+
if stride <= 0:
|
|
468
|
+
stride = max(1, (win_h if axis == 'y' else win_w) // 10)
|
|
469
|
+
if axis == 'y':
|
|
470
|
+
win = min(win_h, h)
|
|
471
|
+
positions = _window_positions(h, win, stride, reverse=reverse)
|
|
472
|
+
first_frame = img[positions[0]:positions[0] + win, :win_w]
|
|
473
|
+
else:
|
|
474
|
+
win = min(win_w, w)
|
|
475
|
+
positions = _window_positions(w, win, stride, reverse=reverse)
|
|
476
|
+
first_frame = img[:win_h, positions[0]:positions[0] + win]
|
|
477
|
+
|
|
478
|
+
first_frame_bgr = _to_bgr8(first_frame)
|
|
479
|
+
frame_h, frame_w = target_h, target_w
|
|
480
|
+
writer = cv2.VideoWriter(
|
|
481
|
+
out_video,
|
|
482
|
+
cv2.VideoWriter_fourcc(*'mp4v'),
|
|
483
|
+
max(1, int(fps)),
|
|
484
|
+
(frame_w, frame_h),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
if not writer.isOpened():
|
|
488
|
+
print(f"\n\tWARNING: Could not open video writer for {out_video}")
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
for pos in positions:
|
|
492
|
+
if axis == 'y':
|
|
493
|
+
frame = img[pos:pos + win, :win_w]
|
|
494
|
+
else:
|
|
495
|
+
frame = img[:win_h, pos:pos + win]
|
|
496
|
+
|
|
497
|
+
frame_bgr = _to_bgr8(frame)
|
|
498
|
+
if frame_bgr.shape[0] != frame_h or frame_bgr.shape[1] != frame_w:
|
|
499
|
+
frame_bgr = cv2.resize(frame_bgr, (frame_w, frame_h), interpolation=cv2.INTER_AREA)
|
|
500
|
+
writer.write(frame_bgr)
|
|
501
|
+
|
|
502
|
+
writer.release()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _export_waterfall_products(
|
|
506
|
+
sonObjs,
|
|
507
|
+
wcp,
|
|
508
|
+
wcm,
|
|
509
|
+
wcr,
|
|
510
|
+
wco,
|
|
511
|
+
mode_selection='auto',
|
|
512
|
+
ss_image=False,
|
|
513
|
+
ss_video=False,
|
|
514
|
+
di_image=False,
|
|
515
|
+
di_video=False,
|
|
516
|
+
fps=10,
|
|
517
|
+
video_resolution='1080p',
|
|
518
|
+
stride=64,
|
|
519
|
+
):
|
|
520
|
+
if not (ss_image or ss_video or di_image or di_video):
|
|
521
|
+
return
|
|
522
|
+
|
|
523
|
+
mode_flags = {
|
|
524
|
+
'wcp': bool(wcp),
|
|
525
|
+
'wcm': bool(wcm),
|
|
526
|
+
'src': bool(wcr),
|
|
527
|
+
'wco': bool(wco),
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
mode_selection = str(mode_selection).strip().lower()
|
|
531
|
+
if mode_selection in {'wcp', 'src', 'wcp+src'}:
|
|
532
|
+
if mode_selection == 'wcp':
|
|
533
|
+
modes = ['wcp']
|
|
534
|
+
elif mode_selection == 'src':
|
|
535
|
+
modes = ['src']
|
|
536
|
+
else:
|
|
537
|
+
modes = ['wcp', 'src']
|
|
538
|
+
else:
|
|
539
|
+
modes = [m for m, enabled in mode_flags.items() if enabled]
|
|
540
|
+
|
|
541
|
+
if len(modes) == 0:
|
|
542
|
+
print('\n\tWaterfall export requested, but no sonogram tile modes are enabled (wcp/wcm/wcr/wco). Skipping.')
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
res_key = str(video_resolution).strip().lower()
|
|
546
|
+
if res_key in {'4k', '2160p'}:
|
|
547
|
+
target_size = (3840, 2160)
|
|
548
|
+
elif res_key in {'1080p', 'fhd'}:
|
|
549
|
+
target_size = (1920, 1080)
|
|
550
|
+
elif res_key in {'720p', 'hd'}:
|
|
551
|
+
target_size = (1280, 720)
|
|
552
|
+
elif res_key in {'4xxp', '480p'}:
|
|
553
|
+
target_size = (854, 480)
|
|
554
|
+
else:
|
|
555
|
+
target_size = (1920, 1080)
|
|
556
|
+
|
|
557
|
+
print('\nGenerating waterfall products...')
|
|
558
|
+
|
|
559
|
+
# Group side-scan beams into port/star pairs by suffix key.
|
|
560
|
+
ss_groups = {}
|
|
561
|
+
down_beams = []
|
|
562
|
+
for son in sonObjs:
|
|
563
|
+
beam = str(getattr(son, 'beamName', ''))
|
|
564
|
+
if _is_sidescan_beam(beam):
|
|
565
|
+
key = _sidescan_group_key(beam)
|
|
566
|
+
group = ss_groups.setdefault(key, {})
|
|
567
|
+
if beam.startswith('ss_port'):
|
|
568
|
+
group['port'] = son
|
|
569
|
+
elif beam.startswith('ss_star'):
|
|
570
|
+
group['star'] = son
|
|
571
|
+
else:
|
|
572
|
+
down_beams.append(son)
|
|
573
|
+
|
|
574
|
+
# Side-scan: combine port/star into one waterfall and scroll top -> bottom.
|
|
575
|
+
if ss_image or ss_video:
|
|
576
|
+
ss_any = False
|
|
577
|
+
for group_key, group in sorted(ss_groups.items()):
|
|
578
|
+
if 'port' not in group or 'star' not in group:
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
proj_dir = group['port'].projDir
|
|
582
|
+
port_range_map = _get_chunk_range_map(group['port'])
|
|
583
|
+
star_range_map = _get_chunk_range_map(group['star'])
|
|
584
|
+
shared_ref = _range_reference_m(port_range_map)
|
|
585
|
+
if shared_ref is None:
|
|
586
|
+
shared_ref = _range_reference_m(star_range_map)
|
|
587
|
+
|
|
588
|
+
for mode in modes:
|
|
589
|
+
port_tiles = _collect_mode_tile_paths(group['port'], mode)
|
|
590
|
+
star_tiles = _collect_mode_tile_paths(group['star'], mode)
|
|
591
|
+
if len(port_tiles) == 0 or len(star_tiles) == 0:
|
|
592
|
+
continue
|
|
593
|
+
|
|
594
|
+
ss_any = True
|
|
595
|
+
|
|
596
|
+
combined = _build_sidescan_combined(
|
|
597
|
+
port_tiles,
|
|
598
|
+
star_tiles,
|
|
599
|
+
port_range_map=port_range_map,
|
|
600
|
+
star_range_map=star_range_map,
|
|
601
|
+
ref_range_m=shared_ref,
|
|
602
|
+
)
|
|
603
|
+
if combined is None:
|
|
604
|
+
continue
|
|
605
|
+
|
|
606
|
+
# Ensure both port and star fully fit inside output frame width.
|
|
607
|
+
combined = _fit_width(combined, target_size[0])
|
|
608
|
+
combined = _downscale_if_needed(combined)
|
|
609
|
+
|
|
610
|
+
out_dir = os.path.join(proj_dir, 'waterfall_exports', 'sidescan', mode)
|
|
611
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
612
|
+
|
|
613
|
+
if ss_image:
|
|
614
|
+
cv2.imwrite(os.path.join(out_dir, 'waterfall.png'), combined)
|
|
615
|
+
|
|
616
|
+
if ss_video:
|
|
617
|
+
_export_scrolling_video(
|
|
618
|
+
combined,
|
|
619
|
+
os.path.join(out_dir, 'waterfall_scroll_t2b.mp4'),
|
|
620
|
+
axis='y',
|
|
621
|
+
reverse=True,
|
|
622
|
+
fps=fps,
|
|
623
|
+
target_size=target_size,
|
|
624
|
+
stride=stride,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
if not ss_any:
|
|
628
|
+
print('\n\tNo side-scan tiles found for requested waterfall mode(s).')
|
|
629
|
+
print('\tEnable or select matching mode(s), e.g., WCP and/or SRC.')
|
|
630
|
+
|
|
631
|
+
# Down-imaging: generate per-beam waterfall and scroll right -> left.
|
|
632
|
+
if di_image or di_video:
|
|
633
|
+
di_any = False
|
|
634
|
+
for son in down_beams:
|
|
635
|
+
beam = str(getattr(son, 'beamName', 'unknown_beam'))
|
|
636
|
+
proj_dir = son.projDir
|
|
637
|
+
down_range_map = _get_chunk_range_map(son)
|
|
638
|
+
down_ref = _range_reference_m(down_range_map)
|
|
639
|
+
|
|
640
|
+
for mode in modes:
|
|
641
|
+
tiles = _collect_mode_tile_paths(son, mode)
|
|
642
|
+
if len(tiles) == 0:
|
|
643
|
+
continue
|
|
644
|
+
|
|
645
|
+
di_any = True
|
|
646
|
+
|
|
647
|
+
strip = _build_strip_from_tiles(
|
|
648
|
+
tiles,
|
|
649
|
+
chunk_range_map=down_range_map,
|
|
650
|
+
ref_range_m=down_ref,
|
|
651
|
+
)
|
|
652
|
+
if strip is None:
|
|
653
|
+
continue
|
|
654
|
+
|
|
655
|
+
strip = _downscale_if_needed(strip)
|
|
656
|
+
|
|
657
|
+
out_dir = os.path.join(proj_dir, 'waterfall_exports', 'down_imaging', mode)
|
|
658
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
659
|
+
|
|
660
|
+
if di_image:
|
|
661
|
+
cv2.imwrite(os.path.join(out_dir, f'{beam}_waterfall.png'), strip)
|
|
662
|
+
|
|
663
|
+
if di_video:
|
|
664
|
+
_export_scrolling_video(
|
|
665
|
+
strip,
|
|
666
|
+
os.path.join(out_dir, f'{beam}_waterfall_scroll.mp4'),
|
|
667
|
+
axis='x',
|
|
668
|
+
reverse=False,
|
|
669
|
+
fps=fps,
|
|
670
|
+
target_size=target_size,
|
|
671
|
+
stride=stride,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if not di_any:
|
|
675
|
+
print('\n\tNo down-imaging tiles found for requested waterfall mode(s).')
|
|
676
|
+
print('\tEnable or select matching mode(s), e.g., WCP and/or SRC.')
|
|
677
|
+
|
|
678
|
+
|
|
108
679
|
#===========================================
|
|
109
680
|
def read_master_func(logfilename='',
|
|
110
681
|
project_mode=0,
|
|
@@ -179,6 +750,14 @@ def read_master_func(logfilename='',
|
|
|
179
750
|
map_mosaic=0,
|
|
180
751
|
banklines=False,
|
|
181
752
|
side_scan_only=False,
|
|
753
|
+
waterfall_ss_image=False,
|
|
754
|
+
waterfall_ss_video=False,
|
|
755
|
+
waterfall_di_image=False,
|
|
756
|
+
waterfall_di_video=False,
|
|
757
|
+
waterfall_video_fps=10,
|
|
758
|
+
waterfall_video_resolution='1080p',
|
|
759
|
+
waterfall_mode_selection='auto',
|
|
760
|
+
waterfall_window_stride=64,
|
|
182
761
|
return_context=False,
|
|
183
762
|
**kwargs):
|
|
184
763
|
|
|
@@ -590,6 +1169,17 @@ def read_master_func(logfilename='',
|
|
|
590
1169
|
# Decode DAT file (varies by model) #
|
|
591
1170
|
############################################################################
|
|
592
1171
|
|
|
1172
|
+
wf_mode = str(waterfall_mode_selection).strip().lower()
|
|
1173
|
+
wf_requested = bool(waterfall_ss_image) or bool(waterfall_ss_video) or bool(waterfall_di_image) or bool(waterfall_di_video)
|
|
1174
|
+
if wf_requested:
|
|
1175
|
+
if wf_mode == 'wcp':
|
|
1176
|
+
wcp = True
|
|
1177
|
+
elif wf_mode == 'src':
|
|
1178
|
+
wcr = True
|
|
1179
|
+
elif wf_mode == 'wcp+src':
|
|
1180
|
+
wcp = True
|
|
1181
|
+
wcr = True
|
|
1182
|
+
|
|
593
1183
|
if (project_mode != 2):
|
|
594
1184
|
|
|
595
1185
|
#####
|
|
@@ -1708,6 +2298,32 @@ def read_master_func(logfilename='',
|
|
|
1708
2298
|
print("Time (s):", round(time.time() - start_time, ndigits=1))
|
|
1709
2299
|
printUsage()
|
|
1710
2300
|
|
|
2301
|
+
if bool(waterfall_ss_image) or bool(waterfall_ss_video) or bool(waterfall_di_image) or bool(waterfall_di_video):
|
|
2302
|
+
start_time = time.time()
|
|
2303
|
+
print("\nGenerating waterfall image/video exports...")
|
|
2304
|
+
try:
|
|
2305
|
+
_export_waterfall_products(
|
|
2306
|
+
sonObjs,
|
|
2307
|
+
wcp=wcp,
|
|
2308
|
+
wcm=wcm,
|
|
2309
|
+
wcr=wcr,
|
|
2310
|
+
wco=wco,
|
|
2311
|
+
mode_selection=waterfall_mode_selection,
|
|
2312
|
+
ss_image=bool(waterfall_ss_image),
|
|
2313
|
+
ss_video=bool(waterfall_ss_video),
|
|
2314
|
+
di_image=bool(waterfall_di_image),
|
|
2315
|
+
di_video=bool(waterfall_di_video),
|
|
2316
|
+
fps=int(waterfall_video_fps),
|
|
2317
|
+
video_resolution=str(waterfall_video_resolution),
|
|
2318
|
+
stride=int(waterfall_window_stride),
|
|
2319
|
+
)
|
|
2320
|
+
except Exception as e:
|
|
2321
|
+
print(f"\nWaterfall export failed: {e}")
|
|
2322
|
+
|
|
2323
|
+
print("Done!")
|
|
2324
|
+
print("Time (s):", round(time.time() - start_time, ndigits=1))
|
|
2325
|
+
printUsage()
|
|
2326
|
+
|
|
1711
2327
|
##############################################
|
|
1712
2328
|
# Let's pickle sonObj so we can reload later #
|
|
1713
2329
|
##############################################
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '5.5.1'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pingmapper
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.5.1
|
|
4
4
|
Summary: Open-source interface for processing recreation-grade side scan sonar datasets and reproducibly mapping benthic habitat
|
|
5
5
|
Author: Cameron Bodine, Daniel Buscombe, Iury T Simoes-Sousa
|
|
6
6
|
Author-email: bodine.cs@gmail.email
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '5.4.9'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pingmapper-5.4.9 → pingmapper-5.5.1}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|