pingmapper 5.4.1__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.1 → pingmapper-5.4.2}/PKG-INFO +3 -2
  2. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/class_portstarObj.py +164 -28
  3. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/class_rectObj.py +55 -0
  4. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/class_sonObj.py +68 -1
  5. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/class_sonObj_nadirgaptest.py +10 -1
  6. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/main_readFiles.py +7 -3
  7. pingmapper-5.4.2/pingmapper/version.py +1 -0
  8. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper.egg-info/PKG-INFO +3 -2
  9. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper.egg-info/SOURCES.txt +1 -0
  10. pingmapper-5.4.2/setup.py +50 -0
  11. pingmapper-5.4.1/pingmapper/version.py +0 -1
  12. {pingmapper-5.4.1 → pingmapper-5.4.2}/LICENSE +0 -0
  13. {pingmapper-5.4.1 → pingmapper-5.4.2}/README.md +0 -0
  14. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/__init__.py +0 -0
  15. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/__main__.py +0 -0
  16. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/class_mapSubstrateObj.py +0 -0
  17. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/default_params.json +0 -0
  18. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/doWork.py +0 -0
  19. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/funcs_common.py +0 -0
  20. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/funcs_model.py +0 -0
  21. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/funcs_rectify.py +0 -0
  22. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/gui_main.py +0 -0
  23. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/main_mapSubstrate.py +0 -0
  24. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/main_rectify.py +0 -0
  25. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/nonGUI_batch_main.py +0 -0
  26. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/nonGui_main.py +0 -0
  27. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
  28. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
  29. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
  30. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/scratch/main.py +0 -0
  31. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/scratch/main_batchDirectory.py +0 -0
  32. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/test_PINGMapper.py +0 -0
  33. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/test_dq_filter.py +0 -0
  34. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/test_time.py +0 -0
  35. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
  36. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
  37. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
  38. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
  39. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
  40. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
  41. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
  42. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
  43. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
  44. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
  45. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
  46. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
  47. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
  48. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
  49. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
  50. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
  51. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
  52. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/export_coverage.py +0 -0
  53. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper/utils/main_mosaic_transects.py +0 -0
  54. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper.egg-info/dependency_links.txt +0 -0
  55. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper.egg-info/requires.txt +0 -0
  56. {pingmapper-5.4.1 → pingmapper-5.4.2}/pingmapper.egg-info/top_level.txt +0 -0
  57. {pingmapper-5.4.1 → pingmapper-5.4.2}/pyproject.toml +0 -0
  58. {pingmapper-5.4.1 → 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.1
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__))
@@ -1650,8 +1651,9 @@ class portstarObj(object):
1650
1651
  return pd.Series(vals).rolling(window=window, center=True, min_periods=1).median().to_numpy()
1651
1652
 
1652
1653
  def _flag_depth_outliers(depth, inst_depth=None, inst_depth_mult=None,
1653
- resid_floor_m=0.5, jump_floor_m=0.5,
1654
- iterative_jump=False):
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):
1655
1657
  depth = np.asarray(depth, dtype=float)
1656
1658
  flags = np.zeros(depth.shape, dtype=bool)
1657
1659
 
@@ -1690,7 +1692,11 @@ class portstarObj(object):
1690
1692
  # back those runs until continuity is restored.
1691
1693
  if iterative_jump:
1692
1694
  work = depth.copy()
1693
- max_iter = max(1, depth.size)
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))
1694
1700
  for _ in range(max_iter):
1695
1701
  valid_work = np.isfinite(work) & (work > 0)
1696
1702
  valid_idx = np.flatnonzero(valid_work)
@@ -1704,7 +1710,7 @@ class portstarObj(object):
1704
1710
 
1705
1711
  step_center = np.nanmedian(step_vals)
1706
1712
  step_mad = np.nanmedian(np.abs(step_vals - step_center))
1707
- jump_thr = max(jump_floor_m, 6.0 * step_mad if np.isfinite(step_mad) else jump_floor_m)
1713
+ jump_thr = max(iterative_jump_floor_m, 6.0 * step_mad if np.isfinite(step_mad) else iterative_jump_floor_m)
1708
1714
 
1709
1715
  diffs = np.abs(np.diff(work[valid_idx]))
1710
1716
  bad_step_pos = np.flatnonzero(np.isfinite(diffs) & (diffs > jump_thr))
@@ -1756,15 +1762,31 @@ class portstarObj(object):
1756
1762
 
1757
1763
  trk_df.reset_index().to_csv(trk_file, index=False, float_format='%.14f')
1758
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
+
1759
1775
  # Load sonar metadata file
1760
1776
  self.port._loadSonMeta()
1761
1777
  portDF = self.port.sonMetaDF
1762
1778
  self.star._loadSonMeta()
1763
1779
  starDF = self.star.sonMetaDF
1780
+ _depth_timing('load metadata')
1764
1781
 
1765
1782
  # Get all chunks
1766
1783
  chunks = pd.unique(portDF['chunk_id'])
1767
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
+
1768
1790
  if detectDep == 0:
1769
1791
  def _depth_series(df):
1770
1792
  # Some formats (e.g., Garmin RSD) may not include dep_m in metadata.
@@ -1786,6 +1808,36 @@ class portstarObj(object):
1786
1808
  portInstDepth = np.where(portValid, portInstDepth, portMetaDepth)
1787
1809
  starInstDepth = np.where(starValid, starInstDepth, starMetaDepth)
1788
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
+
1789
1841
  if smthDep:
1790
1842
  # print("\nSmoothing depth values...")
1791
1843
  portInstDepth = savgol_filter(portInstDepth, 51, 3)
@@ -1818,6 +1870,7 @@ class portstarObj(object):
1818
1870
 
1819
1871
  portDF['dep_m_adjBy'] = _format_depth_adjustment(portDF['pixM'])
1820
1872
  starDF['dep_m_adjBy'] = _format_depth_adjustment(starDF['pixM'])
1873
+ _depth_timing('prepare instrument depth')
1821
1874
 
1822
1875
  elif detectDep > 0:
1823
1876
  # Prepare depth detection dictionaries
@@ -1904,12 +1957,15 @@ class portstarObj(object):
1904
1957
 
1905
1958
  portDF['dep_m_adjBy'] = _format_depth_adjustment(portDF['pixM'])
1906
1959
  starDF['dep_m_adjBy'] = _format_depth_adjustment(starDF['pixM'])
1960
+ _depth_timing('prepare detected depth')
1907
1961
 
1908
1962
  # Outlier and jump filtering before interpolation.
1909
1963
  # detectDep=0: continuity-only acoustic QC.
1910
1964
  # detectDep=1/2: continuity QC plus instrument-depth proportional cap.
1911
1965
  portArr = pd.to_numeric(portDF['dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1912
1966
  starArr = pd.to_numeric(starDF['dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1967
+ portRawArr = portArr.copy()
1968
+ starRawArr = starArr.copy()
1913
1969
  portInst = pd.to_numeric(portDF['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1914
1970
  starInst = pd.to_numeric(starDF['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
1915
1971
 
@@ -1917,8 +1973,8 @@ class portstarObj(object):
1917
1973
  starFlags = np.zeros(starArr.shape, dtype=bool)
1918
1974
 
1919
1975
  if detectDep == 0:
1920
- portFlags |= _flag_depth_outliers(portArr, iterative_jump=True)
1921
- starFlags |= _flag_depth_outliers(starArr, iterative_jump=True)
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)
1922
1978
  elif detectDep in (1, 2):
1923
1979
  portFlags |= _flag_depth_outliers(portArr, portInst, inst_depth_mult=3.0)
1924
1980
  starFlags |= _flag_depth_outliers(starArr, starInst, inst_depth_mult=3.0)
@@ -1949,14 +2005,17 @@ class portstarObj(object):
1949
2005
  portArr[portFlags] = np.nan
1950
2006
  starArr[starFlags] = np.nan
1951
2007
  portDF['dep_m'] = portArr
2008
+ portDF['dep_m_raw'] = portRawArr
1952
2009
  starDF['dep_m'] = starArr
2010
+ starDF['dep_m_raw'] = starRawArr
2011
+ _depth_timing('outlier and jump filtering')
1953
2012
 
1954
2013
  # Interpolate over nan's (and set zeros to nan)
1955
2014
  portDep = portDF['dep_m'].to_numpy(copy=True)
1956
2015
  starDep = starDF['dep_m'].to_numpy(copy=True)
1957
2016
 
1958
- portInterp = np.isnan(portDep) | (portDep == 0)
1959
- starInterp = np.isnan(starDep) | (starDep == 0)
2017
+ portInterp = np.isnan(portDep) | (portDep == 0) | portPreFlags
2018
+ starInterp = np.isnan(starDep) | (starDep == 0) | starPreFlags
1960
2019
 
1961
2020
  portDep[portDep == 0] = np.nan
1962
2021
  starDep[starDep == 0] = np.nan
@@ -1976,12 +2035,14 @@ class portstarObj(object):
1976
2035
  starDep[nans] = 0
1977
2036
  starDF['dep_m'] = starDep
1978
2037
  starDF['dep_m_interp'] = starInterp.astype(np.uint8)
2038
+ _depth_timing('interpolate depth')
1979
2039
 
1980
2040
  # Export to csv
1981
2041
  portDF.to_csv(self.port.sonMetaFile, index=False, float_format='%.14f')
1982
2042
  starDF.to_csv(self.star.sonMetaFile, index=False, float_format='%.14f')
1983
2043
  _sync_trackline_depth(self.port, portDF)
1984
2044
  _sync_trackline_depth(self.star, starDF)
2045
+ _depth_timing('write metadata and trackline')
1985
2046
 
1986
2047
  try:
1987
2048
  # Take average of both estimates to store with downlooking sonar csv
@@ -2003,6 +2064,9 @@ class portstarObj(object):
2003
2064
  depDF['dep_m_adjBy'] = portDF['dep_m_adjBy']
2004
2065
  depDF['dep_m_interp'] = portDF['dep_m_interp']
2005
2066
 
2067
+ _depth_timing('build return depth dataframe')
2068
+ print('\tDepth timing [{}] total: {:.2f}s'.format(beam_pair, time.perf_counter() - depth_timer_start))
2069
+
2006
2070
  del portDF, starDF
2007
2071
  gc.collect()
2008
2072
  return depDF
@@ -2059,20 +2123,65 @@ class portstarObj(object):
2059
2123
  self.star._loadSonMeta()
2060
2124
  starDF = self.star.sonMetaDF
2061
2125
 
2062
- portDF = portDF.loc[portDF['chunk_id'] == i, ['inst_dep_m', 'dep_m', 'pixM']]
2063
- 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)))
2064
2157
 
2065
2158
  portInstDepth = pd.to_numeric(portDF['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
2066
2159
  portMetaDepth = pd.to_numeric(portDF['dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
2067
- portInstDepth = np.where(np.isfinite(portInstDepth) & (portInstDepth > 0), portInstDepth, portMetaDepth)
2068
- portInst = np.nan_to_num(portInstDepth / portDF['pixM'].to_numpy(dtype=float), nan=0.0).astype(int)
2069
- 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)
2070
2171
 
2071
2172
  starInstDepth = pd.to_numeric(starDF['inst_dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
2072
2173
  starMetaDepth = pd.to_numeric(starDF['dep_m'], errors='coerce').to_numpy(dtype=float, copy=True)
2073
- starInstDepth = np.where(np.isfinite(starInstDepth) & (starInstDepth > 0), starInstDepth, starMetaDepth)
2074
- starInst = np.nan_to_num(starInstDepth / starDF['pixM'].to_numpy(dtype=float), nan=0.0).astype(int)
2075
- 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)
2076
2185
 
2077
2186
  # Ensure port/star same length
2078
2187
  if (portAuto.shape[0] != starAuto.shape[0]):
@@ -2080,27 +2189,37 @@ class portstarObj(object):
2080
2189
  sL = starAuto.shape[0]
2081
2190
  # Add rows to shortest array from longest array
2082
2191
  if (pL > sL):
2083
- starAuto = np.append(starAuto, portAuto[(sL-pL):])
2084
- 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)
2085
2196
  else:
2086
- portAuto = np.append(portAuto, starAuto[(pL-sL):])
2087
- 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)
2088
2201
 
2089
2202
  # Relocate depths relative to horizontal center of image
2090
2203
  c = int(mergeSon.shape[1]/2)
2091
2204
 
2092
2205
  portInst = c - portInst
2093
2206
  portAuto = c - portAuto
2207
+ portRaw = c - portRaw
2094
2208
 
2095
2209
  starInst = c + starInst
2096
2210
  starAuto = c + starAuto
2211
+ starRaw = c + starRaw
2097
2212
 
2098
2213
  # maybe flip???
2099
2214
  portInst = np.flip(portInst)
2100
2215
  portAuto = np.flip(portAuto)
2216
+ portRaw = np.flip(portRaw)
2217
+ portInterp = np.flip(portInterp)
2101
2218
 
2102
2219
  starInst = np.flip(starInst)
2103
2220
  starAuto = np.flip(starAuto)
2221
+ starRaw = np.flip(starRaw)
2222
+ starInterp = np.flip(starInterp)
2104
2223
 
2105
2224
  #############
2106
2225
  # Export Plot
@@ -2127,14 +2246,31 @@ class portstarObj(object):
2127
2246
  outFile = os.path.join(outDir, projName+'_Bedpick_'+addZero+str(i)+tileFile)
2128
2247
 
2129
2248
  plt.imshow(mergeSon, cmap='gray')
2130
- if acousticBed:
2131
- plt.plot(portInst, y, 'r-.', lw=1, label='Acoustic Depth')
2132
- plt.plot(starInst, y, 'r-.', lw=1)
2133
- del portInst, starInst
2134
- if autoBed:
2135
- plt.plot(portAuto, y, 'b-.', lw=1, label='Auto Depth')
2136
- plt.plot(starAuto, y, 'b-.', lw=1)
2137
- 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
2138
2274
 
2139
2275
  plt.legend(loc = 'lower right', prop={'size':4}) # create the plot legend
2140
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.1
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.1'
File without changes
File without changes
File without changes
File without changes