pingmapper 5.5.0__tar.gz → 5.5.2__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.0 → pingmapper-5.5.2}/PKG-INFO +1 -1
  2. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/class_mapSubstrateObj.py +1 -1
  3. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/class_portstarObj.py +9 -3
  4. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/class_rectObj.py +5 -5
  5. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/class_sonObj.py +185 -17
  6. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/class_sonObj_nadirgaptest.py +27 -3
  7. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/default_params.json +3 -0
  8. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/gui_main.py +36 -2
  9. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/main_readFiles.py +12 -0
  10. pingmapper-5.5.2/pingmapper/version.py +1 -0
  11. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper.egg-info/PKG-INFO +1 -1
  12. pingmapper-5.5.0/pingmapper/version.py +0 -1
  13. {pingmapper-5.5.0 → pingmapper-5.5.2}/LICENSE +0 -0
  14. {pingmapper-5.5.0 → pingmapper-5.5.2}/README.md +0 -0
  15. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/__init__.py +0 -0
  16. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/__main__.py +0 -0
  17. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/doWork.py +0 -0
  18. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/funcs_common.py +0 -0
  19. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/funcs_model.py +0 -0
  20. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/funcs_rectify.py +0 -0
  21. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/main_mapSubstrate.py +0 -0
  22. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/main_rectify.py +0 -0
  23. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/nonGUI_batch_main.py +0 -0
  24. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/nonGui_main.py +0 -0
  25. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
  26. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
  27. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
  28. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/scratch/main.py +0 -0
  29. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/scratch/main_batchDirectory.py +0 -0
  30. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/self_check.py +0 -0
  31. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/test_PINGMapper.py +0 -0
  32. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/test_cli_self_check.py +0 -0
  33. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/test_dq_filter.py +0 -0
  34. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/test_time.py +0 -0
  35. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
  36. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
  37. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
  38. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
  39. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
  40. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
  41. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
  42. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
  43. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
  44. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
  45. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
  46. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
  47. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
  48. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
  49. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
  50. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
  51. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
  52. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/export_coverage.py +0 -0
  53. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper/utils/main_mosaic_transects.py +0 -0
  54. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper.egg-info/SOURCES.txt +0 -0
  55. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper.egg-info/dependency_links.txt +0 -0
  56. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper.egg-info/requires.txt +0 -0
  57. {pingmapper-5.5.0 → pingmapper-5.5.2}/pingmapper.egg-info/top_level.txt +0 -0
  58. {pingmapper-5.5.0 → pingmapper-5.5.2}/pyproject.toml +0 -0
  59. {pingmapper-5.5.0 → pingmapper-5.5.2}/setup.cfg +0 -0
  60. {pingmapper-5.5.0 → pingmapper-5.5.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.5.0
3
+ Version: 5.5.2
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.cm.get_cmap('viridis')
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[subID]
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 Polygon
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("Substrate = 0")
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.cm.get_cmap(self.son_colorMap_name)(norm_data)
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.cm.get_cmap(self.son_colorMap_name)(norm_data)[:, :, :3] * 255.0, 0, 255).astype(np.uint8)
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.cm.get_cmap(self.son_colorMap_name)(norm_data)[:, :, :3] * 65535.0, 0, 65535).astype(np.uint16)
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.cm.get_cmap(self.son_colorMap_name)(norm_data)[:, :, :3] * 255.0, 0, 255).astype(np.uint8)
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.cm.get_cmap(self.son_colorMap_name)(norm_data)[:, :, :3] * 65535.0, 0, 65535).astype(np.uint16)
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:
@@ -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,82 @@ 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 = np.asarray(sonDat, dtype=np.float32)
1323
+ if arr.size == 0:
1324
+ return arr
1325
+
1326
+ arr[~np.isfinite(arr)] = 0.0
1327
+ arr[arr < 0] = 0.0
1328
+
1329
+ valid = arr > 0
1330
+ if not np.any(valid):
1331
+ return arr
1332
+
1333
+ if use_db:
1334
+ # Use the smallest positive intensity as the log floor to avoid
1335
+ # collapsing zeros to -inf and to preserve relative low amplitudes.
1336
+ floor = float(np.nanmin(arr[valid]))
1337
+ if (not np.isfinite(floor)) or floor <= 0:
1338
+ floor = 1.0
1339
+ arr = np.maximum(arr, floor)
1340
+ arr = 20.0 * np.log10(arr)
1341
+ arr = arr - np.nanmin(arr)
1342
+
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
+ arr[~valid] = 0.0
1360
+ return arr
1361
+
1362
+ def _apply_clahe_chunked(self, arr01, clip_limit):
1363
+ """Apply CLAHE in row-wise stripes to reduce peak memory usage."""
1364
+ src = np.asarray(arr01, dtype=np.float32)
1365
+ h, w = src.shape
1366
+ if h == 0 or w == 0:
1367
+ return src
1368
+
1369
+ # Keep per-stripe working set bounded. equalize_adapthist promotes to
1370
+ # float64 internally, so we target moderate stripe sizes.
1371
+ stripe_rows = int(getattr(self, 'sonar_clahe_rows_per_chunk', 512))
1372
+ if stripe_rows <= 0:
1373
+ stripe_rows = 512
1374
+ stripe_rows = min(stripe_rows, h)
1375
+
1376
+ out = np.empty_like(src, dtype=np.float32)
1377
+ for start in range(0, h, stripe_rows):
1378
+ end = min(start + stripe_rows, h)
1379
+ stripe = src[start:end, :]
1380
+ stripe_eq = exposure.equalize_adapthist(stripe, clip_limit=clip_limit)
1381
+ out[start:end, :] = np.asarray(stripe_eq, dtype=np.float32)
1382
+
1383
+ return out
1384
+
1271
1385
  def _apply_tvg(self, sonDat):
1272
1386
  sonDat = np.asarray(sonDat, dtype=np.float32)
1273
1387
 
@@ -1872,7 +1986,7 @@ class sonObj(object):
1872
1986
  return out
1873
1987
 
1874
1988
  def _colorize_array_batched(self, norm_data, valid_mask, cmap_name, scale_max, out_dtype):
1875
- cmap = plt.cm.get_cmap(cmap_name)
1989
+ cmap = plt.get_cmap(cmap_name)
1876
1990
  height, width = norm_data.shape
1877
1991
  out = np.zeros((height, width, 3), dtype=out_dtype)
1878
1992
 
@@ -1979,7 +2093,8 @@ class sonObj(object):
1979
2093
  k,
1980
2094
  imgOutPrefix,
1981
2095
  tileFile='.jpg',
1982
- colormap=False):
2096
+ colormap=False,
2097
+ apply_display_enhancement=True):
1983
2098
  '''
1984
2099
  Using currently saved ping ping returns stored in self.sonDAT,
1985
2100
  saves an unrectified image of the sonar echogram.
@@ -2016,7 +2131,15 @@ class sonObj(object):
2016
2131
  data = self._prepare_export_uint16(self.sonDat)
2017
2132
  data = self._reserve_zero_for_nodata(data)
2018
2133
  else:
2019
- data = self.sonDat.astype('uint8') # Get the sonar data
2134
+ arr = self.sonDat
2135
+ if apply_display_enhancement and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False))):
2136
+ arr = self._apply_display_enhancements(arr)
2137
+
2138
+ if np.issubdtype(np.asarray(arr).dtype, np.floating):
2139
+ data = self._convert_son_dat_to_uint8(arr)
2140
+ else:
2141
+ data = np.clip(arr, 0, 255).astype(np.uint8)
2142
+
2020
2143
  data = self._reserve_zero_for_nodata(data)
2021
2144
 
2022
2145
  # File name zero padding
@@ -2039,7 +2162,8 @@ class sonObj(object):
2039
2162
  k,
2040
2163
  imgOutPrefix,
2041
2164
  tileFile='.jpg',
2042
- colormap=False):
2165
+ colormap=False,
2166
+ apply_display_enhancement=True):
2043
2167
  '''
2044
2168
  Using currently saved ping ping returns stored in self.sonDAT,
2045
2169
  saves an unrectified image of the sonar echogram.
@@ -2077,7 +2201,15 @@ class sonObj(object):
2077
2201
  data = self._prepare_export_uint16(self.sonDat)
2078
2202
  data = self._reserve_zero_for_nodata(data)
2079
2203
  else:
2080
- data = self.sonDat.astype('uint8') # Get the sonar data
2204
+ arr = self.sonDat
2205
+ if apply_display_enhancement and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False))):
2206
+ arr = self._apply_display_enhancements(arr)
2207
+
2208
+ if np.issubdtype(np.asarray(arr).dtype, np.floating):
2209
+ data = self._convert_son_dat_to_uint8(arr)
2210
+ else:
2211
+ data = np.clip(arr, 0, 255).astype(np.uint8)
2212
+
2081
2213
  data = self._reserve_zero_for_nodata(data)
2082
2214
 
2083
2215
  # File name zero padding
@@ -2147,40 +2279,44 @@ class sonObj(object):
2147
2279
  if self.wcp:
2148
2280
  # Do speed correction
2149
2281
  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)
2282
+ pre_spd_enhance = (not export_16bit) and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)))
2283
+ 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
2284
 
2152
2285
  if self.sonDat is not np.nan:
2153
- self._writeTilesPlot(chunk, imgOutPrefix='wcp', tileFile=tileFile, colormap=True)
2286
+ self._writeTilesPlot(chunk, imgOutPrefix='wcp', tileFile=tileFile, colormap=True, apply_display_enhancement=not pre_spd_enhance)
2154
2287
  else:
2155
2288
  pass
2156
2289
 
2157
2290
  if self.wcm:
2158
2291
  # Do speed correction
2159
2292
  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)
2293
+ pre_spd_enhance = (not export_16bit) and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)))
2294
+ 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
2295
 
2162
2296
  if self.sonDat is not np.nan:
2163
- self._writeTilesPlot(chunk, imgOutPrefix='wcm', tileFile=tileFile, colormap=True)
2297
+ self._writeTilesPlot(chunk, imgOutPrefix='wcm', tileFile=tileFile, colormap=True, apply_display_enhancement=not pre_spd_enhance)
2164
2298
  else:
2165
2299
  pass
2166
2300
 
2167
2301
  if self.wcr_src:
2168
2302
  # Do speed correction
2169
2303
  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)
2304
+ pre_spd_enhance = (not export_16bit) and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)))
2305
+ 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
2306
 
2172
2307
  if self.sonDat is not np.nan:
2173
- self._writeTilesPlot(chunk, imgOutPrefix='src', tileFile=tileFile, colormap=True)
2308
+ self._writeTilesPlot(chunk, imgOutPrefix='src', tileFile=tileFile, colormap=True, apply_display_enhancement=not pre_spd_enhance)
2174
2309
  else:
2175
2310
  pass
2176
2311
 
2177
2312
  if self.wco:
2178
2313
  # Do speed correction
2179
2314
  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)
2315
+ pre_spd_enhance = (not export_16bit) and (bool(getattr(self, 'sonar_db_transform', False)) or bool(getattr(self, 'sonar_clahe', False)))
2316
+ 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
2317
 
2182
2318
  if self.sonDat is not np.nan:
2183
- self._writeTilesPlot(chunk, imgOutPrefix='wco', tileFile=tileFile, colormap=True)
2319
+ self._writeTilesPlot(chunk, imgOutPrefix='wco', tileFile=tileFile, colormap=True, apply_display_enhancement=not pre_spd_enhance)
2184
2320
  else:
2185
2321
  pass
2186
2322
 
@@ -2200,7 +2336,8 @@ class sonObj(object):
2200
2336
  son=True,
2201
2337
  integer=True,
2202
2338
  do_egn=False,
2203
- stretch_wcp=False):
2339
+ stretch_wcp=False,
2340
+ pre_spd_enhance=False):
2204
2341
 
2205
2342
  if not hasattr(self, 'sonMetaDF'):
2206
2343
  self._loadSonMeta()
@@ -2248,6 +2385,10 @@ class sonObj(object):
2248
2385
 
2249
2386
  sonDat = self.sonDat
2250
2387
 
2388
+ if pre_spd_enhance and not export_16bit:
2389
+ sonDat = self._apply_display_enhancements(sonDat)
2390
+ self.sonDat = sonDat
2391
+
2251
2392
  if spdCor == 0:
2252
2393
  # Don't do speed correction
2253
2394
  pass
@@ -2310,7 +2451,12 @@ class sonObj(object):
2310
2451
  clip=False, preserve_range=True)#.astype('uint8')
2311
2452
 
2312
2453
  if integer:
2313
- self.sonDat = sonDat.astype('uint8')
2454
+ if pre_spd_enhance and np.issubdtype(np.asarray(sonDat).dtype, np.floating):
2455
+ # CLAHE outputs 0..1 floats; convert with robust scaling so
2456
+ # display-enhanced data does not collapse to near-black.
2457
+ self.sonDat = self._convert_son_dat_to_uint8(sonDat)
2458
+ else:
2459
+ self.sonDat = sonDat.astype('uint8')
2314
2460
  else:
2315
2461
  self.sonDat = sonDat
2316
2462
 
@@ -2413,6 +2559,12 @@ class sonObj(object):
2413
2559
  self._use_jsf_weighting = True
2414
2560
  self._ensure_jsf_global_scale_max(sonMetaAll)
2415
2561
 
2562
+ self._use_cerulean_power_scale = False
2563
+ if 'min_pwr_db' in sonMeta.columns and 'max_pwr_db' in sonMeta.columns:
2564
+ self.minPwrDb = pd.to_numeric(sonMeta['min_pwr_db'], errors='coerce').to_numpy(dtype=float)
2565
+ self.maxPwrDb = pd.to_numeric(sonMeta['max_pwr_db'], errors='coerce').to_numpy(dtype=float)
2566
+ self._use_cerulean_power_scale = True
2567
+
2416
2568
  # Load chunk's sonar data into memory
2417
2569
  self._loadSonChunk()
2418
2570
  # Do PPDRC filter
@@ -2427,7 +2579,12 @@ class sonObj(object):
2427
2579
  del self.bytesPerSample
2428
2580
  if hasattr(self, 'weightingFactor'):
2429
2581
  del self.weightingFactor
2582
+ if hasattr(self, 'minPwrDb'):
2583
+ del self.minPwrDb
2584
+ if hasattr(self, 'maxPwrDb'):
2585
+ del self.maxPwrDb
2430
2586
  self._use_jsf_weighting = False
2587
+ self._use_cerulean_power_scale = False
2431
2588
 
2432
2589
  return
2433
2590
 
@@ -2477,6 +2634,12 @@ class sonObj(object):
2477
2634
  self._use_jsf_weighting = True
2478
2635
  self._ensure_jsf_global_scale_max(sonMetaAll_full)
2479
2636
 
2637
+ self._use_cerulean_power_scale = False
2638
+ if 'min_pwr_db' in sonMeta.columns and 'max_pwr_db' in sonMeta.columns:
2639
+ self.minPwrDb = pd.to_numeric(sonMeta['min_pwr_db'], errors='coerce').to_numpy(dtype=float)
2640
+ self.maxPwrDb = pd.to_numeric(sonMeta['max_pwr_db'], errors='coerce').to_numpy(dtype=float)
2641
+ self._use_cerulean_power_scale = True
2642
+
2480
2643
  # Load chunk's sonar data into memory
2481
2644
  self._loadSonChunk()
2482
2645
 
@@ -2489,7 +2652,12 @@ class sonObj(object):
2489
2652
  del self.bytesPerSample
2490
2653
  if hasattr(self, 'weightingFactor'):
2491
2654
  del self.weightingFactor
2655
+ if hasattr(self, 'minPwrDb'):
2656
+ del self.minPwrDb
2657
+ if hasattr(self, 'maxPwrDb'):
2658
+ del self.maxPwrDb
2492
2659
  self._use_jsf_weighting = False
2660
+ self._use_cerulean_power_scale = False
2493
2661
 
2494
2662
  return
2495
2663
 
@@ -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
@@ -1500,7 +1524,7 @@ class sonObj(object):
1500
1524
  # plt.savefig(outfile)
1501
1525
 
1502
1526
  norm_data = data / 255.0
1503
- colored_data = plt.cm.get_cmap(self.sonogram_colorMap)(norm_data)
1527
+ colored_data = plt.get_cmap(self.sonogram_colorMap)(norm_data)
1504
1528
  colored_data = (colored_data[:, :, :3] * 255).astype('uint8')
1505
1529
  data = colored_data
1506
1530
 
@@ -25,6 +25,9 @@
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_clip_limit":0.01,
28
31
  "wcp":false,
29
32
  "wcm":true,
30
33
  "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,36 @@ 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
+ text_clahe_clip = sg.Text('CLAHE Clip Limit', size=(20,1))
325
+ in_clahe_clip = sg.Input(
326
+ key='sonar_clahe_clip_limit',
327
+ default_text=default_params.get('sonar_clahe_clip_limit', 0.01),
328
+ size=(10,1),
329
+ tooltip=tip_clahe_clip,
330
+ )
309
331
 
310
- col_egn_1 = sg.Column([[text_egn_gamma, slide_egn_gamma], [text_egn_gain, slide_egn_gain]], pad=0)
332
+ col_egn_1 = sg.Column(
333
+ [
334
+ [text_egn_gamma, slide_egn_gamma],
335
+ [text_egn_gain, slide_egn_gain],
336
+ [check_sonar_db],
337
+ [check_sonar_clahe],
338
+ [text_clahe_clip, in_clahe_clip],
339
+ ],
340
+ pad=0,
341
+ )
311
342
  col_egn_2 = sg.Column([[check_egn], [text_egn_stretch, combo_egn_stretch], [text_egn_factor, in_egn_factor]], pad=0)
312
343
  # Add to layout
313
344
  layout.append([sg.HorizontalSeparator()])
@@ -804,6 +835,9 @@ def gui(batch: bool):
804
835
  'egn_stretch_factor':float(values['egn_stretch_factor']),
805
836
  'tone_gamma':float(values['tone_gamma']),
806
837
  'tone_gain':float(values['tone_gain']),
838
+ 'sonar_db_transform':values['sonar_db_transform'],
839
+ 'sonar_clahe':values['sonar_clahe'],
840
+ 'sonar_clahe_clip_limit':float(values['sonar_clahe_clip_limit']),
807
841
  'wcp':values['wcp'],
808
842
  'wcm':values['wcm'],
809
843
  'wcr':values['wcr'],
@@ -718,6 +718,9 @@ 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_clip_limit=0.01,
721
724
  wcp=False,
722
725
  wcm=False,
723
726
  wcr=False,
@@ -1038,6 +1041,9 @@ def read_master_func(logfilename='',
1038
1041
  son.son8bit = sonar_obj.son8bit
1039
1042
  if hasattr(sonar_obj, 'sample_dtype'):
1040
1043
  son.sample_dtype = sonar_obj.sample_dtype
1044
+ elif file_type == '.svlog':
1045
+ # Cerulean pwr_results are uint16 values in little-endian order.
1046
+ son.sample_dtype = '<u2'
1041
1047
  if hasattr(sonar_obj, 'export_raw_16bit'):
1042
1048
  son.export_raw_16bit = sonar_obj.export_raw_16bit
1043
1049
  if hasattr(son, 'sample_dtype'):
@@ -1201,6 +1207,9 @@ def read_master_func(logfilename='',
1201
1207
  son.cropRange = cropRange
1202
1208
  son.tone_gamma = tone_gamma
1203
1209
  son.tone_gain = tone_gain
1210
+ son.sonar_db_transform = bool(sonar_db_transform)
1211
+ son.sonar_clahe = bool(sonar_clahe)
1212
+ son.sonar_clahe_clip_limit = float(sonar_clahe_clip_limit)
1204
1213
  # Do range crop, if necessary
1205
1214
  if cropRange > 0.0:
1206
1215
  if file_type == '.xtf' and _is_sidescan_beam(getattr(son, 'beamName', '')):
@@ -1394,6 +1403,9 @@ def read_master_func(logfilename='',
1394
1403
  son.wcp = wcp
1395
1404
  son.tone_gamma = tone_gamma
1396
1405
  son.tone_gain = tone_gain
1406
+ son.sonar_db_transform = bool(sonar_db_transform)
1407
+ son.sonar_clahe = bool(sonar_clahe)
1408
+ son.sonar_clahe_clip_limit = float(sonar_clahe_clip_limit)
1397
1409
 
1398
1410
  beam = son.beamName
1399
1411
  if wcr:
@@ -0,0 +1 @@
1
+ __version__ = '5.5.2'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.5.0
3
+ Version: 5.5.2
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.0'
File without changes
File without changes
File without changes
File without changes
File without changes