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.
- {pingmapper-5.4.0 → pingmapper-5.4.2}/PKG-INFO +3 -2
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_portstarObj.py +376 -28
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_rectObj.py +55 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_sonObj.py +68 -1
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_sonObj_nadirgaptest.py +10 -1
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/main_readFiles.py +7 -3
- pingmapper-5.4.2/pingmapper/version.py +1 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/PKG-INFO +3 -2
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/SOURCES.txt +1 -0
- pingmapper-5.4.2/setup.py +50 -0
- pingmapper-5.4.0/pingmapper/version.py +0 -1
- {pingmapper-5.4.0 → pingmapper-5.4.2}/LICENSE +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/README.md +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/__init__.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/__main__.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/class_mapSubstrateObj.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/default_params.json +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/doWork.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/funcs_common.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/funcs_model.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/funcs_rectify.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/gui_main.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/main_mapSubstrate.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/main_rectify.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/nonGUI_batch_main.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/nonGui_main.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/scratch/main.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/scratch/main_batchDirectory.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/test_PINGMapper.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/test_dq_filter.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/test_time.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/export_coverage.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/main_mosaic_transects.py +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/dependency_links.txt +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/requires.txt +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper.egg-info/top_level.txt +0 -0
- {pingmapper-5.4.0 → pingmapper-5.4.2}/pyproject.toml +0 -0
- {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.
|
|
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.
|
|
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
|
[)](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/
|
|
1203
|
-
|
|
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
|
-
|
|
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.
|
|
1411
|
-
bedMax = int(np.
|
|
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
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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 =
|
|
1872
|
-
starInst =
|
|
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 =
|
|
1875
|
-
portInst =
|
|
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
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
[)](https://pypi.org/project/pingmapper/)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pingmapper-5.4.0 → pingmapper-5.4.2}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|