pingmapper 5.4.0__tar.gz → 5.4.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 (58) hide show
  1. {pingmapper-5.4.0 → pingmapper-5.4.2}/PKG-INFO +3 -2
  2. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_portstarObj.py +376 -28
  3. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_rectObj.py +55 -0
  4. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_sonObj.py +68 -1
  5. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_sonObj_nadirgaptest.py +10 -1
  6. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/main_readFiles.py +7 -3
  7. pingmapper-5.4.2/pingmapper/version.py +1 -0
  8. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/PKG-INFO +3 -2
  9. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/SOURCES.txt +1 -0
  10. pingmapper-5.4.2/setup.py +50 -0
  11. pingmapper-5.4.0/pingmapper/version.py +0 -1
  12. {pingmapper-5.4.0 → pingmapper-5.4.2}/LICENSE +0 -0
  13. {pingmapper-5.4.0 → pingmapper-5.4.2}/README.md +0 -0
  14. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/__init__.py +0 -0
  15. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/__main__.py +0 -0
  16. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_mapSubstrateObj.py +0 -0
  17. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/default_params.json +0 -0
  18. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/doWork.py +0 -0
  19. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/funcs_common.py +0 -0
  20. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/funcs_model.py +0 -0
  21. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/funcs_rectify.py +0 -0
  22. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/gui_main.py +0 -0
  23. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/main_mapSubstrate.py +0 -0
  24. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/main_rectify.py +0 -0
  25. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/nonGUI_batch_main.py +0 -0
  26. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/nonGui_main.py +0 -0
  27. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
  28. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
  29. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
  30. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/scratch/main.py +0 -0
  31. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/scratch/main_batchDirectory.py +0 -0
  32. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/test_PINGMapper.py +0 -0
  33. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/test_dq_filter.py +0 -0
  34. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/test_time.py +0 -0
  35. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
  36. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
  37. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
  38. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
  39. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
  40. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
  41. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
  42. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
  43. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
  44. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
  45. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
  46. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
  47. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
  48. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
  49. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
  50. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
  51. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
  52. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/export_coverage.py +0 -0
  53. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/main_mosaic_transects.py +0 -0
  54. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/dependency_links.txt +0 -0
  55. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/requires.txt +0 -0
  56. {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/top_level.txt +0 -0
  57. {pingmapper-5.4.0 → pingmapper-5.4.2}/pyproject.toml +0 -0
  58. {pingmapper-5.4.0 → pingmapper-5.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.4.0
3
+ Version: 5.4.2
4
4
  Summary: Open-source interface for processing recreation-grade side scan sonar datasets and reproducibly mapping benthic habitat
5
5
  Author: Daniel Buscombe
6
6
  Author-email: Cameron Bodine <bodine.cs@gmail.email>
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering :: Visualization
17
17
  Classifier: Topic :: Scientific/Engineering :: Oceanography
18
18
  Classifier: Topic :: Scientific/Engineering :: GIS
19
19
  Classifier: Topic :: Scientific/Engineering :: Hydrology
20
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.6
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: pinginstaller<3,>=2
@@ -33,6 +33,7 @@ Requires-Dist: tensorflow<3,>=2.20; extra == "ml"
33
33
  Requires-Dist: tf-keras<3,>=2.20; extra == "ml"
34
34
  Requires-Dist: transformers<5,>=4.57; extra == "ml"
35
35
  Dynamic: license-file
36
+ Dynamic: requires-python
36
37
 
37
38
  # PING-Mapper
38
39
  [![PyPI - Version](https://img.shields.io/pypi/v/pingmapper?style=flat-square&label=Latest%20Version%20(PyPi))](https://pypi.org/project/pingmapper/)
@@ -31,6 +31,7 @@
31
31
 
32
32
 
33
33
  import os, sys
34
+ import time
34
35
 
35
36
  # Add 'pingmapper' to the path, may not need after pypi package...
36
37
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -1199,13 +1200,45 @@ class portstarObj(object):
1199
1200
  # Find ping-wise water column width from min and max depth prediction
1200
1201
  Wp = maxDepths+minDepths
1201
1202
 
1202
- # Try cropping so water column ~1/3 of target size area
1203
- WCProp = 1/3
1203
+ # Try cropping so water column ~1/4 of target size area.
1204
+ # Keeping less water column generally improves bed segmentation stability.
1205
+ WCProp = 1/4
1204
1206
 
1205
1207
  # Buffers so we don't crop too much
1206
1208
  WwcBuf = 150
1207
1209
  WsBuf = 150
1208
1210
 
1211
+ # Use instrument depth to constrain max bed search depth when available.
1212
+ # This helps avoid far-range false positives when returns are only valid
1213
+ # near the center (e.g., shallow channels with large configured range).
1214
+ # inst_depth_mult=3.0 means max search depth = 3x instrument depth, which
1215
+ # keeps the water column at roughly 25% of each side's range.
1216
+ inst_depth_mult = 3.0
1217
+
1218
+ if not hasattr(self.port, 'sonMetaDF'):
1219
+ self.port._loadSonMeta()
1220
+ if not hasattr(self.star, 'sonMetaDF'):
1221
+ self.star._loadSonMeta()
1222
+
1223
+ portChunk = self.port.sonMetaDF[self.port.sonMetaDF['chunk_id'] == i]
1224
+ starChunk = self.star.sonMetaDF[self.star.sonMetaDF['chunk_id'] == i]
1225
+
1226
+ portInstM = pd.to_numeric(portChunk['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1227
+ starInstM = pd.to_numeric(starChunk['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1228
+ portPixM = pd.to_numeric(portChunk['pixM'], errors='coerce').to_numpy(dtype=float, copy=True)
1229
+ starPixM = pd.to_numeric(starChunk['pixM'], errors='coerce').to_numpy(dtype=float, copy=True)
1230
+
1231
+ portInstPix = np.where((portInstM > 0) & (portPixM > 0), portInstM / portPixM, np.nan)
1232
+ starInstPix = np.where((starInstM > 0) & (starPixM > 0), starInstM / starPixM, np.nan)
1233
+
1234
+ instPix = np.concatenate((portInstPix, starInstPix))
1235
+ validInstPix = instPix[np.isfinite(instPix) & (instPix > 0)]
1236
+ if validInstPix.size > 0:
1237
+ instMedPix = np.nanmedian(validInstPix)
1238
+ maxDepByInst = instMedPix * inst_depth_mult
1239
+ if np.isfinite(maxDepByInst) and maxDepByInst > 0:
1240
+ maxDep = min(maxDep, maxDepByInst)
1241
+
1209
1242
  # Sum Wp to determine area of water column
1210
1243
  WpArea = np.nansum(Wp)
1211
1244
 
@@ -1235,16 +1268,31 @@ class portstarObj(object):
1235
1268
  if Ws > (C-(Wwc/2)):
1236
1269
  Ws = int( C - (Wwc/2) - (W/2) - WsBuf)
1237
1270
 
1271
+ # If Ws is negative, inst_depth * mult exceeds the recorded range.
1272
+ # Pad the far-range side with zeros so the crop proportions are correct.
1273
+ # Zero columns = no acoustic return, which the model interprets as bed,
1274
+ # anchoring the pick at the data boundary when depth approaches range.
1275
+ pad_far = 0
1276
+ if Ws < 0:
1277
+ pad_far = int(-Ws)
1278
+ Ws = 0
1279
+
1238
1280
  # Crop the original sonogram
1239
1281
  ## Port Crop
1240
1282
  lC = Ws # left side crop
1241
1283
  rC = int(C - (Wwc/2)) # right side crop
1242
1284
  portCrop = son3bnd[:, lC:rC,:]
1285
+ if pad_far > 0:
1286
+ _pad = np.zeros((portCrop.shape[0], pad_far, portCrop.shape[2]), dtype=np.uint8)
1287
+ portCrop = np.concatenate((_pad, portCrop), axis=1) # extend far range (left)
1243
1288
 
1244
1289
  ## Star Crop
1245
1290
  lC = int(C + (Wwc/2)) # left side crop
1246
1291
  rC = int(N - Ws) # right side crop
1247
1292
  starCrop = son3bnd[:, lC:rC, :]
1293
+ if pad_far > 0:
1294
+ _pad = np.zeros((starCrop.shape[0], pad_far, starCrop.shape[2]), dtype=np.uint8)
1295
+ starCrop = np.concatenate((starCrop, _pad), axis=1) # extend far range (right)
1248
1296
 
1249
1297
 
1250
1298
  ## Concatenate port & star crop
@@ -1277,8 +1325,9 @@ class portstarObj(object):
1277
1325
  # Calculate depth from prediction
1278
1326
  portDepPixCrop, starDepPixCrop = self._findBed(crop_label) # get pixel location of bed
1279
1327
 
1280
- # add Wwc/2 to get final estimate at original sonogram dimensions
1281
- portDepPixFinal = np.flip( np.asarray(portDepPixCrop) + int(Wwc/2) )
1328
+ # add Wwc/2 to get final estimate at original sonogram dimensions.
1329
+ # Subtract pad_far from port since padding shifted its columns left.
1330
+ portDepPixFinal = np.flip( np.asarray(portDepPixCrop) + int(Wwc/2) - pad_far )
1282
1331
  starDepPixFinal = np.flip( np.asarray(starDepPixCrop) + int(Wwc/2) )
1283
1332
 
1284
1333
  #############
@@ -1400,6 +1449,34 @@ class portstarObj(object):
1400
1449
  isChunk = son.sonMetaDF['chunk_id'] == chunk
1401
1450
  sonMeta = son.sonMetaDF[isChunk].reset_index()
1402
1451
  acoustic_depth = pd.to_numeric(sonMeta['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1452
+ acoustic_med = pd.Series(acoustic_depth).rolling(window=31, center=True, min_periods=1).median().to_numpy()
1453
+ acoustic_resid = np.abs(acoustic_depth - acoustic_med)
1454
+ acoustic_valid = np.isfinite(acoustic_depth) & (acoustic_depth > 0)
1455
+ acoustic_bad = np.zeros(acoustic_depth.shape, dtype=bool)
1456
+ if acoustic_valid.any():
1457
+ resid_valid = acoustic_resid[acoustic_valid]
1458
+ resid_center = np.nanmedian(resid_valid)
1459
+ resid_mad = np.nanmedian(np.abs(resid_valid - resid_center))
1460
+ resid_thr = max(0.5, 6.0 * resid_mad if np.isfinite(resid_mad) else 0.5)
1461
+ acoustic_bad |= acoustic_valid & (acoustic_resid > resid_thr)
1462
+
1463
+ if acoustic_depth.size >= 3:
1464
+ prev_step = acoustic_depth[1:-1] - acoustic_depth[:-2]
1465
+ next_step = acoustic_depth[2:] - acoustic_depth[1:-1]
1466
+ step_mag = np.abs(np.concatenate((prev_step, next_step)))
1467
+ step_mag = step_mag[np.isfinite(step_mag)]
1468
+ if step_mag.size > 0:
1469
+ step_center = np.nanmedian(step_mag)
1470
+ step_mad = np.nanmedian(np.abs(step_mag - step_center))
1471
+ jump_thr = max(0.5, 6.0 * step_mad if np.isfinite(step_mad) else 0.5)
1472
+ acoustic_bad[1:-1] |= (
1473
+ acoustic_valid[1:-1] & acoustic_valid[:-2] & acoustic_valid[2:] &
1474
+ (np.abs(prev_step) > jump_thr) &
1475
+ (np.abs(next_step) > jump_thr) &
1476
+ ((prev_step * next_step) < 0)
1477
+ )
1478
+
1479
+ acoustic_depth = np.where(acoustic_bad, np.nan, acoustic_depth)
1403
1480
  acousticBed = np.round(acoustic_depth / sonMeta['pixM'].to_numpy(dtype=float, copy=True), 0)
1404
1481
  acousticBed = acousticBed[np.isfinite(acousticBed) & (acousticBed > 0)]
1405
1482
 
@@ -1407,8 +1484,8 @@ class portstarObj(object):
1407
1484
  # Step 1 : Acoustic Bedpick Filter
1408
1485
  # Use acoustic bed pick to crop image
1409
1486
  if acousticBed.size > 0:
1410
- bedMin = max(int(np.nanmin(acousticBed)) - 50, 0)
1411
- bedMax = int(np.nanmax(acousticBed)) + pix_buf
1487
+ bedMin = max(int(np.nanpercentile(acousticBed, 5)) - 50, 0)
1488
+ bedMax = int(np.nanpercentile(acousticBed, 95)) + pix_buf
1412
1489
  else:
1413
1490
  bedMin = 0
1414
1491
  bedMax = H
@@ -1570,6 +1647,87 @@ class portstarObj(object):
1570
1647
  return '0 pixels'
1571
1648
  return str(float(adjDep) / float(valid_pix.iloc[0])) + ' pixels'
1572
1649
 
1650
+ def _rolling_median(vals, window=31):
1651
+ return pd.Series(vals).rolling(window=window, center=True, min_periods=1).median().to_numpy()
1652
+
1653
+ def _flag_depth_outliers(depth, inst_depth=None, inst_depth_mult=None,
1654
+ resid_floor_m=0.5, jump_floor_m=0.5,
1655
+ iterative_jump=False, iterative_max_iter=64,
1656
+ iterative_jump_floor_m=None):
1657
+ depth = np.asarray(depth, dtype=float)
1658
+ flags = np.zeros(depth.shape, dtype=bool)
1659
+
1660
+ valid = np.isfinite(depth) & (depth > 0)
1661
+ if not valid.any():
1662
+ return flags
1663
+
1664
+ med = _rolling_median(depth)
1665
+ resid = np.abs(depth - med)
1666
+ resid_valid = resid[valid]
1667
+ resid_center = np.nanmedian(resid_valid)
1668
+ resid_mad = np.nanmedian(np.abs(resid_valid - resid_center))
1669
+ resid_thr = max(resid_floor_m, 6.0 * resid_mad if np.isfinite(resid_mad) else resid_floor_m)
1670
+ flags |= valid & (resid > resid_thr)
1671
+
1672
+ if depth.size >= 3:
1673
+ prev_step = depth[1:-1] - depth[:-2]
1674
+ next_step = depth[2:] - depth[1:-1]
1675
+ step_mag = np.abs(np.concatenate((prev_step, next_step)))
1676
+ step_mag = step_mag[np.isfinite(step_mag)]
1677
+ if step_mag.size > 0:
1678
+ step_center = np.nanmedian(step_mag)
1679
+ step_mad = np.nanmedian(np.abs(step_mag - step_center))
1680
+ jump_thr = max(jump_floor_m, 6.0 * step_mad if np.isfinite(step_mad) else jump_floor_m)
1681
+ spike_mid = (
1682
+ valid[1:-1] & valid[:-2] & valid[2:] &
1683
+ (np.abs(prev_step) > jump_thr) &
1684
+ (np.abs(next_step) > jump_thr) &
1685
+ ((prev_step * next_step) < 0)
1686
+ )
1687
+ flags[1:-1] |= spike_mid
1688
+
1689
+ # Acoustic depth failures often persist as a step change rather than
1690
+ # a single-ping spike (e.g., 2.4 m -> 6.3 m for many consecutive
1691
+ # records). Iteratively flagging the later side of large jumps peels
1692
+ # back those runs until continuity is restored.
1693
+ if iterative_jump:
1694
+ work = depth.copy()
1695
+ if iterative_jump_floor_m is None:
1696
+ iterative_jump_floor_m = jump_floor_m
1697
+ # Cap iterations so very large tracks do not degrade to near O(n^2)
1698
+ # behavior when many jump-like artifacts are present.
1699
+ max_iter = max(1, int(iterative_max_iter))
1700
+ for _ in range(max_iter):
1701
+ valid_work = np.isfinite(work) & (work > 0)
1702
+ valid_idx = np.flatnonzero(valid_work)
1703
+ if valid_idx.size < 2:
1704
+ break
1705
+
1706
+ step_vals = np.abs(np.diff(work[valid_idx]))
1707
+ step_vals = step_vals[np.isfinite(step_vals)]
1708
+ if step_vals.size == 0:
1709
+ break
1710
+
1711
+ step_center = np.nanmedian(step_vals)
1712
+ step_mad = np.nanmedian(np.abs(step_vals - step_center))
1713
+ jump_thr = max(iterative_jump_floor_m, 6.0 * step_mad if np.isfinite(step_mad) else iterative_jump_floor_m)
1714
+
1715
+ diffs = np.abs(np.diff(work[valid_idx]))
1716
+ bad_step_pos = np.flatnonzero(np.isfinite(diffs) & (diffs > jump_thr))
1717
+ if bad_step_pos.size == 0:
1718
+ break
1719
+
1720
+ new_bad_idx = valid_idx[bad_step_pos + 1]
1721
+ flags[new_bad_idx] = True
1722
+ work[new_bad_idx] = np.nan
1723
+
1724
+ if inst_depth is not None and inst_depth_mult is not None:
1725
+ inst_depth = np.asarray(inst_depth, dtype=float)
1726
+ inst_valid = np.isfinite(inst_depth) & (inst_depth > 0)
1727
+ flags |= valid & inst_valid & (depth > (inst_depth * inst_depth_mult))
1728
+
1729
+ return flags
1730
+
1573
1731
  def _sync_trackline_depth(beam_obj, beam_df):
1574
1732
  trk_file = os.path.join(beam_obj.metaDir, 'Trackline_Smth_' + beam_obj.beamName + '.csv')
1575
1733
  if not os.path.exists(trk_file):
@@ -1586,6 +1744,8 @@ class portstarObj(object):
1586
1744
  depth_cols.append('dep_m_smth')
1587
1745
  if 'dep_m_adjBy' in beam_df.columns:
1588
1746
  depth_cols.append('dep_m_adjBy')
1747
+ if 'dep_m_interp' in beam_df.columns:
1748
+ depth_cols.append('dep_m_interp')
1589
1749
 
1590
1750
  depth_df = beam_df[depth_cols].drop_duplicates(subset=['record_num'], keep='last').set_index('record_num')
1591
1751
  trk_df = trk_df.set_index('record_num')
@@ -1597,18 +1757,36 @@ class portstarObj(object):
1597
1757
  trk_df['dep_m_smth'] = depth_df['dep_m_smth']
1598
1758
  if 'dep_m_adjBy' in depth_df.columns:
1599
1759
  trk_df['dep_m_adjBy'] = depth_df['dep_m_adjBy']
1760
+ if 'dep_m_interp' in depth_df.columns:
1761
+ trk_df['dep_m_interp'] = depth_df['dep_m_interp']
1600
1762
 
1601
1763
  trk_df.reset_index().to_csv(trk_file, index=False, float_format='%.14f')
1602
1764
 
1765
+ depth_timer_start = time.perf_counter()
1766
+ depth_timer_last = depth_timer_start
1767
+ beam_pair = '{} / {}'.format(self.port.beamName, self.star.beamName)
1768
+
1769
+ def _depth_timing(label):
1770
+ nonlocal depth_timer_last
1771
+ now = time.perf_counter()
1772
+ print('\tDepth timing [{}] {}: {:.2f}s'.format(beam_pair, label, now - depth_timer_last))
1773
+ depth_timer_last = now
1774
+
1603
1775
  # Load sonar metadata file
1604
1776
  self.port._loadSonMeta()
1605
1777
  portDF = self.port.sonMetaDF
1606
1778
  self.star._loadSonMeta()
1607
1779
  starDF = self.star.sonMetaDF
1780
+ _depth_timing('load metadata')
1608
1781
 
1609
1782
  # Get all chunks
1610
1783
  chunks = pd.unique(portDF['chunk_id'])
1611
1784
 
1785
+ # Track points invalidated during pre-smoothing QC so output metadata
1786
+ # reflects all flagged depth values, not only post-smoothing flags.
1787
+ portPreFlags = np.zeros(len(portDF), dtype=bool)
1788
+ starPreFlags = np.zeros(len(starDF), dtype=bool)
1789
+
1612
1790
  if detectDep == 0:
1613
1791
  def _depth_series(df):
1614
1792
  # Some formats (e.g., Garmin RSD) may not include dep_m in metadata.
@@ -1630,6 +1808,36 @@ class portstarObj(object):
1630
1808
  portInstDepth = np.where(portValid, portInstDepth, portMetaDepth)
1631
1809
  starInstDepth = np.where(starValid, starInstDepth, starMetaDepth)
1632
1810
 
1811
+ # Flag outliers on raw instrument depth BEFORE smoothing so sharp
1812
+ # jumps (e.g. sonar lock-on to a false deep target) are caught on
1813
+ # the un-blurred signal rather than on the smoothed ramp they become
1814
+ # after savgol. NaNs are linearly filled so savgol has no gaps.
1815
+ def _fill_nans_linear(arr):
1816
+ nans = np.isnan(arr) | (arr <= 0)
1817
+ x = np.arange(len(arr))
1818
+ if nans.any() and (~nans).any():
1819
+ arr[nans] = np.interp(x[nans], x[~nans], arr[~nans])
1820
+ return arr
1821
+
1822
+ # Keep one-ping spike sensitivity, but require a much larger jump for
1823
+ # iterative run peeling so true depth regime changes are not over-flagged.
1824
+ portPreFlags = _flag_depth_outliers(
1825
+ portInstDepth,
1826
+ iterative_jump=True,
1827
+ iterative_max_iter=512,
1828
+ iterative_jump_floor_m=4.0,
1829
+ )
1830
+ starPreFlags = _flag_depth_outliers(
1831
+ starInstDepth,
1832
+ iterative_jump=True,
1833
+ iterative_max_iter=512,
1834
+ iterative_jump_floor_m=4.0,
1835
+ )
1836
+ portInstDepth[portPreFlags] = np.nan
1837
+ starInstDepth[starPreFlags] = np.nan
1838
+ portInstDepth = _fill_nans_linear(portInstDepth)
1839
+ starInstDepth = _fill_nans_linear(starInstDepth)
1840
+
1633
1841
  if smthDep:
1634
1842
  # print("\nSmoothing depth values...")
1635
1843
  portInstDepth = savgol_filter(portInstDepth, 51, 3)
@@ -1662,6 +1870,7 @@ class portstarObj(object):
1662
1870
 
1663
1871
  portDF['dep_m_adjBy'] = _format_depth_adjustment(portDF['pixM'])
1664
1872
  starDF['dep_m_adjBy'] = _format_depth_adjustment(starDF['pixM'])
1873
+ _depth_timing('prepare instrument depth')
1665
1874
 
1666
1875
  elif detectDep > 0:
1667
1876
  # Prepare depth detection dictionaries
@@ -1748,11 +1957,66 @@ class portstarObj(object):
1748
1957
 
1749
1958
  portDF['dep_m_adjBy'] = _format_depth_adjustment(portDF['pixM'])
1750
1959
  starDF['dep_m_adjBy'] = _format_depth_adjustment(starDF['pixM'])
1960
+ _depth_timing('prepare detected depth')
1961
+
1962
+ # Outlier and jump filtering before interpolation.
1963
+ # detectDep=0: continuity-only acoustic QC.
1964
+ # detectDep=1/2: continuity QC plus instrument-depth proportional cap.
1965
+ portArr = pd.to_numeric(portDF['dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1966
+ starArr = pd.to_numeric(starDF['dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1967
+ portRawArr = portArr.copy()
1968
+ starRawArr = starArr.copy()
1969
+ portInst = pd.to_numeric(portDF['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1970
+ starInst = pd.to_numeric(starDF['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1971
+
1972
+ portFlags = np.zeros(portArr.shape, dtype=bool)
1973
+ starFlags = np.zeros(starArr.shape, dtype=bool)
1974
+
1975
+ if detectDep == 0:
1976
+ portFlags |= _flag_depth_outliers(portArr, iterative_jump=True, iterative_jump_floor_m=4.0)
1977
+ starFlags |= _flag_depth_outliers(starArr, iterative_jump=True, iterative_jump_floor_m=4.0)
1978
+ elif detectDep in (1, 2):
1979
+ portFlags |= _flag_depth_outliers(portArr, portInst, inst_depth_mult=3.0)
1980
+ starFlags |= _flag_depth_outliers(starArr, starInst, inst_depth_mult=3.0)
1981
+
1982
+ # If sides diverge strongly, invalidate the side farther from
1983
+ # instrument depth so interpolation can recover continuity.
1984
+ pair_valid = np.isfinite(portArr) & np.isfinite(starArr) & (portArr > 0) & (starArr > 0)
1985
+ diverge = pair_valid & (np.abs(portArr - starArr) > 5.0)
1986
+ if diverge.any():
1987
+ portErr = np.abs(portArr - portInst)
1988
+ starErr = np.abs(starArr - starInst)
1989
+
1990
+ inst_pair_valid = np.isfinite(portInst) & (portInst > 0) & np.isfinite(starInst) & (starInst > 0)
1991
+ choose_by_inst = diverge & inst_pair_valid
1992
+ portFlags |= choose_by_inst & (portErr >= starErr)
1993
+ starFlags |= choose_by_inst & (starErr > portErr)
1994
+
1995
+ # Fallback for rows without valid instrument depth on one/both sides.
1996
+ fallback = diverge & (~inst_pair_valid)
1997
+ if fallback.any():
1998
+ pmed = _rolling_median(portArr)
1999
+ smed = _rolling_median(starArr)
2000
+ presid = np.abs(portArr - pmed)
2001
+ sresid = np.abs(starArr - smed)
2002
+ portFlags |= fallback & (presid >= sresid)
2003
+ starFlags |= fallback & (sresid > presid)
2004
+
2005
+ portArr[portFlags] = np.nan
2006
+ starArr[starFlags] = np.nan
2007
+ portDF['dep_m'] = portArr
2008
+ portDF['dep_m_raw'] = portRawArr
2009
+ starDF['dep_m'] = starArr
2010
+ starDF['dep_m_raw'] = starRawArr
2011
+ _depth_timing('outlier and jump filtering')
1751
2012
 
1752
2013
  # Interpolate over nan's (and set zeros to nan)
1753
2014
  portDep = portDF['dep_m'].to_numpy(copy=True)
1754
2015
  starDep = starDF['dep_m'].to_numpy(copy=True)
1755
2016
 
2017
+ portInterp = np.isnan(portDep) | (portDep == 0) | portPreFlags
2018
+ starInterp = np.isnan(starDep) | (starDep == 0) | starPreFlags
2019
+
1756
2020
  portDep[portDep == 0] = np.nan
1757
2021
  starDep[starDep == 0] = np.nan
1758
2022
 
@@ -1762,6 +2026,7 @@ class portstarObj(object):
1762
2026
  else:
1763
2027
  portDep[nans] = 0
1764
2028
  portDF['dep_m'] = portDep
2029
+ portDF['dep_m_interp'] = portInterp.astype(np.uint8)
1765
2030
 
1766
2031
  nans, x = np.isnan(starDep), lambda z: z.nonzero()[0]
1767
2032
  if (~nans).any():
@@ -1769,27 +2034,38 @@ class portstarObj(object):
1769
2034
  else:
1770
2035
  starDep[nans] = 0
1771
2036
  starDF['dep_m'] = starDep
2037
+ starDF['dep_m_interp'] = starInterp.astype(np.uint8)
2038
+ _depth_timing('interpolate depth')
1772
2039
 
1773
2040
  # Export to csv
1774
2041
  portDF.to_csv(self.port.sonMetaFile, index=False, float_format='%.14f')
1775
2042
  starDF.to_csv(self.star.sonMetaFile, index=False, float_format='%.14f')
1776
2043
  _sync_trackline_depth(self.port, portDF)
1777
2044
  _sync_trackline_depth(self.star, starDF)
2045
+ _depth_timing('write metadata and trackline')
1778
2046
 
1779
2047
  try:
1780
2048
  # Take average of both estimates to store with downlooking sonar csv
1781
- depDF = pd.DataFrame(columns=['dep_m', 'dep_m_Method', 'dep_m_smth', 'dep_m_adjBy'])
2049
+ depDF = pd.DataFrame(columns=['dep_m', 'dep_m_Method', 'dep_m_smth', 'dep_m_adjBy', 'dep_m_interp'])
1782
2050
  depDF['dep_m'] = np.nanmean([portDF['dep_m'].to_numpy(), starDF['dep_m'].to_numpy()], axis=0)
1783
2051
  depDF['dep_m_Method'] = portDF['dep_m_Method']
1784
2052
  depDF['dep_m_smth'] = portDF['dep_m_smth']
1785
2053
  depDF['dep_m_adjBy'] = portDF['dep_m_adjBy']
2054
+ depDF['dep_m_interp'] = np.maximum(
2055
+ pd.to_numeric(portDF['dep_m_interp'], errors='coerce').fillna(0).to_numpy(dtype=np.uint8, copy=True),
2056
+ pd.to_numeric(starDF['dep_m_interp'], errors='coerce').fillna(0).to_numpy(dtype=np.uint8, copy=True)
2057
+ )
1786
2058
  except:
1787
2059
  # In case port and star are not same length
1788
- depDF = pd.DataFrame(columns=['dep_m', 'dep_m_Method', 'dep_m_smth', 'dep_m_adjBy'])
2060
+ depDF = pd.DataFrame(columns=['dep_m', 'dep_m_Method', 'dep_m_smth', 'dep_m_adjBy', 'dep_m_interp'])
1789
2061
  depDF['dep_m'] = portDF['dep_m']
1790
2062
  depDF['dep_m_Method'] = portDF['dep_m_Method']
1791
2063
  depDF['dep_m_smth'] = portDF['dep_m_smth']
1792
2064
  depDF['dep_m_adjBy'] = portDF['dep_m_adjBy']
2065
+ depDF['dep_m_interp'] = portDF['dep_m_interp']
2066
+
2067
+ _depth_timing('build return depth dataframe')
2068
+ print('\tDepth timing [{}] total: {:.2f}s'.format(beam_pair, time.perf_counter() - depth_timer_start))
1793
2069
 
1794
2070
  del portDF, starDF
1795
2071
  gc.collect()
@@ -1847,20 +2123,65 @@ class portstarObj(object):
1847
2123
  self.star._loadSonMeta()
1848
2124
  starDF = self.star.sonMetaDF
1849
2125
 
1850
- portDF = portDF.loc[portDF['chunk_id'] == i, ['inst_dep_m', 'dep_m', 'pixM']]
1851
- starDF = starDF.loc[starDF['chunk_id'] == i, ['inst_dep_m', 'dep_m', 'pixM']]
2126
+ def _depth_to_pixels(df, depth_col):
2127
+ depth = pd.to_numeric(df[depth_col], errors='coerce').to_numpy(dtype=float, copy=True)
2128
+ pix = pd.to_numeric(df['pixM'], errors='coerce').to_numpy(dtype=float, copy=True)
2129
+ return np.divide(
2130
+ depth,
2131
+ pix,
2132
+ out=np.full(depth.shape, np.nan, dtype=float),
2133
+ where=np.isfinite(pix) & (pix != 0)
2134
+ )
2135
+
2136
+ def _pad_to_match(values, reference):
2137
+ diff = reference.shape[0] - values.shape[0]
2138
+ if diff > 0:
2139
+ values = np.append(values, reference[-diff:])
2140
+ return values
2141
+
2142
+ portCols = ['inst_dep_m', 'dep_m', 'pixM']
2143
+ starCols = ['inst_dep_m', 'dep_m', 'pixM']
2144
+ if 'dep_m_interp' in portDF.columns:
2145
+ portCols.append('dep_m_interp')
2146
+ if 'dep_m_raw' in portDF.columns:
2147
+ portCols.append('dep_m_raw')
2148
+ if 'dep_m_interp' in starDF.columns:
2149
+ starCols.append('dep_m_interp')
2150
+ if 'dep_m_raw' in starDF.columns:
2151
+ starCols.append('dep_m_raw')
2152
+
2153
+ portDF = portDF.loc[portDF['chunk_id'] == i, portCols]
2154
+ starDF = starDF.loc[starDF['chunk_id'] == i, starCols]
2155
+
2156
+ detectDepMode = int(getattr(self.port, 'detectDep', getattr(self.star, 'detectDep', -1)))
1852
2157
 
1853
2158
  portInstDepth = pd.to_numeric(portDF['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1854
2159
  portMetaDepth = pd.to_numeric(portDF['dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1855
- portInstDepth = np.where(np.isfinite(portInstDepth) & (portInstDepth > 0), portInstDepth, portMetaDepth)
1856
- portInst = np.nan_to_num(portInstDepth / portDF['pixM'].to_numpy(dtype=float), nan=0.0).astype(int)
1857
- portAuto = (portDF['dep_m'] / portDF['pixM']).to_numpy(dtype=int, copy=True)
2160
+ if not np.isfinite(portInstDepth).any():
2161
+ portInstDepth = portMetaDepth.copy()
2162
+ portDF = portDF.copy()
2163
+ portDF['inst_depth_plot'] = portInstDepth
2164
+ portInst = np.nan_to_num(_depth_to_pixels(portDF, 'inst_depth_plot'), nan=0.0)
2165
+ portAuto = _depth_to_pixels(portDF, 'dep_m')
2166
+ portRaw = _depth_to_pixels(portDF, 'dep_m_raw') if 'dep_m_raw' in portDF.columns else portAuto.copy()
2167
+ if 'dep_m_interp' in portDF.columns:
2168
+ portInterp = pd.to_numeric(portDF['dep_m_interp'], errors='coerce').fillna(0).to_numpy(dtype=np.uint8, copy=True).astype(bool)
2169
+ else:
2170
+ portInterp = np.zeros(portAuto.shape, dtype=bool)
1858
2171
 
1859
2172
  starInstDepth = pd.to_numeric(starDF['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1860
2173
  starMetaDepth = pd.to_numeric(starDF['dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1861
- starInstDepth = np.where(np.isfinite(starInstDepth) & (starInstDepth > 0), starInstDepth, starMetaDepth)
1862
- starInst = np.nan_to_num(starInstDepth / starDF['pixM'].to_numpy(dtype=float), nan=0.0).astype(int)
1863
- starAuto = (starDF['dep_m'] / starDF['pixM']).to_numpy(dtype=int, copy=True)
2174
+ if not np.isfinite(starInstDepth).any():
2175
+ starInstDepth = starMetaDepth.copy()
2176
+ starDF = starDF.copy()
2177
+ starDF['inst_depth_plot'] = starInstDepth
2178
+ starInst = np.nan_to_num(_depth_to_pixels(starDF, 'inst_depth_plot'), nan=0.0)
2179
+ starAuto = _depth_to_pixels(starDF, 'dep_m')
2180
+ starRaw = _depth_to_pixels(starDF, 'dep_m_raw') if 'dep_m_raw' in starDF.columns else starAuto.copy()
2181
+ if 'dep_m_interp' in starDF.columns:
2182
+ starInterp = pd.to_numeric(starDF['dep_m_interp'], errors='coerce').fillna(0).to_numpy(dtype=np.uint8, copy=True).astype(bool)
2183
+ else:
2184
+ starInterp = np.zeros(starAuto.shape, dtype=bool)
1864
2185
 
1865
2186
  # Ensure port/star same length
1866
2187
  if (portAuto.shape[0] != starAuto.shape[0]):
@@ -1868,27 +2189,37 @@ class portstarObj(object):
1868
2189
  sL = starAuto.shape[0]
1869
2190
  # Add rows to shortest array from longest array
1870
2191
  if (pL > sL):
1871
- starAuto = np.append(starAuto, portAuto[(sL-pL):])
1872
- starInst = np.append(starInst, portInst[(sL-pL):])
2192
+ starAuto = _pad_to_match(starAuto, portAuto)
2193
+ starInst = _pad_to_match(starInst, portInst)
2194
+ starRaw = _pad_to_match(starRaw, portRaw)
2195
+ starInterp = _pad_to_match(starInterp, portInterp)
1873
2196
  else:
1874
- portAuto = np.append(portAuto, starAuto[(pL-sL):])
1875
- portInst = np.append(portInst, starInst[(pL-sL):])
2197
+ portAuto = _pad_to_match(portAuto, starAuto)
2198
+ portInst = _pad_to_match(portInst, starInst)
2199
+ portRaw = _pad_to_match(portRaw, starRaw)
2200
+ portInterp = _pad_to_match(portInterp, starInterp)
1876
2201
 
1877
2202
  # Relocate depths relative to horizontal center of image
1878
2203
  c = int(mergeSon.shape[1]/2)
1879
2204
 
1880
2205
  portInst = c - portInst
1881
2206
  portAuto = c - portAuto
2207
+ portRaw = c - portRaw
1882
2208
 
1883
2209
  starInst = c + starInst
1884
2210
  starAuto = c + starAuto
2211
+ starRaw = c + starRaw
1885
2212
 
1886
2213
  # maybe flip???
1887
2214
  portInst = np.flip(portInst)
1888
2215
  portAuto = np.flip(portAuto)
2216
+ portRaw = np.flip(portRaw)
2217
+ portInterp = np.flip(portInterp)
1889
2218
 
1890
2219
  starInst = np.flip(starInst)
1891
2220
  starAuto = np.flip(starAuto)
2221
+ starRaw = np.flip(starRaw)
2222
+ starInterp = np.flip(starInterp)
1892
2223
 
1893
2224
  #############
1894
2225
  # Export Plot
@@ -1915,14 +2246,31 @@ class portstarObj(object):
1915
2246
  outFile = os.path.join(outDir, projName+'_Bedpick_'+addZero+str(i)+tileFile)
1916
2247
 
1917
2248
  plt.imshow(mergeSon, cmap='gray')
1918
- if acousticBed:
1919
- plt.plot(portInst, y, 'r-.', lw=1, label='Acoustic Depth')
1920
- plt.plot(starInst, y, 'r-.', lw=1)
1921
- del portInst, starInst
1922
- if autoBed:
1923
- plt.plot(portAuto, y, 'b-.', lw=1, label='Auto Depth')
1924
- plt.plot(starAuto, y, 'b-.', lw=1)
1925
- del portAuto, starAuto
2249
+ if detectDepMode == 0:
2250
+ if acousticBed:
2251
+ portInstGood = np.where(~portInterp, portInst, np.nan)
2252
+ starInstGood = np.where(~starInterp, starInst, np.nan)
2253
+ portInstBad = np.where(portInterp, portInst, np.nan)
2254
+ starInstBad = np.where(starInterp, starInst, np.nan)
2255
+ portInterpDepth = np.where(portInterp, portAuto, np.nan)
2256
+ starInterpDepth = np.where(starInterp, starAuto, np.nan)
2257
+
2258
+ plt.plot(portInstGood, y, '-.', color='lime', lw=1, label='Instrument Depth (Good)')
2259
+ plt.plot(starInstGood, y, '-.', color='lime', lw=1)
2260
+ plt.plot(portInstBad, y, '-.', color='red', lw=1, label='Instrument Depth (Flagged)')
2261
+ plt.plot(starInstBad, y, '-.', color='red', lw=1)
2262
+ plt.plot(portInterpDepth, y, '-.', color='yellow', lw=1, label='Interpolated Depth')
2263
+ plt.plot(starInterpDepth, y, '-.', color='yellow', lw=1)
2264
+ del portInstGood, starInstGood, portInstBad, starInstBad, portInterpDepth, starInterpDepth
2265
+ else:
2266
+ if acousticBed:
2267
+ plt.plot(portInst, y, 'r-.', lw=1, label='Instrument Depth')
2268
+ plt.plot(starInst, y, 'r-.', lw=1)
2269
+ if autoBed:
2270
+ plt.plot(portAuto, y, 'b-.', lw=1, label='Auto Depth')
2271
+ plt.plot(starAuto, y, 'b-.', lw=1)
2272
+
2273
+ del portInst, starInst, portAuto, starAuto, portRaw, starRaw, portInterp, starInterp
1926
2274
 
1927
2275
  plt.legend(loc = 'lower right', prop={'size':4}) # create the plot legend
1928
2276
  plt.savefig(outFile, dpi=300, bbox_inches='tight')
@@ -2151,6 +2151,7 @@ class rectObj(sonObj):
2151
2151
 
2152
2152
  # Open smoothed trackline/range extent file
2153
2153
  trkMeta = pd.read_csv(trkMetaFile)
2154
+ trkMeta = self._sanitizeProjectedTrackMeta(trkMeta, wgs=wgs)
2154
2155
 
2155
2156
  # Create geodataframe
2156
2157
  gdf = gpd.GeoDataFrame(
@@ -2165,6 +2166,59 @@ class rectObj(sonObj):
2165
2166
  del trkMetaFile
2166
2167
 
2167
2168
  return
2169
+
2170
+ #===========================================================================
2171
+ def _sanitizeProjectedTrackMeta(self, trkMeta, wgs=False):
2172
+
2173
+ if wgs:
2174
+ return trkMeta
2175
+
2176
+ trkMeta = trkMeta.copy()
2177
+
2178
+ def _reproject_from_lonlat(df, lon_col, lat_col, x_col, y_col):
2179
+ if lon_col not in df.columns or lat_col not in df.columns:
2180
+ return df
2181
+
2182
+ lon = pd.to_numeric(df[lon_col], errors='coerce')
2183
+ lat = pd.to_numeric(df[lat_col], errors='coerce')
2184
+ valid_lonlat = lon.notna() & lat.notna()
2185
+ if not valid_lonlat.any():
2186
+ return df
2187
+
2188
+ need_xy = pd.Series(False, index=df.index)
2189
+ if x_col not in df.columns or y_col not in df.columns:
2190
+ need_xy[:] = True
2191
+ else:
2192
+ x = pd.to_numeric(df[x_col], errors='coerce')
2193
+ y = pd.to_numeric(df[y_col], errors='coerce')
2194
+
2195
+ # EPSG:326xx eastings should be positive and reasonably bounded.
2196
+ # Rebuild projected XY when stored values are missing or obviously invalid.
2197
+ need_xy = (
2198
+ x.isna() |
2199
+ y.isna() |
2200
+ (x <= 0) |
2201
+ (x > 1000000) |
2202
+ (y <= 0)
2203
+ ) & valid_lonlat
2204
+
2205
+ if not need_xy.any():
2206
+ return df
2207
+
2208
+ geo = gpd.GeoDataFrame(
2209
+ df.loc[need_xy, [lon_col, lat_col]].copy(),
2210
+ geometry=gpd.points_from_xy(lon[need_xy], lat[need_xy]),
2211
+ crs='EPSG:4326',
2212
+ ).to_crs(self.humDat['epsg'])
2213
+
2214
+ df.loc[need_xy, x_col] = geo.geometry.x.to_numpy()
2215
+ df.loc[need_xy, y_col] = geo.geometry.y.to_numpy()
2216
+ return df
2217
+
2218
+ trkMeta = _reproject_from_lonlat(trkMeta, 'trk_lons', 'trk_lats', 'trk_utm_es', 'trk_utm_ns')
2219
+ trkMeta = _reproject_from_lonlat(trkMeta, 'range_lons', 'range_lats', 'range_es', 'range_ns')
2220
+
2221
+ return trkMeta
2168
2222
 
2169
2223
  #===========================================================================
2170
2224
  def _exportCovShp(self,
@@ -2212,6 +2266,7 @@ class rectObj(sonObj):
2212
2266
  # dfs = [df1, df2]
2213
2267
 
2214
2268
  df1 = pd.read_csv(self.smthTrkFile)
2269
+ df1 = self._sanitizeProjectedTrackMeta(df1, wgs=wgs)
2215
2270
  dfs = [df1]
2216
2271
 
2217
2272
  filt = 0
@@ -260,6 +260,8 @@ class sonObj(object):
260
260
  dq_src_utc_offset=0.0,
261
261
  dq_target_utc_offset=0.0,
262
262
  dq_time_offset=0.0,
263
+ filter_coord_outliers=True,
264
+ coord_iqr_scale=3.0,
263
265
  ):
264
266
  '''
265
267
  '''
@@ -271,6 +273,11 @@ class sonObj(object):
271
273
  # print('len', len(sonDF))
272
274
  # print(sonDF)
273
275
 
276
+ ##############################
277
+ # GPS Coordinate Outlier Filter
278
+ if filter_coord_outliers:
279
+ sonDF = self._filterCoordOutliers(sonDF, iqr_scale=coord_iqr_scale)
280
+
274
281
  #############################
275
282
  # Do Heading Deviation Filter
276
283
  if max_heading_dev > 0:
@@ -690,7 +697,16 @@ class sonObj(object):
690
697
  if not filtCol in sonDF.columns:
691
698
  sonDF[filtCol] = True
692
699
 
693
- time_table = pd.read_csv(time_table)
700
+ # Guard against bool/test toggles; only load when a real table is provided.
701
+ if isinstance(time_table, (bool, np.bool_)) or time_table is None:
702
+ return sonDF
703
+ if isinstance(time_table, str) and time_table.strip() == '':
704
+ return sonDF
705
+
706
+ if isinstance(time_table, pd.DataFrame):
707
+ time_table = time_table.copy()
708
+ else:
709
+ time_table = pd.read_csv(time_table)
694
710
 
695
711
  for i, row in time_table.iterrows():
696
712
 
@@ -733,6 +749,57 @@ class sonObj(object):
733
749
 
734
750
  return sonDF
735
751
 
752
+ # ======================================================================
753
+ def _filterCoordOutliers(self,
754
+ sonDF,
755
+ iqr_scale=3.0):
756
+ '''
757
+ Flag pings with extreme GPS coordinate outliers using the IQR method
758
+ on the lon and lat columns. For each coordinate field, pings that fall
759
+ outside Q1 - iqr_scale*IQR .. Q3 + iqr_scale*IQR are marked False
760
+ in the 'filter' column.
761
+
762
+ ----------
763
+ Parameters
764
+ ----------
765
+ sonDF : DataFrame
766
+ Ping metadata dataframe.
767
+ iqr_scale : float
768
+ Multiplier applied to the IQR to set the outlier fence.
769
+ Default 3.0 (flags only extreme outliers).
770
+
771
+ -------
772
+ Returns
773
+ -------
774
+ sonDF with 'filter' column updated.
775
+ '''
776
+
777
+ filtCol = 'filter'
778
+
779
+ if filtCol not in sonDF.columns:
780
+ sonDF[filtCol] = True
781
+
782
+ for coord_col in ['lon', 'lat']:
783
+ if coord_col not in sonDF.columns:
784
+ continue
785
+
786
+ vals = sonDF[coord_col]
787
+ q1 = vals.quantile(0.25)
788
+ q3 = vals.quantile(0.75)
789
+ iqr = q3 - q1
790
+
791
+ lower = q1 - iqr_scale * iqr
792
+ upper = q3 + iqr_scale * iqr
793
+
794
+ outlier_mask = (vals < lower) | (vals > upper)
795
+ n_flagged = outlier_mask.sum()
796
+ if n_flagged > 0:
797
+ print(f"\n _filterCoordOutliers: flagged {n_flagged} pings with {coord_col} outside "
798
+ f"[{lower:.6f}, {upper:.6f}]")
799
+ sonDF.loc[outlier_mask, filtCol] = False
800
+
801
+ return sonDF
802
+
736
803
  # ======================================================================
737
804
  def _filterAOI(self,
738
805
  sonDF,
@@ -675,7 +675,16 @@ class sonObj(object):
675
675
  if not filtCol in sonDF.columns:
676
676
  sonDF[filtCol] = True
677
677
 
678
- time_table = pd.read_csv(time_table)
678
+ # Guard against bool/test toggles; only load when a real table is provided.
679
+ if isinstance(time_table, (bool, np.bool_)) or time_table is None:
680
+ return sonDF
681
+ if isinstance(time_table, str) and time_table.strip() == '':
682
+ return sonDF
683
+
684
+ if isinstance(time_table, pd.DataFrame):
685
+ time_table = time_table.copy()
686
+ else:
687
+ time_table = pd.read_csv(time_table)
679
688
 
680
689
  for i, row in time_table.iterrows():
681
690
 
@@ -126,6 +126,8 @@ def read_master_func(logfilename='',
126
126
  dq_src_utc_offset = 0.0,
127
127
  dq_target_utc_offset = 0.0,
128
128
  dq_time_offset = 0.0,
129
+ filter_coord_outliers = True,
130
+ coord_iqr_scale = 3.0,
129
131
  tempC=10,
130
132
  nchunk=500,
131
133
  cropRange=0,
@@ -1025,7 +1027,7 @@ def read_master_func(logfilename='',
1025
1027
  # For Filtering #
1026
1028
  ############################################################################
1027
1029
 
1028
- if dq_table or max_heading_deviation > 0 or min_speed > 0 or max_speed > 0 or aoi or time_table:
1030
+ if dq_table or max_heading_deviation > 0 or min_speed > 0 or max_speed > 0 or aoi or time_table or filter_coord_outliers:
1029
1031
 
1030
1032
  start_time = time.time()
1031
1033
 
@@ -1060,7 +1062,8 @@ def read_master_func(logfilename='',
1060
1062
  son0 = portstar[maxRec]
1061
1063
  df0 = son0._doSonarFiltering(max_heading_deviation, max_heading_distance, min_speed, max_speed, aoi, time_table,
1062
1064
  dq_table, dq_time_field, dq_flag_field, dq_keep_values,
1063
- dq_src_utc_offset, dq_target_utc_offset, dq_time_offset)
1065
+ dq_src_utc_offset, dq_target_utc_offset, dq_time_offset,
1066
+ filter_coord_outliers=filter_coord_outliers, coord_iqr_scale=coord_iqr_scale)
1064
1067
 
1065
1068
  # Add filter to other beam
1066
1069
  son1 = portstar[minRec]
@@ -1110,7 +1113,8 @@ def read_master_func(logfilename='',
1110
1113
  for son in downbeams:
1111
1114
  df = son._doSonarFiltering(max_heading_deviation, max_heading_distance, min_speed, max_speed, aoi, time_table,
1112
1115
  dq_table, dq_time_field, dq_flag_field, dq_keep_values,
1113
- dq_src_utc_offset, dq_target_utc_offset, dq_time_offset)
1116
+ dq_src_utc_offset, dq_target_utc_offset, dq_time_offset,
1117
+ filter_coord_outliers=filter_coord_outliers, coord_iqr_scale=coord_iqr_scale)
1114
1118
 
1115
1119
  df = df[df['filter'] == True]
1116
1120
 
@@ -0,0 +1 @@
1
+ __version__ = '5.4.2'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.4.0
3
+ Version: 5.4.2
4
4
  Summary: Open-source interface for processing recreation-grade side scan sonar datasets and reproducibly mapping benthic habitat
5
5
  Author: Daniel Buscombe
6
6
  Author-email: Cameron Bodine <bodine.cs@gmail.email>
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering :: Visualization
17
17
  Classifier: Topic :: Scientific/Engineering :: Oceanography
18
18
  Classifier: Topic :: Scientific/Engineering :: GIS
19
19
  Classifier: Topic :: Scientific/Engineering :: Hydrology
20
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.6
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: pinginstaller<3,>=2
@@ -33,6 +33,7 @@ Requires-Dist: tensorflow<3,>=2.20; extra == "ml"
33
33
  Requires-Dist: tf-keras<3,>=2.20; extra == "ml"
34
34
  Requires-Dist: transformers<5,>=4.57; extra == "ml"
35
35
  Dynamic: license-file
36
+ Dynamic: requires-python
36
37
 
37
38
  # PING-Mapper
38
39
  [![PyPI - Version](https://img.shields.io/pypi/v/pingmapper?style=flat-square&label=Latest%20Version%20(PyPi))](https://pypi.org/project/pingmapper/)
@@ -1,6 +1,7 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
+ setup.py
4
5
  pingmapper/__init__.py
5
6
  pingmapper/__main__.py
6
7
  pingmapper/class_mapSubstrateObj.py
@@ -0,0 +1,50 @@
1
+ from setuptools import setup, find_packages
2
+ from pathlib import Path
3
+
4
+ DESCRIPTION = 'Open-source interface for processing recreation-grade side scan sonar datasets and reproducibly mapping benthic habitat'
5
+ LONG_DESCRIPTION = Path('README.md').read_text()
6
+
7
+ exec(open('pingmapper/version.py').read())
8
+
9
+ setup(
10
+ name="pingmapper",
11
+ version=__version__,
12
+ author="Cameron Bodine, Daniel Buscombe",
13
+ author_email="bodine.cs@gmail.email",
14
+ description=DESCRIPTION,
15
+ long_description=LONG_DESCRIPTION,
16
+ long_description_content_type='text/markdown',
17
+ packages=find_packages(),
18
+ data_files=[("pingmapper_config", ["pingmapper/default_params.json"])],
19
+ classifiers=[
20
+ "Development Status :: 5 - Production/Stable",
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Topic :: Scientific/Engineering",
25
+ "Topic :: Scientific/Engineering :: Visualization",
26
+ "Topic :: Scientific/Engineering :: Oceanography",
27
+ "Topic :: Scientific/Engineering :: GIS",
28
+ "Topic :: Scientific/Engineering :: Hydrology"
29
+ ],
30
+ keywords=[
31
+ "pingmapper",
32
+ "sonar",
33
+ "ecology",
34
+ "remotesensing",
35
+ "sidescan",
36
+ "sidescan-sonar",
37
+ "aquatic",
38
+ "humminbird",
39
+ "lowrance",
40
+ "gis",
41
+ "oceanography",
42
+ "limnology",],
43
+ python_requires=">=3.6",
44
+ install_requires=['pinginstaller', 'pingwizard', 'pingverter'],
45
+ project_urls={
46
+ "Issues": "https://github.com/CameronBodine/PINGMapper/issues",
47
+ "GitHub":"https://github.com/CameronBodine/PINGMapper",
48
+ "Homepage":"https://cameronbodine.github.io/PINGMapper/",
49
+ },
50
+ )
@@ -1 +0,0 @@
1
- __version__ = '5.4.0'
File without changes
File without changes
File without changes
File without changes