pingmapper 5.5.2__tar.gz → 5.5.4__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.
Files changed (60) hide show
  1. {pingmapper-5.5.2 → pingmapper-5.5.4}/PKG-INFO +1 -1
  2. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/class_rectObj.py +74 -8
  3. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/class_sonObj.py +104 -18
  4. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/default_params.json +1 -0
  5. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/funcs_rectify.py +34 -0
  6. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/gui_main.py +8 -0
  7. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/main_readFiles.py +7 -0
  8. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/main_rectify.py +16 -0
  9. pingmapper-5.5.4/pingmapper/version.py +1 -0
  10. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper.egg-info/PKG-INFO +1 -1
  11. pingmapper-5.5.2/pingmapper/version.py +0 -1
  12. {pingmapper-5.5.2 → pingmapper-5.5.4}/LICENSE +0 -0
  13. {pingmapper-5.5.2 → pingmapper-5.5.4}/README.md +0 -0
  14. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/__init__.py +0 -0
  15. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/__main__.py +0 -0
  16. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/class_mapSubstrateObj.py +0 -0
  17. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/class_portstarObj.py +0 -0
  18. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/class_sonObj_nadirgaptest.py +0 -0
  19. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/doWork.py +0 -0
  20. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/funcs_common.py +0 -0
  21. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/funcs_model.py +0 -0
  22. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/main_mapSubstrate.py +0 -0
  23. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/nonGUI_batch_main.py +0 -0
  24. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/nonGui_main.py +0 -0
  25. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
  26. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
  27. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
  28. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/scratch/main.py +0 -0
  29. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/scratch/main_batchDirectory.py +0 -0
  30. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/self_check.py +0 -0
  31. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/test_PINGMapper.py +0 -0
  32. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/test_cli_self_check.py +0 -0
  33. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/test_dq_filter.py +0 -0
  34. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/test_time.py +0 -0
  35. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
  36. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
  37. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
  38. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
  39. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
  40. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
  41. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
  42. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
  43. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
  44. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
  45. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
  46. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
  47. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
  48. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
  49. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
  50. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
  51. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
  52. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/export_coverage.py +0 -0
  53. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper/utils/main_mosaic_transects.py +0 -0
  54. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper.egg-info/SOURCES.txt +0 -0
  55. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper.egg-info/dependency_links.txt +0 -0
  56. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper.egg-info/requires.txt +0 -0
  57. {pingmapper-5.5.2 → pingmapper-5.5.4}/pingmapper.egg-info/top_level.txt +0 -0
  58. {pingmapper-5.5.2 → pingmapper-5.5.4}/pyproject.toml +0 -0
  59. {pingmapper-5.5.2 → pingmapper-5.5.4}/setup.cfg +0 -0
  60. {pingmapper-5.5.2 → pingmapper-5.5.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.5.2
3
+ Version: 5.5.4
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
@@ -554,18 +554,22 @@ class rectObj(sonObj):
554
554
  fb[lats] = fb[ylat]
555
555
 
556
556
  if es not in fb.columns:
557
- if xutm in fb.columns:
558
- fb[es] = fb[xutm]
559
- elif lons in fb.columns and lats in fb.columns:
557
+ if lons in fb.columns and lats in fb.columns:
558
+ # Prefer projecting lon/lat through self.trans (the correct
559
+ # target EPSG) rather than reusing a raw xutm column, which
560
+ # may hold a different (e.g. Humminbird-internal Web Mercator)
561
+ # coordinate system and silently corrupt downstream geometry.
560
562
  e_fb, _ = self.trans(fb[lons].to_numpy(), fb[lats].to_numpy())
561
563
  fb[es] = e_fb
564
+ elif xutm in fb.columns:
565
+ fb[es] = fb[xutm]
562
566
 
563
567
  if ns not in fb.columns:
564
- if yutm in fb.columns:
565
- fb[ns] = fb[yutm]
566
- elif lons in fb.columns and lats in fb.columns:
568
+ if lons in fb.columns and lats in fb.columns:
567
569
  _, n_fb = self.trans(fb[lons].to_numpy(), fb[lats].to_numpy())
568
570
  fb[ns] = n_fb
571
+ elif yutm in fb.columns:
572
+ fb[ns] = fb[yutm]
569
573
 
570
574
  if 'cog' not in fb.columns:
571
575
  if 'instr_heading' in fb.columns:
@@ -1099,6 +1103,26 @@ class rectObj(sonObj):
1099
1103
  else:
1100
1104
  continue
1101
1105
 
1106
+ # Guard against spline overshoot: the linear spline fit to range
1107
+ # extent coordinates can extrapolate far beyond the sonar's actual
1108
+ # range near gaps left by upstream filtering (time/speed/coord
1109
+ # outlier filters). Fall back to the geometrically-derived
1110
+ # (un-smoothed) range extent point for any ping whose smoothed
1111
+ # point lands unreasonably far from its trackline origin.
1112
+ if {'record_num', 'range_lon', 'range_lat', 'utm_es', 'utm_ns', 'range'}.issubset(schunkDF.columns):
1113
+ e_smth_chk, n_smth_chk = self.trans(smthChunk['range_lons'].to_numpy(), smthChunk['range_lats'].to_numpy())
1114
+ chk = schunkDF[['record_num', 'utm_es', 'utm_ns', 'range', 'range_lon', 'range_lat']].set_index('record_num')
1115
+ chk = chk.reindex(smthChunk['record_num'].to_numpy())
1116
+
1117
+ dist = np.hypot(e_smth_chk - chk['utm_es'].to_numpy(), n_smth_chk - chk['utm_ns'].to_numpy())
1118
+ max_expected = np.maximum(chk['range'].to_numpy(), 1.0) * 3.0 + 50.0
1119
+ bad = np.isfinite(dist) & (dist > max_expected)
1120
+ n_bad = int(np.sum(bad))
1121
+ if n_bad > 0:
1122
+ print(f"\n _interpRangeCoords: corrected {n_bad} spline-overshoot range extent point(s) in chunk {chunk}.")
1123
+ smthChunk.loc[bad, 'range_lons'] = chk['range_lon'].to_numpy()[bad]
1124
+ smthChunk.loc[bad, 'range_lats'] = chk['range_lat'].to_numpy()[bad]
1125
+
1102
1126
  rs_chunks.append(smthChunk.copy())
1103
1127
 
1104
1128
  ##################################################
@@ -2724,6 +2748,8 @@ class rectObj(sonObj):
2724
2748
  img = self.sonDat.copy()
2725
2749
  if son:
2726
2750
  img = self._reserve_zero_for_nodata(img, preserve_zeros=True)
2751
+ if bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)):
2752
+ img = self._apply_display_enhancements(img)
2727
2753
  apply_post_rect_colormap = use_16bit and self._rect_colormap_selected(son=son)
2728
2754
  source_scale_bounds = None
2729
2755
  if apply_post_rect_colormap:
@@ -2747,6 +2773,8 @@ class rectObj(sonObj):
2747
2773
  out = np.flip(np.flip(np.flip(out,1),0),1)
2748
2774
  if use_16bit:
2749
2775
  out = out.astype(np.float32)
2776
+ elif bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)):
2777
+ out = out.astype(np.float32)
2750
2778
  else:
2751
2779
  out = out.astype('uint8')
2752
2780
 
@@ -2780,7 +2808,10 @@ class rectObj(sonObj):
2780
2808
  out16 = out16_raw
2781
2809
  self._write_rect_geotiff(gtiff, out16, epsg, transform, colormap=None)
2782
2810
  else:
2783
- out8 = np.clip(out, 0, 255).astype(np.uint8)
2811
+ if bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)):
2812
+ out8 = self._convert_son_dat_to_uint8(out)
2813
+ else:
2814
+ out8 = np.clip(out, 0, 255).astype(np.uint8)
2784
2815
  self._write_rect_geotiff(gtiff, out8, epsg, transform, colormap=self.son_colorMap)
2785
2816
 
2786
2817
  del out, img
@@ -2815,6 +2846,8 @@ class rectObj(sonObj):
2815
2846
  img = self.sonDat
2816
2847
  if son:
2817
2848
  img = self._reserve_zero_for_nodata(img, preserve_zeros=True)
2849
+ if bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)):
2850
+ img = self._apply_display_enhancements(img)
2818
2851
  apply_post_rect_colormap = son and use_16bit and self._rect_colormap_selected(son=son)
2819
2852
  source_scale_bounds = None
2820
2853
  if apply_post_rect_colormap:
@@ -2887,6 +2920,8 @@ class rectObj(sonObj):
2887
2920
  out = np.flip(np.flip(np.flip(out,1),0),1)
2888
2921
  if son and use_16bit:
2889
2922
  out = out.astype(np.float32)
2923
+ elif son and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False))):
2924
+ out = out.astype(np.float32)
2890
2925
  else:
2891
2926
  out = out.astype('uint8')
2892
2927
 
@@ -2926,7 +2961,10 @@ class rectObj(sonObj):
2926
2961
  self._write_rect_geotiff(gtiff, out16, epsg, transform, colormap=None)
2927
2962
  else:
2928
2963
  colormap = self.son_colorMap if son else class_colormap
2929
- out8 = np.clip(out, 0, 255).astype(np.uint8)
2964
+ if son and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False))):
2965
+ out8 = self._convert_son_dat_to_uint8(out)
2966
+ else:
2967
+ out8 = np.clip(out, 0, 255).astype(np.uint8)
2930
2968
  self._write_rect_geotiff(gtiff, out8, epsg, transform, colormap=colormap)
2931
2969
 
2932
2970
  del out
@@ -2986,6 +3024,34 @@ class rectObj(sonObj):
2986
3024
  # Set output name
2987
3025
  f_out = f.replace('temp.tif', '.tif')
2988
3026
 
3027
+ # Sanity check the georeferenced extent before warping. A corrupt/outlier
3028
+ # coordinate upstream can produce a geotransform spanning thousands of km,
3029
+ # which causes gdal.Warp to attempt allocating a raster with an absurd
3030
+ # pixel count (surfacing as a misleading "free disk space" GDAL error).
3031
+ src = gdal.Open(f)
3032
+ if src is not None:
3033
+ gt = src.GetGeoTransform()
3034
+ width_m = abs(gt[1]) * src.RasterXSize
3035
+ height_m = abs(gt[5]) * src.RasterYSize
3036
+ src = None
3037
+
3038
+ est_cols = width_m / pix_res
3039
+ est_rows = height_m / pix_res
3040
+
3041
+ _MAX_PIXELS = 5e8 # ~500 million pixels safety cap
3042
+ if est_cols * est_rows > _MAX_PIXELS:
3043
+ print(
3044
+ f"\nWARNING: Skipping pixel resolution resize for {os.path.basename(f)}. "
3045
+ f"Georeferenced extent ({width_m:.1f} x {height_m:.1f} m) at {pix_res} m resolution "
3046
+ f"would require ~{est_cols*est_rows:,.0f} pixels. This indicates a corrupt or outlier "
3047
+ f"ping coordinate in this chunk. Keeping the un-resized GeoTiff instead."
3048
+ )
3049
+ try:
3050
+ os.rename(f, f_out)
3051
+ except OSError:
3052
+ pass
3053
+ return
3054
+
2989
3055
  # Reopen f and warp to pix_res
2990
3056
  t = gdal.Warp(f_out, f, xRes = pix_res, yRes = pix_res, targetAlignedPixels=True)
2991
3057
 
@@ -1319,6 +1319,44 @@ class sonObj(object):
1319
1319
  if not use_db and not use_clahe:
1320
1320
  return sonDat
1321
1321
 
1322
+ arr = self._apply_display_db_transform(sonDat)
1323
+ if arr.size == 0:
1324
+ return arr
1325
+
1326
+ valid = arr > 0
1327
+ if not np.any(valid):
1328
+ return arr
1329
+
1330
+ if use_clahe:
1331
+ clip_limit = float(getattr(self, 'sonar_clahe_clip_limit', 0.01))
1332
+ if (not np.isfinite(clip_limit)) or clip_limit <= 0:
1333
+ clip_limit = 0.01
1334
+
1335
+ global_bounds = None
1336
+ if bool(getattr(self, 'sonar_clahe_global', True)):
1337
+ global_bounds = self._ensure_clahe_global_bounds()
1338
+
1339
+ if global_bounds is not None:
1340
+ arr01 = self._normalize_with_bounds(arr, global_bounds[0], global_bounds[1])
1341
+ else:
1342
+ max_val = float(np.nanmax(arr))
1343
+ if not (np.isfinite(max_val) and max_val > 0):
1344
+ return arr
1345
+ arr01 = np.clip(arr / max_val, 0.0, 1.0)
1346
+
1347
+ try:
1348
+ arr01 = exposure.equalize_adapthist(arr01, clip_limit=clip_limit)
1349
+ arr = np.asarray(arr01, dtype=np.float32) * 255.0
1350
+ except MemoryError:
1351
+ arr = self._apply_clahe_chunked(arr01, clip_limit)
1352
+ except np.core._exceptions._ArrayMemoryError:
1353
+ arr = self._apply_clahe_chunked(arr01, clip_limit)
1354
+
1355
+ # Preserve no-data convention for empty samples.
1356
+ arr[~valid] = 0.0
1357
+ return arr
1358
+
1359
+ def _apply_display_db_transform(self, sonDat):
1322
1360
  arr = np.asarray(sonDat, dtype=np.float32)
1323
1361
  if arr.size == 0:
1324
1362
  return arr
@@ -1330,7 +1368,7 @@ class sonObj(object):
1330
1368
  if not np.any(valid):
1331
1369
  return arr
1332
1370
 
1333
- if use_db:
1371
+ if bool(getattr(self, 'sonar_db_transform', False)):
1334
1372
  # Use the smallest positive intensity as the log floor to avoid
1335
1373
  # collapsing zeros to -inf and to preserve relative low amplitudes.
1336
1374
  floor = float(np.nanmin(arr[valid]))
@@ -1340,25 +1378,73 @@ class sonObj(object):
1340
1378
  arr = 20.0 * np.log10(arr)
1341
1379
  arr = arr - np.nanmin(arr)
1342
1380
 
1343
- if use_clahe:
1344
- max_val = float(np.nanmax(arr))
1345
- if np.isfinite(max_val) and max_val > 0:
1346
- arr01 = np.clip(arr / max_val, 0.0, 1.0)
1347
- clip_limit = float(getattr(self, 'sonar_clahe_clip_limit', 0.01))
1348
- if (not np.isfinite(clip_limit)) or clip_limit <= 0:
1349
- clip_limit = 0.01
1350
- try:
1351
- arr01 = exposure.equalize_adapthist(arr01, clip_limit=clip_limit)
1352
- arr = np.asarray(arr01, dtype=np.float32)
1353
- except MemoryError:
1354
- arr = self._apply_clahe_chunked(arr01, clip_limit)
1355
- except np.core._exceptions._ArrayMemoryError:
1356
- arr = self._apply_clahe_chunked(arr01, clip_limit)
1357
-
1358
- # Preserve no-data convention for empty samples.
1359
1381
  arr[~valid] = 0.0
1360
1382
  return arr
1361
1383
 
1384
+ def _ensure_clahe_global_bounds(self):
1385
+ if not bool(getattr(self, 'sonar_clahe_global', True)):
1386
+ return None
1387
+
1388
+ cache = getattr(self, '_sonar_clahe_global_bounds', None)
1389
+ if isinstance(cache, tuple) and len(cache) == 2:
1390
+ lo, hi = cache
1391
+ if np.isfinite(lo) and np.isfinite(hi) and hi > lo:
1392
+ return cache
1393
+
1394
+ if not hasattr(self, 'sonMetaDF'):
1395
+ self._loadSonMeta()
1396
+
1397
+ if not hasattr(self, 'sonMetaDF') or self.sonMetaDF is None or len(self.sonMetaDF) == 0:
1398
+ return None
1399
+
1400
+ saved_sonDat = getattr(self, 'sonDat', None)
1401
+ saved_sonDat16 = getattr(self, 'sonDat16', None)
1402
+ saved_pingMax = getattr(self, 'pingMax', None)
1403
+
1404
+ chunk_ids = pd.unique(self.sonMetaDF['chunk_id'])
1405
+ sample_cap = int(getattr(self, 'sonar_clahe_global_sample_cap', 200000))
1406
+ if sample_cap <= 0:
1407
+ sample_cap = 200000
1408
+
1409
+ sampled_vals = []
1410
+ for chunk in chunk_ids:
1411
+ self._getScanChunkSingle(int(chunk))
1412
+ arr = self._apply_display_db_transform(self.sonDat)
1413
+ valid = arr > 0
1414
+ if not np.any(valid):
1415
+ continue
1416
+
1417
+ vals = arr[valid].astype(np.float32, copy=False)
1418
+ if vals.size > sample_cap:
1419
+ idx = np.linspace(0, vals.size - 1, sample_cap, dtype=np.int64)
1420
+ vals = vals[idx]
1421
+ sampled_vals.append(vals)
1422
+
1423
+ if len(sampled_vals) == 0:
1424
+ self.sonDat = saved_sonDat
1425
+ self.sonDat16 = saved_sonDat16
1426
+ self.pingMax = saved_pingMax
1427
+ return None
1428
+
1429
+ pooled = np.concatenate(sampled_vals)
1430
+ lo = float(np.nanpercentile(pooled, 1.0))
1431
+ hi = float(np.nanpercentile(pooled, 99.5))
1432
+ if (not np.isfinite(lo)) or (not np.isfinite(hi)) or hi <= lo:
1433
+ lo = float(np.nanmin(pooled))
1434
+ hi = float(np.nanmax(pooled))
1435
+
1436
+ if (not np.isfinite(lo)) or (not np.isfinite(hi)) or hi <= lo:
1437
+ self.sonDat = saved_sonDat
1438
+ self.sonDat16 = saved_sonDat16
1439
+ self.pingMax = saved_pingMax
1440
+ return None
1441
+
1442
+ self._sonar_clahe_global_bounds = (lo, hi)
1443
+ self.sonDat = saved_sonDat
1444
+ self.sonDat16 = saved_sonDat16
1445
+ self.pingMax = saved_pingMax
1446
+ return self._sonar_clahe_global_bounds
1447
+
1362
1448
  def _apply_clahe_chunked(self, arr01, clip_limit):
1363
1449
  """Apply CLAHE in row-wise stripes to reduce peak memory usage."""
1364
1450
  src = np.asarray(arr01, dtype=np.float32)
@@ -1378,7 +1464,7 @@ class sonObj(object):
1378
1464
  end = min(start + stripe_rows, h)
1379
1465
  stripe = src[start:end, :]
1380
1466
  stripe_eq = exposure.equalize_adapthist(stripe, clip_limit=clip_limit)
1381
- out[start:end, :] = np.asarray(stripe_eq, dtype=np.float32)
1467
+ out[start:end, :] = np.asarray(stripe_eq, dtype=np.float32) * 255.0
1382
1468
 
1383
1469
  return out
1384
1470
 
@@ -27,6 +27,7 @@
27
27
  "tone_gain":1.0,
28
28
  "sonar_db_transform":false,
29
29
  "sonar_clahe":false,
30
+ "sonar_clahe_global":true,
30
31
  "sonar_clahe_clip_limit":0.01,
31
32
  "wcp":false,
32
33
  "wcm":true,
@@ -149,11 +149,44 @@ def smoothTrackline(projDir='', x_offset='', y_offset='', nchunk ='', cog=True,
149
149
  # filter = int(nchunk*0.1)
150
150
 
151
151
  # print('\n\n\ntransect:', name, 'filter:', filter)
152
+ # Snapshot raw coordinates before _interpTrack() may drop duplicates
153
+ # in-place, so we can validate/repair the smoothed output below.
154
+ raw_lon = group['lon'].copy()
155
+ raw_lat = group['lat'].copy()
156
+
152
157
  smoothed = son._interpTrack(df=group, dropDup=True, filt=filter, deg=3)
153
158
 
154
159
  # smooth trackline fit
155
160
  # if smoothed is not None:
156
161
  if len(smoothed.columns) > 4:
162
+ # Guard against cubic spline overshoot (Runge's phenomenon) across
163
+ # long or gappy transects (e.g. gaps left by time/speed/coord
164
+ # outlier filters). Fall back to the raw GPS fix for any ping
165
+ # whose smoothed position was displaced further than the
166
+ # transect's own spatial extent would allow.
167
+ raw_e, raw_n = son.trans(raw_lon.to_numpy(), raw_lat.to_numpy())
168
+ raw_e = pd.Series(raw_e, index=raw_lon.index).reindex(smoothed.index).to_numpy()
169
+ raw_n = pd.Series(raw_n, index=raw_lat.index).reindex(smoothed.index).to_numpy()
170
+
171
+ smth_e, smth_n = son.trans(smoothed['lons'].to_numpy(), smoothed['lats'].to_numpy())
172
+
173
+ dist = np.hypot(smth_e - raw_e, smth_n - raw_n)
174
+ span = np.hypot(np.nanmax(raw_e) - np.nanmin(raw_e), np.nanmax(raw_n) - np.nanmin(raw_n))
175
+ max_allowed = max(span, 50.0) * 2.0
176
+
177
+ bad = np.isfinite(dist) & (dist > max_allowed)
178
+ n_bad = int(np.sum(bad))
179
+ if n_bad > 0:
180
+ print(f"\n smoothTrackline: corrected {n_bad} spline-overshoot trackline point(s) in transect {name}.")
181
+ fix_lon = pd.Series(raw_lon.to_numpy(), index=raw_lon.index).reindex(smoothed.index).to_numpy()
182
+ fix_lat = pd.Series(raw_lat.to_numpy(), index=raw_lat.index).reindex(smoothed.index).to_numpy()
183
+ smoothed.loc[bad, 'lons'] = fix_lon[bad]
184
+ smoothed.loc[bad, 'lats'] = fix_lat[bad]
185
+ if 'utm_es' in smoothed.columns and 'utm_ns' in smoothed.columns:
186
+ e_fix, n_fix = son.trans(smoothed.loc[bad, 'lons'].to_numpy(), smoothed.loc[bad, 'lats'].to_numpy())
187
+ smoothed.loc[bad, 'utm_es'] = e_fix
188
+ smoothed.loc[bad, 'utm_ns'] = n_fix
189
+
157
190
  smoothed['transect'] = int(name)
158
191
  sDF = pd.concat([sDF, smoothed], ignore_index=False)
159
192
 
@@ -166,6 +199,7 @@ def smoothTrackline(projDir='', x_offset='', y_offset='', nchunk ='', cog=True,
166
199
 
167
200
  del smoothed
168
201
 
202
+
169
203
  # Save sonDF
170
204
  if len(transect_dropped) > 0:
171
205
 
@@ -321,6 +321,12 @@ def gui(batch: bool):
321
321
  default=default_params.get('sonar_clahe', False),
322
322
  tooltip=tip_clahe,
323
323
  )
324
+ check_sonar_clahe_global = sg.Checkbox(
325
+ 'Global CLAHE reference',
326
+ key='sonar_clahe_global',
327
+ default=default_params.get('sonar_clahe_global', True),
328
+ tooltip='Use one project-wide intensity reference before CLAHE so chunk brightness stays consistent. This adds a prepass, but reduces chunk-to-chunk contrast shifts.',
329
+ )
324
330
  text_clahe_clip = sg.Text('CLAHE Clip Limit', size=(20,1))
325
331
  in_clahe_clip = sg.Input(
326
332
  key='sonar_clahe_clip_limit',
@@ -335,6 +341,7 @@ def gui(batch: bool):
335
341
  [text_egn_gain, slide_egn_gain],
336
342
  [check_sonar_db],
337
343
  [check_sonar_clahe],
344
+ [check_sonar_clahe_global],
338
345
  [text_clahe_clip, in_clahe_clip],
339
346
  ],
340
347
  pad=0,
@@ -837,6 +844,7 @@ def gui(batch: bool):
837
844
  'tone_gain':float(values['tone_gain']),
838
845
  'sonar_db_transform':values['sonar_db_transform'],
839
846
  'sonar_clahe':values['sonar_clahe'],
847
+ 'sonar_clahe_global':values['sonar_clahe_global'],
840
848
  'sonar_clahe_clip_limit':float(values['sonar_clahe_clip_limit']),
841
849
  'wcp':values['wcp'],
842
850
  'wcm':values['wcm'],
@@ -720,6 +720,7 @@ def read_master_func(logfilename='',
720
720
  tone_gain=1.0,
721
721
  sonar_db_transform=False,
722
722
  sonar_clahe=False,
723
+ sonar_clahe_global=True,
723
724
  sonar_clahe_clip_limit=0.01,
724
725
  wcp=False,
725
726
  wcm=False,
@@ -1209,7 +1210,10 @@ def read_master_func(logfilename='',
1209
1210
  son.tone_gain = tone_gain
1210
1211
  son.sonar_db_transform = bool(sonar_db_transform)
1211
1212
  son.sonar_clahe = bool(sonar_clahe)
1213
+ son.sonar_clahe_global = bool(sonar_clahe_global)
1212
1214
  son.sonar_clahe_clip_limit = float(sonar_clahe_clip_limit)
1215
+ if son.sonar_clahe:
1216
+ son._ensure_clahe_global_bounds()
1213
1217
  # Do range crop, if necessary
1214
1218
  if cropRange > 0.0:
1215
1219
  if file_type == '.xtf' and _is_sidescan_beam(getattr(son, 'beamName', '')):
@@ -1405,7 +1409,10 @@ def read_master_func(logfilename='',
1405
1409
  son.tone_gain = tone_gain
1406
1410
  son.sonar_db_transform = bool(sonar_db_transform)
1407
1411
  son.sonar_clahe = bool(sonar_clahe)
1412
+ son.sonar_clahe_global = bool(sonar_clahe_global)
1408
1413
  son.sonar_clahe_clip_limit = float(sonar_clahe_clip_limit)
1414
+ if son.sonar_clahe:
1415
+ son._ensure_clahe_global_bounds()
1409
1416
 
1410
1417
  beam = son.beamName
1411
1418
  if wcr:
@@ -126,6 +126,10 @@ def rectify_master_func(logfilename='',
126
126
  egn_stretch_factor=1,
127
127
  tone_gamma=1.0,
128
128
  tone_gain=1.0,
129
+ sonar_db_transform=False,
130
+ sonar_clahe=False,
131
+ sonar_clahe_global=True,
132
+ sonar_clahe_clip_limit=0.01,
129
133
  wcp=False,
130
134
  wcm=False,
131
135
  wcr=False,
@@ -368,6 +372,12 @@ def rectify_master_func(logfilename='',
368
372
  son.rect_wcr = rect_wcr
369
373
  son.export_16bit = bool(export_16bit) and (not bool(getattr(son, 'son8bit', True)))
370
374
  son.export_colormap_uint8 = bool(export_colormap_uint8)
375
+ son.sonar_db_transform = bool(sonar_db_transform)
376
+ son.sonar_clahe = bool(sonar_clahe)
377
+ son.sonar_clahe_global = bool(sonar_clahe_global)
378
+ son.sonar_clahe_clip_limit = float(sonar_clahe_clip_limit)
379
+ if son.sonar_clahe:
380
+ son._ensure_clahe_global_bounds()
371
381
 
372
382
  # ############################################################################
373
383
  # # Rectify Heading sonar imagery - Pingwise, not Rubbersheeting #
@@ -462,6 +472,12 @@ def rectify_master_func(logfilename='',
462
472
  for son in portstar:
463
473
  son.rect_wcp = rect_wcp
464
474
  son.rect_wcr = rect_wcr
475
+ son.sonar_db_transform = bool(sonar_db_transform)
476
+ son.sonar_clahe = bool(sonar_clahe)
477
+ son.sonar_clahe_global = bool(sonar_clahe_global)
478
+ son.sonar_clahe_clip_limit = float(sonar_clahe_clip_limit)
479
+ if son.sonar_clahe:
480
+ son._ensure_clahe_global_bounds()
465
481
 
466
482
  if (rect_wcp and rubberSheeting) or (rect_wcr and rubberSheeting):
467
483
  # Always use COG for rubber sheeting
@@ -0,0 +1 @@
1
+ __version__ = '5.5.4'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.5.2
3
+ Version: 5.5.4
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.5.2'
File without changes
File without changes
File without changes
File without changes
File without changes