pingmapper 5.5.1__tar.gz → 5.5.3__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.1 → pingmapper-5.5.3}/PKG-INFO +1 -1
  2. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/class_rectObj.py +16 -2
  3. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/class_sonObj.py +270 -16
  4. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/class_sonObj_nadirgaptest.py +26 -2
  5. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/default_params.json +4 -0
  6. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/gui_main.py +44 -2
  7. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/main_readFiles.py +19 -0
  8. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/main_rectify.py +16 -0
  9. pingmapper-5.5.3/pingmapper/version.py +1 -0
  10. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper.egg-info/PKG-INFO +1 -1
  11. pingmapper-5.5.1/pingmapper/version.py +0 -1
  12. {pingmapper-5.5.1 → pingmapper-5.5.3}/LICENSE +0 -0
  13. {pingmapper-5.5.1 → pingmapper-5.5.3}/README.md +0 -0
  14. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/__init__.py +0 -0
  15. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/__main__.py +0 -0
  16. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/class_mapSubstrateObj.py +0 -0
  17. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/class_portstarObj.py +0 -0
  18. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/doWork.py +0 -0
  19. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/funcs_common.py +0 -0
  20. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/funcs_model.py +0 -0
  21. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/funcs_rectify.py +0 -0
  22. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/main_mapSubstrate.py +0 -0
  23. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/nonGUI_batch_main.py +0 -0
  24. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/nonGui_main.py +0 -0
  25. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
  26. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
  27. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
  28. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/scratch/main.py +0 -0
  29. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/scratch/main_batchDirectory.py +0 -0
  30. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/self_check.py +0 -0
  31. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/test_PINGMapper.py +0 -0
  32. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/test_cli_self_check.py +0 -0
  33. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/test_dq_filter.py +0 -0
  34. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/test_time.py +0 -0
  35. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
  36. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
  37. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
  38. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
  39. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
  40. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
  41. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
  42. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
  43. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
  44. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
  45. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
  46. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
  47. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
  48. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
  49. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
  50. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
  51. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
  52. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/export_coverage.py +0 -0
  53. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper/utils/main_mosaic_transects.py +0 -0
  54. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper.egg-info/SOURCES.txt +0 -0
  55. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper.egg-info/dependency_links.txt +0 -0
  56. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper.egg-info/requires.txt +0 -0
  57. {pingmapper-5.5.1 → pingmapper-5.5.3}/pingmapper.egg-info/top_level.txt +0 -0
  58. {pingmapper-5.5.1 → pingmapper-5.5.3}/pyproject.toml +0 -0
  59. {pingmapper-5.5.1 → pingmapper-5.5.3}/setup.cfg +0 -0
  60. {pingmapper-5.5.1 → pingmapper-5.5.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.5.1
3
+ Version: 5.5.3
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
@@ -2724,6 +2724,8 @@ class rectObj(sonObj):
2724
2724
  img = self.sonDat.copy()
2725
2725
  if son:
2726
2726
  img = self._reserve_zero_for_nodata(img, preserve_zeros=True)
2727
+ if bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)):
2728
+ img = self._apply_display_enhancements(img)
2727
2729
  apply_post_rect_colormap = use_16bit and self._rect_colormap_selected(son=son)
2728
2730
  source_scale_bounds = None
2729
2731
  if apply_post_rect_colormap:
@@ -2747,6 +2749,8 @@ class rectObj(sonObj):
2747
2749
  out = np.flip(np.flip(np.flip(out,1),0),1)
2748
2750
  if use_16bit:
2749
2751
  out = out.astype(np.float32)
2752
+ elif bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)):
2753
+ out = out.astype(np.float32)
2750
2754
  else:
2751
2755
  out = out.astype('uint8')
2752
2756
 
@@ -2780,7 +2784,10 @@ class rectObj(sonObj):
2780
2784
  out16 = out16_raw
2781
2785
  self._write_rect_geotiff(gtiff, out16, epsg, transform, colormap=None)
2782
2786
  else:
2783
- out8 = np.clip(out, 0, 255).astype(np.uint8)
2787
+ if bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)):
2788
+ out8 = self._convert_son_dat_to_uint8(out)
2789
+ else:
2790
+ out8 = np.clip(out, 0, 255).astype(np.uint8)
2784
2791
  self._write_rect_geotiff(gtiff, out8, epsg, transform, colormap=self.son_colorMap)
2785
2792
 
2786
2793
  del out, img
@@ -2815,6 +2822,8 @@ class rectObj(sonObj):
2815
2822
  img = self.sonDat
2816
2823
  if son:
2817
2824
  img = self._reserve_zero_for_nodata(img, preserve_zeros=True)
2825
+ if bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)):
2826
+ img = self._apply_display_enhancements(img)
2818
2827
  apply_post_rect_colormap = son and use_16bit and self._rect_colormap_selected(son=son)
2819
2828
  source_scale_bounds = None
2820
2829
  if apply_post_rect_colormap:
@@ -2887,6 +2896,8 @@ class rectObj(sonObj):
2887
2896
  out = np.flip(np.flip(np.flip(out,1),0),1)
2888
2897
  if son and use_16bit:
2889
2898
  out = out.astype(np.float32)
2899
+ elif son and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False))):
2900
+ out = out.astype(np.float32)
2890
2901
  else:
2891
2902
  out = out.astype('uint8')
2892
2903
 
@@ -2926,7 +2937,10 @@ class rectObj(sonObj):
2926
2937
  self._write_rect_geotiff(gtiff, out16, epsg, transform, colormap=None)
2927
2938
  else:
2928
2939
  colormap = self.son_colorMap if son else class_colormap
2929
- out8 = np.clip(out, 0, 255).astype(np.uint8)
2940
+ if son and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False))):
2941
+ out8 = self._convert_son_dat_to_uint8(out)
2942
+ else:
2943
+ out8 = np.clip(out, 0, 255).astype(np.uint8)
2930
2944
  self._write_rect_geotiff(gtiff, out8, epsg, transform, colormap=colormap)
2931
2945
 
2932
2946
  del out
@@ -30,6 +30,7 @@
30
30
  # SOFTWARE.
31
31
 
32
32
  import os, sys
33
+ from skimage import exposure
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__))
@@ -708,10 +709,34 @@ class sonObj(object):
708
709
  else:
709
710
  time_table = pd.read_csv(time_table)
710
711
 
712
+ if 'start_seconds' not in time_table.columns or 'end_seconds' not in time_table.columns:
713
+ raise ValueError("time_table must contain 'start_seconds' and 'end_seconds' columns.")
714
+
715
+ # Normalize to numeric and drop unusable rows.
716
+ time_table['start_seconds'] = pd.to_numeric(time_table['start_seconds'], errors='coerce')
717
+ time_table['end_seconds'] = pd.to_numeric(time_table['end_seconds'], errors='coerce')
718
+ time_table = time_table.dropna(subset=['start_seconds', 'end_seconds'])
719
+
720
+ if time_table.empty:
721
+ return sonDF
722
+
723
+ son_time = pd.to_numeric(sonDF[time_col], errors='coerce')
724
+ son_min = son_time.min()
725
+
726
+ # Auto-detect relative clip ranges when sonar time is epoch-like and
727
+ # clip-table values are small. This allows 0..N seconds-from-start.
728
+ tt_max = np.nanmax(np.abs(time_table[['start_seconds', 'end_seconds']].to_numpy(dtype=float)))
729
+ use_relative_seconds = np.isfinite(son_min) and son_min > 1e8 and np.isfinite(tt_max) and tt_max < 1e7
730
+
731
+ offset = float(son_min) if use_relative_seconds else 0.0
732
+
711
733
  for i, row in time_table.iterrows():
712
734
 
713
- start = row['start_seconds']
714
- end = row['end_seconds']
735
+ start = float(row['start_seconds']) + offset
736
+ end = float(row['end_seconds']) + offset
737
+
738
+ if end < start:
739
+ start, end = end, start
715
740
 
716
741
  # dfFilt = sonDF[(sonDF['time_s'] >= start) & (sonDF['time_s'] <= end)]
717
742
  sonDF.loc[(sonDF[time_col] >= start) & (sonDF[time_col] <= end) & (sonDF[filtCol] == True), filtTimeCol] = True
@@ -1238,6 +1263,19 @@ class sonObj(object):
1238
1263
  if np.isfinite(wf) and wf > 0:
1239
1264
  dat = dat * (2.0 ** (-float(wf)))
1240
1265
 
1266
+ if bool(getattr(self, '_use_cerulean_power_scale', False)):
1267
+ dat = dat.astype(np.float32, copy=False)
1268
+ if hasattr(self, 'minPwrDb') and hasattr(self, 'maxPwrDb') and i < len(self.minPwrDb) and i < len(self.maxPwrDb):
1269
+ min_db = self.minPwrDb[i]
1270
+ max_db = self.maxPwrDb[i]
1271
+ if np.isfinite(min_db) and np.isfinite(max_db) and (max_db > min_db):
1272
+ # Issue #206: Cerulean pwr_results[] are normalized
1273
+ # uint16 values that must be rescaled with min/max dB.
1274
+ dat_db = float(min_db) + (dat / 65535.0) * (float(max_db) - float(min_db))
1275
+ # Keep values non-negative for downstream intensity
1276
+ # processing while preserving the per-ping dB span.
1277
+ dat = dat_db - float(min_db)
1278
+
1241
1279
  n_samples = min(len(dat), sonDat.shape[0])
1242
1280
  if n_samples <= 0:
1243
1281
  skipped_reads += 1
@@ -1268,6 +1306,168 @@ class sonObj(object):
1268
1306
  self.sonDat = self._convert_son_dat_to_uint8(sonDat)
1269
1307
  return
1270
1308
 
1309
+ def _apply_display_enhancements(self, sonDat):
1310
+ """Optional display-stage intensity transforms before uint8 mapping.
1311
+
1312
+ These transforms are intended to improve perceptual contrast of exported
1313
+ sonograms while keeping the processing pipeline opt-in and backward-
1314
+ compatible by default.
1315
+ """
1316
+ use_db = bool(getattr(self, 'sonar_db_transform', False))
1317
+ use_clahe = bool(getattr(self, 'sonar_clahe', False))
1318
+
1319
+ if not use_db and not use_clahe:
1320
+ return sonDat
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):
1360
+ arr = np.asarray(sonDat, dtype=np.float32)
1361
+ if arr.size == 0:
1362
+ return arr
1363
+
1364
+ arr[~np.isfinite(arr)] = 0.0
1365
+ arr[arr < 0] = 0.0
1366
+
1367
+ valid = arr > 0
1368
+ if not np.any(valid):
1369
+ return arr
1370
+
1371
+ if bool(getattr(self, 'sonar_db_transform', False)):
1372
+ # Use the smallest positive intensity as the log floor to avoid
1373
+ # collapsing zeros to -inf and to preserve relative low amplitudes.
1374
+ floor = float(np.nanmin(arr[valid]))
1375
+ if (not np.isfinite(floor)) or floor <= 0:
1376
+ floor = 1.0
1377
+ arr = np.maximum(arr, floor)
1378
+ arr = 20.0 * np.log10(arr)
1379
+ arr = arr - np.nanmin(arr)
1380
+
1381
+ arr[~valid] = 0.0
1382
+ return arr
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
+
1448
+ def _apply_clahe_chunked(self, arr01, clip_limit):
1449
+ """Apply CLAHE in row-wise stripes to reduce peak memory usage."""
1450
+ src = np.asarray(arr01, dtype=np.float32)
1451
+ h, w = src.shape
1452
+ if h == 0 or w == 0:
1453
+ return src
1454
+
1455
+ # Keep per-stripe working set bounded. equalize_adapthist promotes to
1456
+ # float64 internally, so we target moderate stripe sizes.
1457
+ stripe_rows = int(getattr(self, 'sonar_clahe_rows_per_chunk', 512))
1458
+ if stripe_rows <= 0:
1459
+ stripe_rows = 512
1460
+ stripe_rows = min(stripe_rows, h)
1461
+
1462
+ out = np.empty_like(src, dtype=np.float32)
1463
+ for start in range(0, h, stripe_rows):
1464
+ end = min(start + stripe_rows, h)
1465
+ stripe = src[start:end, :]
1466
+ stripe_eq = exposure.equalize_adapthist(stripe, clip_limit=clip_limit)
1467
+ out[start:end, :] = np.asarray(stripe_eq, dtype=np.float32) * 255.0
1468
+
1469
+ return out
1470
+
1271
1471
  def _apply_tvg(self, sonDat):
1272
1472
  sonDat = np.asarray(sonDat, dtype=np.float32)
1273
1473
 
@@ -1979,7 +2179,8 @@ class sonObj(object):
1979
2179
  k,
1980
2180
  imgOutPrefix,
1981
2181
  tileFile='.jpg',
1982
- colormap=False):
2182
+ colormap=False,
2183
+ apply_display_enhancement=True):
1983
2184
  '''
1984
2185
  Using currently saved ping ping returns stored in self.sonDAT,
1985
2186
  saves an unrectified image of the sonar echogram.
@@ -2016,7 +2217,15 @@ class sonObj(object):
2016
2217
  data = self._prepare_export_uint16(self.sonDat)
2017
2218
  data = self._reserve_zero_for_nodata(data)
2018
2219
  else:
2019
- data = self.sonDat.astype('uint8') # Get the sonar data
2220
+ arr = self.sonDat
2221
+ if apply_display_enhancement and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False))):
2222
+ arr = self._apply_display_enhancements(arr)
2223
+
2224
+ if np.issubdtype(np.asarray(arr).dtype, np.floating):
2225
+ data = self._convert_son_dat_to_uint8(arr)
2226
+ else:
2227
+ data = np.clip(arr, 0, 255).astype(np.uint8)
2228
+
2020
2229
  data = self._reserve_zero_for_nodata(data)
2021
2230
 
2022
2231
  # File name zero padding
@@ -2039,7 +2248,8 @@ class sonObj(object):
2039
2248
  k,
2040
2249
  imgOutPrefix,
2041
2250
  tileFile='.jpg',
2042
- colormap=False):
2251
+ colormap=False,
2252
+ apply_display_enhancement=True):
2043
2253
  '''
2044
2254
  Using currently saved ping ping returns stored in self.sonDAT,
2045
2255
  saves an unrectified image of the sonar echogram.
@@ -2077,7 +2287,15 @@ class sonObj(object):
2077
2287
  data = self._prepare_export_uint16(self.sonDat)
2078
2288
  data = self._reserve_zero_for_nodata(data)
2079
2289
  else:
2080
- data = self.sonDat.astype('uint8') # Get the sonar data
2290
+ arr = self.sonDat
2291
+ if apply_display_enhancement and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False))):
2292
+ arr = self._apply_display_enhancements(arr)
2293
+
2294
+ if np.issubdtype(np.asarray(arr).dtype, np.floating):
2295
+ data = self._convert_son_dat_to_uint8(arr)
2296
+ else:
2297
+ data = np.clip(arr, 0, 255).astype(np.uint8)
2298
+
2081
2299
  data = self._reserve_zero_for_nodata(data)
2082
2300
 
2083
2301
  # File name zero padding
@@ -2147,40 +2365,44 @@ class sonObj(object):
2147
2365
  if self.wcp:
2148
2366
  # Do speed correction
2149
2367
  export_16bit = bool(getattr(self, 'export_16bit', False)) and not bool(getattr(self, 'son8bit', False))
2150
- self._doSpdCor(chunk, spdCor=spdCor, mask_shdw=mask_shdw, maxCrop=maxCrop, do_egn=self.egn, stretch_wcp=True, integer=not export_16bit)
2368
+ pre_spd_enhance = (not export_16bit) and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)))
2369
+ self._doSpdCor(chunk, spdCor=spdCor, mask_shdw=mask_shdw, maxCrop=maxCrop, do_egn=self.egn, stretch_wcp=True, integer=not export_16bit, pre_spd_enhance=pre_spd_enhance)
2151
2370
 
2152
2371
  if self.sonDat is not np.nan:
2153
- self._writeTilesPlot(chunk, imgOutPrefix='wcp', tileFile=tileFile, colormap=True)
2372
+ self._writeTilesPlot(chunk, imgOutPrefix='wcp', tileFile=tileFile, colormap=True, apply_display_enhancement=not pre_spd_enhance)
2154
2373
  else:
2155
2374
  pass
2156
2375
 
2157
2376
  if self.wcm:
2158
2377
  # Do speed correction
2159
2378
  export_16bit = bool(getattr(self, 'export_16bit', False)) and not bool(getattr(self, 'son8bit', False))
2160
- self._doSpdCor(chunk, spdCor=spdCor, mask_shdw=mask_shdw, mask_wc=True, maxCrop=maxCrop, do_egn=self.egn, stretch_wcp=True, integer=not export_16bit)
2379
+ pre_spd_enhance = (not export_16bit) and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)))
2380
+ self._doSpdCor(chunk, spdCor=spdCor, mask_shdw=mask_shdw, mask_wc=True, maxCrop=maxCrop, do_egn=self.egn, stretch_wcp=True, integer=not export_16bit, pre_spd_enhance=pre_spd_enhance)
2161
2381
 
2162
2382
  if self.sonDat is not np.nan:
2163
- self._writeTilesPlot(chunk, imgOutPrefix='wcm', tileFile=tileFile, colormap=True)
2383
+ self._writeTilesPlot(chunk, imgOutPrefix='wcm', tileFile=tileFile, colormap=True, apply_display_enhancement=not pre_spd_enhance)
2164
2384
  else:
2165
2385
  pass
2166
2386
 
2167
2387
  if self.wcr_src:
2168
2388
  # Do speed correction
2169
2389
  export_16bit = bool(getattr(self, 'export_16bit', False)) and not bool(getattr(self, 'son8bit', False))
2170
- self._doSpdCor(chunk, spdCor=spdCor, mask_shdw=mask_shdw, src=True, maxCrop=maxCrop, do_egn=self.egn, stretch_wcp=True, integer=not export_16bit)
2390
+ pre_spd_enhance = (not export_16bit) and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)))
2391
+ self._doSpdCor(chunk, spdCor=spdCor, mask_shdw=mask_shdw, src=True, maxCrop=maxCrop, do_egn=self.egn, stretch_wcp=True, integer=not export_16bit, pre_spd_enhance=pre_spd_enhance)
2171
2392
 
2172
2393
  if self.sonDat is not np.nan:
2173
- self._writeTilesPlot(chunk, imgOutPrefix='src', tileFile=tileFile, colormap=True)
2394
+ self._writeTilesPlot(chunk, imgOutPrefix='src', tileFile=tileFile, colormap=True, apply_display_enhancement=not pre_spd_enhance)
2174
2395
  else:
2175
2396
  pass
2176
2397
 
2177
2398
  if self.wco:
2178
2399
  # Do speed correction
2179
2400
  export_16bit = bool(getattr(self, 'export_16bit', False)) and not bool(getattr(self, 'son8bit', False))
2180
- self._doSpdCor(chunk, spdCor=spdCor, mask_bed=True, maxCrop=maxCrop, do_egn=self.egn, stretch_wcp=True, integer=not export_16bit)
2401
+ pre_spd_enhance = (not export_16bit) and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)))
2402
+ self._doSpdCor(chunk, spdCor=spdCor, mask_bed=True, maxCrop=maxCrop, do_egn=self.egn, stretch_wcp=True, integer=not export_16bit, pre_spd_enhance=pre_spd_enhance)
2181
2403
 
2182
2404
  if self.sonDat is not np.nan:
2183
- self._writeTilesPlot(chunk, imgOutPrefix='wco', tileFile=tileFile, colormap=True)
2405
+ self._writeTilesPlot(chunk, imgOutPrefix='wco', tileFile=tileFile, colormap=True, apply_display_enhancement=not pre_spd_enhance)
2184
2406
  else:
2185
2407
  pass
2186
2408
 
@@ -2200,7 +2422,8 @@ class sonObj(object):
2200
2422
  son=True,
2201
2423
  integer=True,
2202
2424
  do_egn=False,
2203
- stretch_wcp=False):
2425
+ stretch_wcp=False,
2426
+ pre_spd_enhance=False):
2204
2427
 
2205
2428
  if not hasattr(self, 'sonMetaDF'):
2206
2429
  self._loadSonMeta()
@@ -2248,6 +2471,10 @@ class sonObj(object):
2248
2471
 
2249
2472
  sonDat = self.sonDat
2250
2473
 
2474
+ if pre_spd_enhance and not export_16bit:
2475
+ sonDat = self._apply_display_enhancements(sonDat)
2476
+ self.sonDat = sonDat
2477
+
2251
2478
  if spdCor == 0:
2252
2479
  # Don't do speed correction
2253
2480
  pass
@@ -2310,7 +2537,12 @@ class sonObj(object):
2310
2537
  clip=False, preserve_range=True)#.astype('uint8')
2311
2538
 
2312
2539
  if integer:
2313
- self.sonDat = sonDat.astype('uint8')
2540
+ if pre_spd_enhance and np.issubdtype(np.asarray(sonDat).dtype, np.floating):
2541
+ # CLAHE outputs 0..1 floats; convert with robust scaling so
2542
+ # display-enhanced data does not collapse to near-black.
2543
+ self.sonDat = self._convert_son_dat_to_uint8(sonDat)
2544
+ else:
2545
+ self.sonDat = sonDat.astype('uint8')
2314
2546
  else:
2315
2547
  self.sonDat = sonDat
2316
2548
 
@@ -2413,6 +2645,12 @@ class sonObj(object):
2413
2645
  self._use_jsf_weighting = True
2414
2646
  self._ensure_jsf_global_scale_max(sonMetaAll)
2415
2647
 
2648
+ self._use_cerulean_power_scale = False
2649
+ if 'min_pwr_db' in sonMeta.columns and 'max_pwr_db' in sonMeta.columns:
2650
+ self.minPwrDb = pd.to_numeric(sonMeta['min_pwr_db'], errors='coerce').to_numpy(dtype=float)
2651
+ self.maxPwrDb = pd.to_numeric(sonMeta['max_pwr_db'], errors='coerce').to_numpy(dtype=float)
2652
+ self._use_cerulean_power_scale = True
2653
+
2416
2654
  # Load chunk's sonar data into memory
2417
2655
  self._loadSonChunk()
2418
2656
  # Do PPDRC filter
@@ -2427,7 +2665,12 @@ class sonObj(object):
2427
2665
  del self.bytesPerSample
2428
2666
  if hasattr(self, 'weightingFactor'):
2429
2667
  del self.weightingFactor
2668
+ if hasattr(self, 'minPwrDb'):
2669
+ del self.minPwrDb
2670
+ if hasattr(self, 'maxPwrDb'):
2671
+ del self.maxPwrDb
2430
2672
  self._use_jsf_weighting = False
2673
+ self._use_cerulean_power_scale = False
2431
2674
 
2432
2675
  return
2433
2676
 
@@ -2477,6 +2720,12 @@ class sonObj(object):
2477
2720
  self._use_jsf_weighting = True
2478
2721
  self._ensure_jsf_global_scale_max(sonMetaAll_full)
2479
2722
 
2723
+ self._use_cerulean_power_scale = False
2724
+ if 'min_pwr_db' in sonMeta.columns and 'max_pwr_db' in sonMeta.columns:
2725
+ self.minPwrDb = pd.to_numeric(sonMeta['min_pwr_db'], errors='coerce').to_numpy(dtype=float)
2726
+ self.maxPwrDb = pd.to_numeric(sonMeta['max_pwr_db'], errors='coerce').to_numpy(dtype=float)
2727
+ self._use_cerulean_power_scale = True
2728
+
2480
2729
  # Load chunk's sonar data into memory
2481
2730
  self._loadSonChunk()
2482
2731
 
@@ -2489,7 +2738,12 @@ class sonObj(object):
2489
2738
  del self.bytesPerSample
2490
2739
  if hasattr(self, 'weightingFactor'):
2491
2740
  del self.weightingFactor
2741
+ if hasattr(self, 'minPwrDb'):
2742
+ del self.minPwrDb
2743
+ if hasattr(self, 'maxPwrDb'):
2744
+ del self.maxPwrDb
2492
2745
  self._use_jsf_weighting = False
2746
+ self._use_cerulean_power_scale = False
2493
2747
 
2494
2748
  return
2495
2749
 
@@ -686,10 +686,34 @@ class sonObj(object):
686
686
  else:
687
687
  time_table = pd.read_csv(time_table)
688
688
 
689
+ if 'start_seconds' not in time_table.columns or 'end_seconds' not in time_table.columns:
690
+ raise ValueError("time_table must contain 'start_seconds' and 'end_seconds' columns.")
691
+
692
+ # Normalize to numeric and drop unusable rows.
693
+ time_table['start_seconds'] = pd.to_numeric(time_table['start_seconds'], errors='coerce')
694
+ time_table['end_seconds'] = pd.to_numeric(time_table['end_seconds'], errors='coerce')
695
+ time_table = time_table.dropna(subset=['start_seconds', 'end_seconds'])
696
+
697
+ if time_table.empty:
698
+ return sonDF
699
+
700
+ son_time = pd.to_numeric(sonDF[time_col], errors='coerce')
701
+ son_min = son_time.min()
702
+
703
+ # Auto-detect relative clip ranges when sonar time is epoch-like and
704
+ # clip-table values are small. This allows 0..N seconds-from-start.
705
+ tt_max = np.nanmax(np.abs(time_table[['start_seconds', 'end_seconds']].to_numpy(dtype=float)))
706
+ use_relative_seconds = np.isfinite(son_min) and son_min > 1e8 and np.isfinite(tt_max) and tt_max < 1e7
707
+
708
+ offset = float(son_min) if use_relative_seconds else 0.0
709
+
689
710
  for i, row in time_table.iterrows():
690
711
 
691
- start = row['start_seconds']
692
- end = row['end_seconds']
712
+ start = float(row['start_seconds']) + offset
713
+ end = float(row['end_seconds']) + offset
714
+
715
+ if end < start:
716
+ start, end = end, start
693
717
 
694
718
  # dfFilt = sonDF[(sonDF['time_s'] >= start) & (sonDF['time_s'] <= end)]
695
719
  sonDF.loc[(sonDF[time_col] >= start) & (sonDF[time_col] <= end) & (sonDF[filtCol] == True), filtTimeCol] = True
@@ -25,6 +25,10 @@
25
25
  "egn_stretch_factor":0.5,
26
26
  "tone_gamma":1.0,
27
27
  "tone_gain":1.0,
28
+ "sonar_db_transform":false,
29
+ "sonar_clahe":false,
30
+ "sonar_clahe_global":true,
31
+ "sonar_clahe_clip_limit":0.01,
28
32
  "wcp":false,
29
33
  "wcm":true,
30
34
  "wcr":false,
@@ -207,7 +207,7 @@ def gui(batch: bool):
207
207
  tip_speed_min = ml_tip('Minimum vessel speed filter in m/s. 0=no minimum speed filter.')
208
208
  tip_speed_max = ml_tip('Maximum vessel speed filter in m/s. 0=no maximum speed filter.')
209
209
  tip_aoi = ml_tip('Optional polygon shapefile (.shp) to spatially filter sonar records to area of interest.')
210
- tip_time_filter = ml_tip('Option to filter sonar records by time range using start/end seconds.')
210
+ tip_time_filter = ml_tip('Filter by start/end time ranges. Use either relative seconds from recording start (e.g., 0-2000) or absolute Unix seconds.')
211
211
  # Cropping
212
212
  text_crop = sg.Text('Crop Range [m]', size=(22,1))
213
213
  in_crop = sg.Input(key='cropRange', default_text=default_params['cropRange'], size=(10,1), tooltip=tip_crop)
@@ -280,6 +280,9 @@ def gui(batch: bool):
280
280
  tip_egn_factor = ml_tip('Percent clip amount used by EGN Stretch Factor when Percent Clip is selected.')
281
281
  tip_gamma = ml_tip('Tone curve control. <1 brightens mid-tones, >1 darkens mid-tones.')
282
282
  tip_gain = ml_tip('Linear brightness multiplier. >1 brightens overall intensity, <1 darkens.')
283
+ tip_db = ml_tip('Apply 20*log10 transform before final 8-bit mapping. Helps reveal low-amplitude detail.')
284
+ tip_clahe = ml_tip('Apply adaptive histogram equalization (CLAHE) after dB transform for local contrast boost.')
285
+ tip_clahe_clip = ml_tip('CLAHE clip limit. Lower values are gentler; higher values increase local contrast.')
283
286
  # EGN
284
287
  check_egn = sg.Checkbox('Empiracal Gain Normalization (EGN)', key='egn', default=default_params['egn'], tooltip=tip_egn)
285
288
 
@@ -306,8 +309,43 @@ def gui(batch: bool):
306
309
  default_value=float(default_params['tone_gain']),
307
310
  tooltip=tip_gain,
308
311
  )
312
+ check_sonar_db = sg.Checkbox(
313
+ 'Display dB Transform',
314
+ key='sonar_db_transform',
315
+ default=default_params.get('sonar_db_transform', False),
316
+ tooltip=tip_db,
317
+ )
318
+ check_sonar_clahe = sg.Checkbox(
319
+ 'Display CLAHE',
320
+ key='sonar_clahe',
321
+ default=default_params.get('sonar_clahe', False),
322
+ tooltip=tip_clahe,
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
+ )
330
+ text_clahe_clip = sg.Text('CLAHE Clip Limit', size=(20,1))
331
+ in_clahe_clip = sg.Input(
332
+ key='sonar_clahe_clip_limit',
333
+ default_text=default_params.get('sonar_clahe_clip_limit', 0.01),
334
+ size=(10,1),
335
+ tooltip=tip_clahe_clip,
336
+ )
309
337
 
310
- col_egn_1 = sg.Column([[text_egn_gamma, slide_egn_gamma], [text_egn_gain, slide_egn_gain]], pad=0)
338
+ col_egn_1 = sg.Column(
339
+ [
340
+ [text_egn_gamma, slide_egn_gamma],
341
+ [text_egn_gain, slide_egn_gain],
342
+ [check_sonar_db],
343
+ [check_sonar_clahe],
344
+ [check_sonar_clahe_global],
345
+ [text_clahe_clip, in_clahe_clip],
346
+ ],
347
+ pad=0,
348
+ )
311
349
  col_egn_2 = sg.Column([[check_egn], [text_egn_stretch, combo_egn_stretch], [text_egn_factor, in_egn_factor]], pad=0)
312
350
  # Add to layout
313
351
  layout.append([sg.HorizontalSeparator()])
@@ -804,6 +842,10 @@ def gui(batch: bool):
804
842
  'egn_stretch_factor':float(values['egn_stretch_factor']),
805
843
  'tone_gamma':float(values['tone_gamma']),
806
844
  'tone_gain':float(values['tone_gain']),
845
+ 'sonar_db_transform':values['sonar_db_transform'],
846
+ 'sonar_clahe':values['sonar_clahe'],
847
+ 'sonar_clahe_global':values['sonar_clahe_global'],
848
+ 'sonar_clahe_clip_limit':float(values['sonar_clahe_clip_limit']),
807
849
  'wcp':values['wcp'],
808
850
  'wcm':values['wcm'],
809
851
  'wcr':values['wcr'],
@@ -718,6 +718,10 @@ def read_master_func(logfilename='',
718
718
  egn_stretch_factor=1,
719
719
  tone_gamma=1.0,
720
720
  tone_gain=1.0,
721
+ sonar_db_transform=False,
722
+ sonar_clahe=False,
723
+ sonar_clahe_global=True,
724
+ sonar_clahe_clip_limit=0.01,
721
725
  wcp=False,
722
726
  wcm=False,
723
727
  wcr=False,
@@ -1038,6 +1042,9 @@ def read_master_func(logfilename='',
1038
1042
  son.son8bit = sonar_obj.son8bit
1039
1043
  if hasattr(sonar_obj, 'sample_dtype'):
1040
1044
  son.sample_dtype = sonar_obj.sample_dtype
1045
+ elif file_type == '.svlog':
1046
+ # Cerulean pwr_results are uint16 values in little-endian order.
1047
+ son.sample_dtype = '<u2'
1041
1048
  if hasattr(sonar_obj, 'export_raw_16bit'):
1042
1049
  son.export_raw_16bit = sonar_obj.export_raw_16bit
1043
1050
  if hasattr(son, 'sample_dtype'):
@@ -1201,6 +1208,12 @@ def read_master_func(logfilename='',
1201
1208
  son.cropRange = cropRange
1202
1209
  son.tone_gamma = tone_gamma
1203
1210
  son.tone_gain = tone_gain
1211
+ son.sonar_db_transform = bool(sonar_db_transform)
1212
+ son.sonar_clahe = bool(sonar_clahe)
1213
+ son.sonar_clahe_global = bool(sonar_clahe_global)
1214
+ son.sonar_clahe_clip_limit = float(sonar_clahe_clip_limit)
1215
+ if son.sonar_clahe:
1216
+ son._ensure_clahe_global_bounds()
1204
1217
  # Do range crop, if necessary
1205
1218
  if cropRange > 0.0:
1206
1219
  if file_type == '.xtf' and _is_sidescan_beam(getattr(son, 'beamName', '')):
@@ -1394,6 +1407,12 @@ def read_master_func(logfilename='',
1394
1407
  son.wcp = wcp
1395
1408
  son.tone_gamma = tone_gamma
1396
1409
  son.tone_gain = tone_gain
1410
+ son.sonar_db_transform = bool(sonar_db_transform)
1411
+ son.sonar_clahe = bool(sonar_clahe)
1412
+ son.sonar_clahe_global = bool(sonar_clahe_global)
1413
+ son.sonar_clahe_clip_limit = float(sonar_clahe_clip_limit)
1414
+ if son.sonar_clahe:
1415
+ son._ensure_clahe_global_bounds()
1397
1416
 
1398
1417
  beam = son.beamName
1399
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.3'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.5.1
3
+ Version: 5.5.3
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.1'
File without changes
File without changes
File without changes
File without changes
File without changes