pingmapper 5.3.0__tar.gz → 5.3.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 (54) hide show
  1. {pingmapper-5.3.0 → pingmapper-5.3.2}/PKG-INFO +1 -1
  2. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/class_mapSubstrateObj.py +14 -2
  3. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/class_portstarObj.py +96 -70
  4. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/class_sonObj.py +40 -28
  5. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/class_sonObj_nadirgaptest.py +37 -48
  6. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/doWork.py +21 -1
  7. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/funcs_common.py +6 -0
  8. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/funcs_model.py +64 -3
  9. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/main_mapSubstrate.py +16 -1
  10. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/main_readFiles.py +66 -46
  11. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/main_rectify.py +9 -0
  12. pingmapper-5.3.2/pingmapper/version.py +1 -0
  13. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper.egg-info/PKG-INFO +1 -1
  14. pingmapper-5.3.0/pingmapper/version.py +0 -1
  15. {pingmapper-5.3.0 → pingmapper-5.3.2}/LICENSE +0 -0
  16. {pingmapper-5.3.0 → pingmapper-5.3.2}/README.md +0 -0
  17. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/__init__.py +0 -0
  18. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/__main__.py +0 -0
  19. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/class_rectObj.py +0 -0
  20. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/default_params.json +0 -0
  21. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/funcs_rectify.py +0 -0
  22. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/gui_main.py +0 -0
  23. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
  24. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
  25. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
  26. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/scratch/main.py +0 -0
  27. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/scratch/main_batchDirectory.py +0 -0
  28. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/test_PINGMapper.py +0 -0
  29. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/test_time.py +0 -0
  30. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
  31. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
  32. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
  33. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
  34. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
  35. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
  36. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
  37. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
  38. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
  39. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
  40. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
  41. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
  42. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
  43. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
  44. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
  45. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
  46. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
  47. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/export_coverage.py +0 -0
  48. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper/utils/main_mosaic_transects.py +0 -0
  49. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper.egg-info/SOURCES.txt +0 -0
  50. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper.egg-info/dependency_links.txt +0 -0
  51. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper.egg-info/requires.txt +0 -0
  52. {pingmapper-5.3.0 → pingmapper-5.3.2}/pingmapper.egg-info/top_level.txt +0 -0
  53. {pingmapper-5.3.0 → pingmapper-5.3.2}/pyproject.toml +0 -0
  54. {pingmapper-5.3.0 → pingmapper-5.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.3.0
3
+ Version: 5.3.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>
@@ -119,7 +119,7 @@ class mapSubObj(rectObj):
119
119
  # Save predictions to npz
120
120
  self._saveSubstrateNpz(substratePred, i, MY_CLASS_NAMES)
121
121
 
122
- del self.substrateModel, substratePred
122
+ del substratePred
123
123
  gc.collect()
124
124
 
125
125
  return
@@ -198,8 +198,20 @@ class mapSubObj(rectObj):
198
198
  del sonWin, wStart, wEnd, softmax_score, est_label
199
199
 
200
200
  # Take mean across all windows to get one final softmax_score array
201
- fSoftmax = np.nanmean(np.stack(winSoftMax, axis=0), axis=0)
201
+ # Compute nan-mean incrementally to avoid allocating a large stacked array
202
+ _first = winSoftMax[0]
203
+ fSum = np.where(~np.isnan(_first), _first, 0.0)
204
+ fCount = (~np.isnan(_first)).astype(np.float32)
205
+ del _first
206
+ for _arr in winSoftMax[1:]:
207
+ _valid = ~np.isnan(_arr)
208
+ fSum += np.where(_valid, _arr, 0.0)
209
+ fCount += _valid
210
+ del _arr, _valid
202
211
  del winSoftMax
212
+ with np.errstate(invalid='ignore'):
213
+ fSoftmax = np.where(fCount > 0, fSum / fCount, np.nan)
214
+ del fSum, fCount
203
215
 
204
216
  # Crop center chunk predictions and recover original dims
205
217
  h, w = origDims # Center chunks original dims
@@ -1349,7 +1349,7 @@ class portstarObj(object):
1349
1349
  del son3bnd, init_label, init_prob, crop_label, crop_prob, sonCrop
1350
1350
  del maxDepths, minDepths, avgDepths, Wp, portDepPixCrop, starDepPixCrop
1351
1351
  del portDepPix, starDepPix
1352
- del model, self.bedpickModel ######## Not sure about this one...
1352
+ del model
1353
1353
 
1354
1354
  # return #self
1355
1355
  return portDepPixFinal, starDepPixFinal, i
@@ -2052,7 +2052,7 @@ class portstarObj(object):
2052
2052
  port_pix = self._getShadowPix(port_label, remShadow)
2053
2053
  star_pix = self._getShadowPix(star_label, remShadow)
2054
2054
 
2055
- del self.shadowModel, model
2055
+ del model
2056
2056
  gc.collect()
2057
2057
  return i, port_pix, star_pix
2058
2058
 
@@ -2101,48 +2101,24 @@ class portstarObj(object):
2101
2101
  del son.sonDat
2102
2102
 
2103
2103
  # Get rid of zeros along water column area
2104
+ # Vectorized: replace class-8 (water column) labels in non-WC area with 0
2105
+ dep_arr = np.asarray(son.bedPick, dtype=int) # (W,)
2106
+ row_idx = np.arange(label.shape[0])[:, np.newaxis] # (H, 1)
2107
+ below_wc = row_idx >= dep_arr[np.newaxis, :] # (H, W)
2108
+ label = np.where(below_wc & (label == 8), 0, label)
2109
+
2110
+ # Zero-fill per ping: propagate the class at the edge of any zero region
2111
+ # into that region. Zeros should form at most one contiguous block per ping
2112
+ # (the comment in the original confirms this). Skip pings with no zeros.
2104
2113
  for p in range(label.shape[1]):
2105
- # Get depth
2106
- d = son.bedPick[p]
2107
-
2108
- # Get ping classification
2109
- ping = label[:, p]
2110
-
2111
- # Get water column
2112
- wc = ping[:d]
2113
-
2114
- # Remove water column
2115
- ping = ping[d:]
2116
-
2117
- # If any water column pics remain, set to zero
2118
- ping = np.where(ping==8, 0, ping)
2119
-
2120
- ##############
2121
- # Remove zeros
2122
- # Find zeros. Should be grouped in contiguous arrays (array[0, 1, 2], array[100, 101, 102], ...
2123
- zero = np.where(ping==0)
2124
-
2125
- if len(zero[0])>0:
2126
- for z in zero:
2127
- # Get index of first and last zero
2128
- f, l = z[0], z[-1]
2129
-
2130
- # Don't fall off edge
2131
- if l+1 < ping.shape[0]:
2132
-
2133
- # Get classification of next pixel
2134
- c = ping[l+1]
2135
-
2136
- # Fill zero region with c
2137
- ping[f:l+1] = c
2138
-
2139
- # Add water column back in
2140
- ping = list(wc)+list(ping)
2141
-
2142
- # Update objects filled with ping
2143
- label[:, p] = ping
2144
-
2145
- del ping
2114
+ d = int(son.bedPick[p])
2115
+ ping_below = label[d:, p]
2116
+ zero_idx = np.where(ping_below == 0)[0]
2117
+ if len(zero_idx) == 0:
2118
+ continue
2119
+ f, l = int(zero_idx[0]), int(zero_idx[-1])
2120
+ if d + l + 1 < label.shape[0]:
2121
+ label[d + f : d + l + 1, p] = ping_below[l + 1]
2146
2122
 
2147
2123
  son.sonDat = label
2148
2124
 
@@ -2299,30 +2275,58 @@ class portstarObj(object):
2299
2275
  # # Do shadow and water column removal
2300
2276
 
2301
2277
  # Store pred stack in variable
2302
- predStack = son.sonDat
2303
-
2304
- # Iterate each classification layer
2305
- for c in range(classes):
2306
- # Get class prediction
2307
- son.sonDat = predStack[:,:,c]
2308
-
2309
- # Remove shadows
2310
- # Get mask
2311
- son._SHW_mask(chunk, son=False)
2278
+ predStack = son.sonDat # (H, W, C)
2279
+ H_pred, W_pred, C_pred = predStack.shape
2312
2280
 
2313
- # Mask out shadows
2314
- son.sonDat = son.sonDat*son.shadowMask
2315
-
2316
- # Remove Water Column
2317
- son._WCR_SRC(sonMeta, son=False)
2318
-
2319
- # Update predStack
2320
- predStack[:,:,c] = son.sonDat
2321
-
2322
- del son.sonDat
2281
+ # Compute shadow mask ONCE — geometry depends only on son.shadow[chunk],
2282
+ # not on which class band we are processing.
2283
+ son.sonDat = predStack[:, :, 0]
2284
+ son._SHW_mask(chunk, son=False)
2285
+ shw_mask = son.shadowMask # (H, W)
2286
+ del son.shadowMask
2323
2287
 
2324
- # Store in son.sonDat
2325
- son.sonDat = predStack
2288
+ # Apply shadow mask to all classes in a single vectorised broadcast
2289
+ predStack = predStack * shw_mask[:, :, np.newaxis]
2290
+
2291
+ # Apply slant range correction (SRC) to all classes in one pass.
2292
+ # Original _WCR_SRC ran an O(H) Python inner loop for every (class, ping)
2293
+ # pair. Here we vectorise the H dimension with numpy and process all C
2294
+ # classes simultaneously, reducing Python iterations from C×W×H → W×C.
2295
+ bedPick = round(sonMeta['dep_m'] / sonMeta['pixM'], 0).astype(int).reset_index(drop=True)
2296
+ srcStack = np.zeros((H_pred, W_pred, C_pred), dtype=np.float32)
2297
+ for j in range(W_pred):
2298
+ depth = int(bedPick[j])
2299
+ if depth >= H_pred:
2300
+ continue
2301
+ dd = float(depth * depth)
2302
+ row_arr = np.arange(depth, H_pred)
2303
+ src_idx = np.round(
2304
+ np.sqrt(row_arr.astype(np.float64) ** 2 - dd)
2305
+ ).astype(int)
2306
+ valid = src_idx < H_pred
2307
+ rows_v = row_arr[valid]
2308
+ sidx_v = src_idx[valid]
2309
+ if len(sidx_v) == 0:
2310
+ continue
2311
+ data_extent = int(sidx_v[-1])
2312
+
2313
+ # Scatter all C classes at once, then zero past range extent
2314
+ pingStack = np.full((H_pred, C_pred), np.nan, dtype=np.float32)
2315
+ pingStack[sidx_v, :] = predStack[rows_v, j, :].astype(np.float32)
2316
+ pingStack[data_extent:, :] = 0
2317
+
2318
+ # Interpolate gaps — NaN pattern is geometry-driven, same for all classes
2319
+ nans = np.isnan(pingStack[:, 0])
2320
+ if nans.any():
2321
+ x = np.arange(H_pred)
2322
+ xnans, xvalid = x[nans], x[~nans]
2323
+ for c in range(C_pred):
2324
+ pingStack[nans, c] = np.interp(
2325
+ xnans, xvalid, pingStack[~nans, c]
2326
+ )
2327
+ srcStack[:, j, :] = pingStack
2328
+
2329
+ son.sonDat = srcStack
2326
2330
 
2327
2331
  # Iterate each class again and:
2328
2332
  for c in range(classes):
@@ -2411,6 +2415,20 @@ class portstarObj(object):
2411
2415
 
2412
2416
 
2413
2417
 
2418
+ #=======================================================================
2419
+ def _preloadRectifyCache(self):
2420
+ '''
2421
+ Pre-load the trackline and sonar-metadata CSVs that _rectify reads
2422
+ on every chunk call. Call this ONCE on the portstarObj in the main
2423
+ process before Parallel dispatch so that each serialised copy of the
2424
+ object already has the data, avoiding per-chunk disk reads.
2425
+ '''
2426
+ portTrkMetaFile = os.path.join(self.port.metaDir, "Trackline_Smth_"+self.port.beamName+".csv")
2427
+ starTrkMetaFile = os.path.join(self.star.metaDir, "Trackline_Smth_"+self.star.beamName+".csv")
2428
+ self.port._trkMetaDF = pd.read_csv(portTrkMetaFile)
2429
+ self.star._trkMetaDF = pd.read_csv(starTrkMetaFile)
2430
+ self.port._loadSonMeta() # populates self.port.sonMetaDF
2431
+
2414
2432
  #=======================================================================
2415
2433
  def _rectify(self, dat, chunk, imgOutPrefix, filt=50, wgs=False, return_rect=False):
2416
2434
  '''
@@ -2473,8 +2491,11 @@ class portstarObj(object):
2473
2491
 
2474
2492
  ###
2475
2493
  # Get top (port range) coordinates
2476
- trkMeta = pd.read_csv(portTrkMetaFile)
2477
- trkMeta = trkMeta[trkMeta['chunk_id']==chunk].reset_index(drop=False) # Filter df by chunk_id
2494
+ # Cache the full CSV on self.port so we only read from disk once per portstarObj
2495
+ if not hasattr(self.port, '_trkMetaDF') or self.port._trkMetaDF is None:
2496
+ self.port._trkMetaDF = pd.read_csv(portTrkMetaFile)
2497
+ portTrkFull = self.port._trkMetaDF
2498
+ trkMeta = portTrkFull[portTrkFull['chunk_id']==chunk].reset_index(drop=False)
2478
2499
 
2479
2500
  # Get range (outer extent) coordinates [xR, yR] to transposed numpy arrays
2480
2501
  xTop, yTop = trkMeta[xRange].to_numpy().T, trkMeta[yRange].to_numpy().T
@@ -2482,8 +2503,11 @@ class portstarObj(object):
2482
2503
 
2483
2504
  ###
2484
2505
  # Get bottom (star range) coordinates
2485
- trkMeta = pd.read_csv(starTrkMetaFile)
2486
- trkMeta = trkMeta[trkMeta['chunk_id']==chunk].reset_index(drop=False) # Filter df by chunk_id
2506
+ # Cache the full CSV on self.star so we only read from disk once per portstarObj
2507
+ if not hasattr(self.star, '_trkMetaDF') or self.star._trkMetaDF is None:
2508
+ self.star._trkMetaDF = pd.read_csv(starTrkMetaFile)
2509
+ starTrkFull = self.star._trkMetaDF
2510
+ trkMeta = starTrkFull[starTrkFull['chunk_id']==chunk].reset_index(drop=False)
2487
2511
 
2488
2512
  # Get range (outer extent) coordinates [xR, yR] to transposed numpy arrays
2489
2513
  xBot, yBot = trkMeta[xRange].to_numpy().T, trkMeta[yRange].to_numpy().T
@@ -2507,7 +2531,9 @@ class portstarObj(object):
2507
2531
 
2508
2532
  # Get pixel size
2509
2533
  # pix_m = self.port.pixM
2510
- self.port._loadSonMeta()
2534
+ # Cache sonMetaDF — _loadSonMeta reads the same CSV every call
2535
+ if not hasattr(self.port, 'sonMetaDF') or self.port.sonMetaDF is None:
2536
+ self.port._loadSonMeta()
2511
2537
  isChunk = self.port.sonMetaDF['chunk_id']==chunk
2512
2538
  sonMeta = self.port.sonMetaDF[isChunk].reset_index()
2513
2539
 
@@ -1310,43 +1310,55 @@ class sonObj(object):
1310
1310
  # bedPick = round(sonMeta['dep_m'] / sonMeta['pix_m'], 0).astype(int)
1311
1311
  bedPick = round(sonMeta['dep_m'] / sonMeta['pixM'], 0).astype(int).reset_index(drop=True)
1312
1312
 
1313
+ H, W = self.sonDat.shape[0], self.sonDat.shape[1]
1314
+
1313
1315
  # Initialize 2d array to store relocated sonar records
1314
- srcDat = np.zeros((self.sonDat.shape[0], self.sonDat.shape[1])).astype(np.float32)#.astype(int)
1316
+ srcDat = np.zeros((H, W), dtype=np.float32)
1317
+
1318
+ # Iterate each ping. The inner row-loop is replaced with numpy vector
1319
+ # ops: compute all slant→horizontal index mappings at once, scatter
1320
+ # intensities, then interpolate gaps.
1321
+ for j in range(W):
1322
+ depth = int(bedPick[j]) # depth at nadir (pixels)
1323
+ if depth >= H:
1324
+ continue
1325
+ dd = float(depth * depth)
1326
+
1327
+ # Rows at or beyond the water column boundary
1328
+ row_arr = np.arange(depth, H)
1329
+ # Horizontal range index via Pythagorean theorem (vectorised)
1330
+ src_idx = np.round(
1331
+ np.sqrt(row_arr.astype(np.float64) ** 2 - dd)
1332
+ ).astype(int)
1333
+
1334
+ # Discard out-of-bounds indices
1335
+ valid = src_idx < H
1336
+ rows_v = row_arr[valid]
1337
+ sidx_v = src_idx[valid]
1338
+ if len(sidx_v) == 0:
1339
+ continue
1315
1340
 
1316
- #Iterate each ping
1317
- for j in range(self.sonDat.shape[1]):
1318
- depth = bedPick[j] # Get depth (in pixels) at nadir
1319
- dd = depth**2
1320
- # Create 1d array to store relocated bed pixels. Set to nan so we
1321
- ## can later interpolate over gaps.
1322
- pingDat = (np.ones((self.sonDat.shape[0])).astype(np.float32)) * np.nan
1323
- dataExtent = 0
1324
- #Iterate each sonar/ping return
1325
- for i in range(self.sonDat.shape[0]):
1326
- if i >= depth:
1327
- intensity = self.sonDat[i,j] # Get the intensity value
1328
- srcIndex = int(round(math.sqrt(i**2 - dd),0)) #Calculate horizontal range (in pixels) using pathagorean theorem
1329
- pingDat[srcIndex] = intensity # Store intensity at appropriate horizontal range
1330
- dataExtent = srcIndex # Store range extent (max range) of ping
1331
- else:
1332
- pass
1333
- pingDat[dataExtent:]=0 # Zero out values past range extent so we don't interpolate past this
1341
+ data_extent = int(sidx_v[-1])
1334
1342
 
1335
- # Process of relocating bed pixels will introduce across track gaps
1336
- ## in the array so we will interpolate over gaps to fill them.
1337
- nans, x = np.isnan(pingDat), lambda z: z.nonzero()[0]
1338
- pingDat[nans] = np.interp(x(nans), x(~nans), pingDat[~nans])
1343
+ # Scatter intensities; duplicate src_idx writes keep last value
1344
+ pingDat = np.full(H, np.nan, dtype=np.float32)
1345
+ pingDat[sidx_v] = self.sonDat[rows_v, j].astype(np.float32)
1346
+ pingDat[data_extent:] = 0 # zero past range extent
1347
+
1348
+ # Interpolate across-track gaps introduced by SRC
1349
+ nans = np.isnan(pingDat)
1350
+ if nans.any():
1351
+ x = np.arange(H)
1352
+ pingDat[nans] = np.interp(x[nans], x[~nans], pingDat[~nans])
1339
1353
 
1340
1354
  # Store relocated ping in output array
1341
1355
  if son:
1342
- srcDat[:,j] = np.around(pingDat, 0)
1356
+ srcDat[:, j] = np.around(pingDat, 0)
1343
1357
  else:
1344
- srcDat[:,j] = pingDat
1345
-
1346
- del pingDat
1358
+ srcDat[:, j] = pingDat
1347
1359
 
1348
1360
  if son:
1349
- self.sonDat = srcDat.astype(int) # Store in class attribute for later use
1361
+ self.sonDat = srcDat.astype(int) # Store in class attribute for later use
1350
1362
  else:
1351
1363
  self.sonDat = srcDat
1352
1364
  del srcDat
@@ -934,75 +934,64 @@ class sonObj(object):
934
934
  # bedPick = round(sonMeta['dep_m'] / sonMeta['pix_m'], 0).astype(int)
935
935
  bedPick = round(sonMeta['dep_m'] / sonMeta['pixM'], 0).astype(int).reset_index(drop=True)
936
936
 
937
+ H, W = self.sonDat.shape[0], self.sonDat.shape[1]
938
+
937
939
  # Get max depth
938
940
  maxDep = max(bedPick)
939
941
  maxGap = int(round(np.tan(np.deg2rad(4)) * maxDep, 0)) # Max nadir gap (assume 4 degrees)
940
942
 
941
- # Initialize 2d array to store relocated sonar records
942
- srcDat = np.zeros((self.sonDat.shape[0]+maxGap, self.sonDat.shape[1])).astype(np.float32)#.astype(int)
943
+ H_out = H + maxGap
943
944
 
944
- print(srcDat.shape, maxGap, self.sonDat.shape)
945
+ # Initialize 2d array to store relocated sonar records
946
+ srcDat = np.zeros((H_out, W), dtype=np.float32)
945
947
 
946
- #Iterate each ping
947
- for j in range(self.sonDat.shape[1]):
948
- depth = bedPick[j] # Get depth (in pixels) at nadir
949
- dd = depth**2
948
+ #Iterate each ping (inner row-loop replaced with numpy vector ops)
949
+ for j in range(W):
950
+ depth = int(bedPick[j]) # Get depth (in pixels) at nadir
951
+ dd = float(depth * depth)
950
952
 
951
- # Calculate gap at nadir (assume 4 degrees)
953
+ # Calculate nadir gap (assume 4 degrees) for this ping
952
954
  nadirGap = int(round(np.tan(np.deg2rad(4)) * depth, 0))
953
955
 
954
- print(depth, nadirGap)
955
-
956
- # Create 1d array to store relocated bed pixels. Set to nan so we
957
- ## can later interpolate over gaps.
958
- pingDat = (np.ones((self.sonDat.shape[0]+maxGap)).astype(np.float32)) * np.nan
959
- dataExtent = 0
960
-
961
- # # Calculate nadir gap (assume 4 degrees)
962
- # nadirGap = int(round(np.tan(np.deg2rad(4)) * depth, 0))
956
+ # Rows at or beyond the water column boundary
957
+ row_arr = np.arange(depth, H)
958
+ # Horizontal range via Pythagorean theorem, plus nadir gap offset
959
+ src_idx = np.round(
960
+ np.sqrt(row_arr.astype(np.float64) ** 2 - dd)
961
+ ).astype(int) + nadirGap
963
962
 
964
- #Iterate each sonar/ping return
965
- for i in range(self.sonDat.shape[0]):
966
- if i >= depth:
967
- intensity = self.sonDat[i,j] # Get the intensity value
968
- srcIndex = int(round(math.sqrt((i)**2 - dd),0)) #Calculate horizontal range (in pixels) using pathagorean theorem
963
+ # Discard out-of-bounds indices
964
+ valid = src_idx < H_out
965
+ rows_v = row_arr[valid]
966
+ sidx_v = src_idx[valid]
969
967
 
970
- srcIndex = srcIndex + nadirGap # Add nadir gap to horizontal range
968
+ pingDat = np.full(H_out, np.nan, dtype=np.float32)
969
+ if len(sidx_v) > 0:
970
+ data_extent = int(sidx_v[-1])
971
+ pingDat[sidx_v] = self.sonDat[rows_v, j].astype(np.float32)
972
+ pingDat[data_extent:] = 0 # Zero out values past range extent
971
973
 
972
- pingDat[srcIndex] = intensity # Store intensity at appropriate horizontal range
973
- dataExtent = srcIndex # Store range extent (max range) of ping
974
- else:
975
- pass
976
- pingDat[dataExtent:]=0 # Zero out values past range extent so we don't interpolate past this
977
-
978
- # # Process of relocating bed pixels will introduce across track gaps
979
- # ## in the array so we will interpolate over gaps to fill them.
980
- # nans, x = np.isnan(pingDat), lambda z: z.nonzero()[0]
981
- # pingDat[nans] = np.interp(x(nans), x(~nans), pingDat[~nans])
974
+ pingDat[:nadirGap] = np.nan # Remove relocated water column
982
975
 
983
- pingDat[:nadirGap] = np.nan # Zero out top maxGap pixels to remove relocated water column
984
- print(pingDat)
985
-
986
- # Find where firs non-zero pixel is
987
- nonZero = np.where(pingDat>0)[0]
988
- print(pingDat[nonZero])
989
-
990
- # Interpolate over gaps past nonZero
976
+ # Interpolate over gaps starting from first non-zero pixel
977
+ nonZero = np.where(pingDat > 0)[0]
991
978
  if len(nonZero) > 0:
992
979
  firstNonZero = nonZero[0]
993
- nans, x = np.isnan(pingDat[firstNonZero:]), lambda z: z.nonzero()[0]
994
- pingDat[firstNonZero:][nans] = np.interp(x(nans), x(~nans), pingDat[firstNonZero:][~nans])
980
+ tail = pingDat[firstNonZero:]
981
+ nans = np.isnan(tail)
982
+ if nans.any():
983
+ x = np.arange(len(tail))
984
+ tail[nans] = np.interp(x[nans], x[~nans], tail[~nans])
985
+ pingDat[firstNonZero:] = tail
995
986
 
996
987
  # Store relocated ping in output array
997
988
  if son:
998
- srcDat[:,j] = np.around(pingDat, 0)
989
+ srcDat[:, j] = np.around(pingDat, 0)
999
990
  else:
1000
- srcDat[:,j] = pingDat
1001
-
1002
- del pingDat
991
+ srcDat[:, j] = pingDat
1003
992
 
1004
993
  if son:
1005
- self.sonDat = srcDat.astype(int) # Store in class attribute for later use
994
+ self.sonDat = srcDat.astype(int) # Store in class attribute for later use
1006
995
  else:
1007
996
  self.sonDat = srcDat
1008
997
  del srcDat
@@ -266,6 +266,7 @@ def doWork(
266
266
  'inFile': in_file,
267
267
  'sonFiles': son_files,
268
268
  'projDir': proj_dir,
269
+ 'return_context': True,
269
270
  })
270
271
 
271
272
  print('\n\n', '***User Parameters***')
@@ -281,7 +282,13 @@ def doWork(
281
282
  print('\n===========================================')
282
283
  print('===========================================')
283
284
  print('***** READING *****')
284
- ss_chan_avail = read_master_func(**run_params)
285
+ read_ctx = read_master_func(**run_params)
286
+ if isinstance(read_ctx, dict):
287
+ ss_chan_avail = bool(read_ctx.get('has_sidescan', False))
288
+ nav_available = bool(read_ctx.get('has_nav', True))
289
+ else:
290
+ ss_chan_avail = bool(read_ctx)
291
+ nav_available = True
285
292
 
286
293
  if ss_chan_avail:
287
294
  rect_wcp = run_params.get('rect_wcp', False)
@@ -293,6 +300,19 @@ def doWork(
293
300
  export_poly = run_params.get('export_poly', False)
294
301
  plt_subclass = run_params.get('pltSubClass', False)
295
302
 
303
+ if not nav_available:
304
+ if rect_wcp or rect_wcr or banklines or coverage or pred_sub or map_sub or export_poly or plt_subclass:
305
+ print('\nWARNING: Navigation info is unavailable for this recording.')
306
+ print('Skipping rectification and substrate mapping workflows (non-georeferenced sonogram-only processing).')
307
+ rect_wcp = False
308
+ rect_wcr = False
309
+ banklines = False
310
+ coverage = False
311
+ pred_sub = False
312
+ map_sub = False
313
+ export_poly = False
314
+ plt_subclass = False
315
+
296
316
  if rect_wcp or rect_wcr or banklines or coverage or pred_sub or map_sub or export_poly:
297
317
  print('\n===========================================')
298
318
  print('===========================================')
@@ -162,6 +162,12 @@ def quiet_tensorflow_warnings():
162
162
  tf.autograph.set_verbosity(0)
163
163
  except Exception:
164
164
  pass
165
+ try:
166
+ # Suppress Keras per-batch/per-step progress bar output
167
+ # (e.g. "1/1 [==============================] - 1s 1s/step")
168
+ tf.keras.utils.disable_interactive_logging()
169
+ except Exception:
170
+ pass
165
171
  except Exception:
166
172
  pass
167
173
 
@@ -42,6 +42,14 @@ from pingmapper.funcs_common import *
42
42
  quiet_tensorflow_warnings()
43
43
  import json
44
44
  import numpy as np
45
+
46
+ # Prevent the transformers/huggingface_hub libraries from making network calls
47
+ # to huggingface.co to check for model updates. The model weights are stored
48
+ # locally, so no internet access is needed. These flags can also be set as
49
+ # system environment variables (TRANSFORMERS_OFFLINE=1, HF_HUB_OFFLINE=1)
50
+ # before launching PINGMapper to achieve the same effect without a code change.
51
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
52
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
45
53
  # import tensorflow as tf
46
54
  # import tensorflow.keras.backend as K
47
55
  # from tensorflow.python.client import device_lib
@@ -114,6 +122,47 @@ Utilities provided courtesy Dr. Dan Buscombe from segmentation_gym
114
122
  https://github.com/Doodleverse/segmentation_gym
115
123
  '''
116
124
 
125
+ #=======================================================================
126
+ def _build_segformer_from_config(id2label, num_classes, num_channels=3):
127
+ '''
128
+ Construct a SegFormer model (nvidia/mit-b0 backbone architecture) entirely
129
+ from a hardcoded local config, without contacting HuggingFace Hub.
130
+
131
+ The config values below are the canonical mit-b0 architecture spec.
132
+ The backbone weights loaded here are random initialisation — they are
133
+ immediately overwritten by model.load_weights() with the Zenodo-hosted
134
+ fine-tuned weights, so no HF-sourced weights are ever used.
135
+ '''
136
+ from transformers import TFSegformerForSemanticSegmentation, SegformerConfig
137
+
138
+ label2id = {label: id for id, label in id2label.items()}
139
+
140
+ config = SegformerConfig(
141
+ num_channels=num_channels,
142
+ num_encoder_blocks=4,
143
+ depths=[2, 2, 2, 2],
144
+ sr_ratios=[8, 4, 2, 1],
145
+ hidden_sizes=[32, 64, 160, 256],
146
+ patch_sizes=[7, 3, 3, 3],
147
+ strides=[4, 2, 2, 2],
148
+ num_attention_heads=[1, 2, 5, 8],
149
+ mlp_ratios=[4, 4, 4, 4],
150
+ hidden_act='gelu',
151
+ hidden_dropout_prob=0.0,
152
+ attention_probs_dropout_prob=0.0,
153
+ classifier_dropout_prob=0.1,
154
+ initializer_range=0.02,
155
+ drop_path_rate=0.1,
156
+ layer_norm_eps=1e-06,
157
+ decoder_hidden_size=256,
158
+ semantic_loss_ignore_index=255,
159
+ num_labels=num_classes,
160
+ id2label=id2label,
161
+ label2id=label2id,
162
+ )
163
+
164
+ return TFSegformerForSemanticSegmentation(config)
165
+
117
166
  #=======================================================================
118
167
  def initModel(weights, configfile, USE_GPU=False):
119
168
  '''
@@ -192,10 +241,21 @@ def initModel(weights, configfile, USE_GPU=False):
192
241
  id2label = {}
193
242
  for k in range(NCLASSES):
194
243
  id2label[k]=str(k)
195
- model = segformer(id2label,num_classes=NCLASSES)
244
+ # SegFormer always uses 3 input channels: seg_file2tensor() converts
245
+ # single-channel images to 3-channel via np.dstack before inference,
246
+ # so the Zenodo weights are always saved with num_channels=3.
247
+ model = _build_segformer_from_config(id2label, NCLASSES, num_channels=3)
248
+ # Subclassed Keras models are lazy — variables don't exist until the
249
+ # first forward pass. Do a dummy call to build them so load_weights()
250
+ # can match the saved HDF5 file's variable names.
251
+ # Input to TFSegformer is NCHW: (batch, channels, height, width)
252
+ dummy = tf.zeros((1, 3, TARGET_SIZE[0], TARGET_SIZE[1]))
253
+ model(dummy, training=False)
196
254
 
197
255
  model.load_weights(weights)
198
- # model = compile_models([model[0]], MODEL)
256
+ # Compile once here so doPredict never needs to recompile between chunks.
257
+ # compile_models mutates the model in-place; we discard the returned list.
258
+ compile_models([model], MODEL)
199
259
 
200
260
  return model, MODEL, N_DATA_BANDS
201
261
 
@@ -209,7 +269,8 @@ def doPredict(model, MODEL, arr, N_DATA_BANDS, NCLASSES, TARGET_SIZE, OTSU_THRES
209
269
  '''
210
270
  '''
211
271
 
212
- model = compile_models([model[0]], MODEL)
272
+ # Model is compiled once in initModel; just normalise to a list here.
273
+ model = [model[0]]
213
274
 
214
275
  # Read array into a cropped and resized tensor
215
276
  image, w, h, bigimage = seg_file2tensor(arr, N_DATA_BANDS, TARGET_SIZE, MODEL)
@@ -107,7 +107,10 @@ def map_master_func(logfilename='',
107
107
  mosaic_nchunk=50,
108
108
  mosaic=False,
109
109
  map_mosaic=0,
110
- banklines=False):
110
+ banklines=False,
111
+ export_16bit=False,
112
+ export_16bit_colormap=False,
113
+ export_colormap_uint8=True):
111
114
 
112
115
  '''
113
116
  Main script to map substrates from side scan sonar imagery.
@@ -182,6 +185,15 @@ def map_master_func(logfilename='',
182
185
  pass # Don't add non-port/star objects since they can't be rectified
183
186
  del son, beam, sonObjs
184
187
 
188
+ if len(mapObjs) == 0:
189
+ print("\nNo side-scan channels available for substrate mapping. Skipping mapping.")
190
+ return
191
+
192
+ nav_available = all(getattr(son, 'trans', None) is not None for son in mapObjs)
193
+ if not nav_available:
194
+ print("\nNavigation info unavailable for side-scan channels. Skipping substrate mapping.")
195
+ return
196
+
185
197
  ################################################
186
198
  # Prepare output directory and update attributes
187
199
  for son in mapObjs:
@@ -399,6 +411,9 @@ def map_master_func(logfilename='',
399
411
  # Create portstarObj
400
412
  psObj = portstarObj(mapObjs)
401
413
 
414
+ # Pre-load CSVs once so each joblib-serialised copy already has the data
415
+ psObj._preloadRectifyCache()
416
+
402
417
  Parallel(n_jobs=safe_n_jobs(len(toMap), threadCnt))(delayed(psObj._mapSubstrate)(map_class_method, c, f) for c, f in tqdm(toMap.items()))
403
418
 
404
419
  del toMap
@@ -157,7 +157,8 @@ def read_master_func(logfilename='',
157
157
  mosaic_nchunk=50,
158
158
  mosaic=False,
159
159
  map_mosaic=0,
160
- banklines=False):
160
+ banklines=False,
161
+ return_context=False):
161
162
 
162
163
  '''
163
164
  Main script to read data from Humminbird sonar recordings. Scripts have been
@@ -303,28 +304,7 @@ def read_master_func(logfilename='',
303
304
  modelDir = get_segmentation_model_dir()
304
305
  if not os.path.exists(modelDir):
305
306
  downloadSegmentationModelsv1_0(modelDir)
306
- getSegformer = True
307
- else:
308
- getSegformer = False
309
-
310
- ###############
311
- # Get segformer
312
- if getSegformer:
313
- NCLASSES = 8
314
- id2label = {}
315
- for k in range(NCLASSES):
316
- id2label[k] = str(k)
317
-
318
- # try downloading segformer pretrained model
319
- try:
320
- _ = segformer(id2label, NCLASSES)
321
- except:
322
- print('\n\n\n\n')
323
- print('ERROR! Unable to download pretrained SegFormer model!')
324
- print('Your network settings are blocking the download.')
325
- print('Please try running on a different network.')
326
- print('Once the script has been run successfully, you should be able')
327
- print('to run PING-Mapper on the current network.')
307
+
328
308
 
329
309
 
330
310
  ###############################################
@@ -393,6 +373,27 @@ def read_master_func(logfilename='',
393
373
  print('\n\nERROR!\n\nFile type {} not supported at this time.'.format(file_type))
394
374
  sys.exit()
395
375
 
376
+ nav_available = bool(getattr(sonar_obj, 'has_position', True))
377
+
378
+ # Sonar-only fallback for sources without navigation fields (e.g., some Cerulean logs).
379
+ # Disable filters that rely on geospatial motion/position so processing can continue.
380
+ if getattr(sonar_obj, 'has_position', True) is False:
381
+ nav_filters_requested = (
382
+ (max_heading_deviation > 0) or
383
+ (min_speed > 0) or
384
+ (max_speed > 0) or
385
+ bool(aoi)
386
+ )
387
+
388
+ if nav_filters_requested:
389
+ print('\nWARNING: Navigation fields are unavailable for this recording (sonar-only mode).')
390
+ print('Disabling nav-dependent filters: max_heading_deviation, min_speed, max_speed, aoi.')
391
+
392
+ max_heading_deviation = 0
393
+ min_speed = 0
394
+ max_speed = 0
395
+ aoi = False
396
+
396
397
  ####################
397
398
  # Create son objects
398
399
  ####################
@@ -1102,29 +1103,41 @@ def read_master_func(logfilename='',
1102
1103
  if _is_sidescan_beam(beam):
1103
1104
  portstar.append(son)
1104
1105
 
1105
- # Create portstarObj
1106
- psObj = portstarObj(portstar)
1107
-
1108
- chunks = []
1109
- for son in portstar:
1110
- # Get chunk id's, ignoring those with nodata
1111
- c = son._getChunkID()
1112
-
1113
- chunks.extend(c)
1114
- del c
1115
- del son
1116
-
1117
- chunks = np.unique(chunks).astype(int)
1106
+ psObj = None
1107
+ chunks = np.array([], dtype=int)
1118
1108
 
1119
- if len(chunks) == 0:
1109
+ if len(portstar) == 0:
1120
1110
  print(
1121
- '\n\nNo valid side-scan chunks available for depth processing. '\
1111
+ '\n\nNo recognized side-scan channels available for depth processing. '\
1122
1112
  'Continuing with down-looking beams only.'
1123
1113
  )
1124
1114
  print('Disabling side-scan-only operations (auto depth, bedpick plot, shadow removal).')
1125
1115
  detectDep = 0
1126
1116
  pltBedPick = False
1127
1117
  remShadow = 0
1118
+ else:
1119
+ # Create portstarObj
1120
+ psObj = portstarObj(portstar)
1121
+
1122
+ chunks = []
1123
+ for son in portstar:
1124
+ # Get chunk id's, ignoring those with nodata
1125
+ c = son._getChunkID()
1126
+
1127
+ chunks.extend(c)
1128
+ del c
1129
+
1130
+ chunks = np.unique(chunks).astype(int)
1131
+
1132
+ if len(chunks) == 0:
1133
+ print(
1134
+ '\n\nNo valid side-scan chunks available for depth processing. '\
1135
+ 'Continuing with down-looking beams only.'
1136
+ )
1137
+ print('Disabling side-scan-only operations (auto depth, bedpick plot, shadow removal).')
1138
+ detectDep = 0
1139
+ pltBedPick = False
1140
+ remShadow = 0
1128
1141
 
1129
1142
  # # Automatically estimate depth
1130
1143
  if detectDep > 0:
@@ -1187,7 +1200,7 @@ def read_master_func(logfilename='',
1187
1200
 
1188
1201
  if saveDepth:
1189
1202
 
1190
- if ss_chan_avail:
1203
+ if ss_chan_avail and psObj is not None:
1191
1204
  # Save detected depth to csv
1192
1205
  depDF = psObj._saveDepth(chunks, detectDep, smthDep, adjDep, instDepAvail)
1193
1206
  else:
@@ -1246,14 +1259,15 @@ def read_master_func(logfilename='',
1246
1259
  del depDF
1247
1260
 
1248
1261
  # Cleanup
1249
- psObj._cleanup()
1262
+ if psObj is not None:
1263
+ psObj._cleanup()
1250
1264
 
1251
1265
  print("\nDone!")
1252
1266
  print("Time (s):", round(time.time() - start_time, ndigits=1))
1253
1267
  printUsage()
1254
1268
 
1255
1269
  # Plot sonar depth and auto depth estimate (if available) on sonogram
1256
- if pltBedPick:
1270
+ if pltBedPick and psObj is not None and len(chunks) > 0:
1257
1271
  start_time = time.time()
1258
1272
 
1259
1273
  print("\n\nExporting bedpick plots to {}...".format(tileFile))
@@ -1264,7 +1278,8 @@ def read_master_func(logfilename='',
1264
1278
  printUsage()
1265
1279
 
1266
1280
  # Cleanup
1267
- psObj._cleanup()
1281
+ if psObj is not None:
1282
+ psObj._cleanup()
1268
1283
  del psObj, portstar
1269
1284
 
1270
1285
  for son in sonObjs:
@@ -1638,8 +1653,13 @@ def read_master_func(logfilename='',
1638
1653
  gc.collect()
1639
1654
  printUsage()
1640
1655
 
1641
- if len(ss_chan_avail) == 0:
1642
- return False
1643
- else:
1644
- return True
1656
+ has_sidescan = len(ss_chan_avail) > 0
1657
+
1658
+ if return_context:
1659
+ return {
1660
+ 'has_sidescan': has_sidescan,
1661
+ 'has_nav': nav_available,
1662
+ }
1663
+
1664
+ return has_sidescan
1645
1665
 
@@ -307,6 +307,15 @@ def rectify_master_func(logfilename='',
307
307
  pass # Don't add non-port/star objects since they can't be rectified
308
308
  del son, beam, rectObjs
309
309
 
310
+ if len(portstar) == 0:
311
+ print("\nNo side-scan channels available for rectification. Skipping rectification.")
312
+ return
313
+
314
+ nav_available = all(getattr(son, 'trans', None) is not None for son in portstar)
315
+ if not nav_available:
316
+ print("\nNavigation info unavailable for side-scan channels. Skipping rectification.")
317
+ return
318
+
310
319
  ############################################################################
311
320
  # Smooth Trackline #
312
321
  ############################################################################
@@ -0,0 +1 @@
1
+ __version__ = '5.3.2'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.3.0
3
+ Version: 5.3.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>
@@ -1 +0,0 @@
1
- __version__ = '5.3.0'
File without changes
File without changes
File without changes
File without changes