pingmapper 5.3.1__tar.gz → 5.3.3__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.1 → pingmapper-5.3.3}/PKG-INFO +1 -1
  2. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/class_mapSubstrateObj.py +14 -2
  3. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/class_portstarObj.py +100 -70
  4. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/class_sonObj.py +40 -28
  5. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/class_sonObj_nadirgaptest.py +37 -48
  6. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/doWork.py +21 -1
  7. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/funcs_common.py +6 -0
  8. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/funcs_model.py +5 -2
  9. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/main_mapSubstrate.py +16 -1
  10. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/main_readFiles.py +65 -24
  11. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/main_rectify.py +9 -0
  12. pingmapper-5.3.3/pingmapper/version.py +1 -0
  13. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper.egg-info/PKG-INFO +1 -1
  14. pingmapper-5.3.1/pingmapper/version.py +0 -1
  15. {pingmapper-5.3.1 → pingmapper-5.3.3}/LICENSE +0 -0
  16. {pingmapper-5.3.1 → pingmapper-5.3.3}/README.md +0 -0
  17. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/__init__.py +0 -0
  18. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/__main__.py +0 -0
  19. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/class_rectObj.py +0 -0
  20. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/default_params.json +0 -0
  21. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/funcs_rectify.py +0 -0
  22. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/gui_main.py +0 -0
  23. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0926.py +0 -0
  24. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/processing_scripts/main_batchDirectory_2024-01-18_0929.py +0 -0
  25. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/scratch/funcs_pyhum_correct.py +0 -0
  26. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/scratch/main.py +0 -0
  27. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/scratch/main_batchDirectory.py +0 -0
  28. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/test_PINGMapper.py +0 -0
  29. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/test_time.py +0 -0
  30. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/DRAFT_Workflows/avg_predictions_Mussel_WBL.py +0 -0
  31. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/DRAFT_Workflows/gen_centerline.py +0 -0
  32. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/DRAFT_Workflows/gen_centerline_from_bankline.py +0 -0
  33. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/DRAFT_Workflows/gen_centerline_trkpnts_fitspline_DRAFT.py +0 -0
  34. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/DRAFT_Workflows/testEXAMPLE_mosaic_logit.py +0 -0
  35. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/RawEGN_avg_predictions.py +0 -0
  36. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/00_substrate_logits_mosaic_transects.py +0 -0
  37. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/00_substrate_shps_mosaic_transects.py +0 -0
  38. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/01_gen_centerline_from_coverage.py +0 -0
  39. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/02_gen_summary_stamp_shps.py +0 -0
  40. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/03_gen_summary_shp.py +0 -0
  41. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/04_combine_summary_shp_csv.py +0 -0
  42. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/05_gen_summary_shp_plots.py +0 -0
  43. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/06_compare_raw-egn_volume.py +0 -0
  44. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/08_raw-egn_hardReacheFreq_hist.py +0 -0
  45. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/09_raw-egn_PatchSize_density.py +0 -0
  46. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/Substrate_Summaries/summarize_project_substrate.py +0 -0
  47. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/export_coverage.py +0 -0
  48. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper/utils/main_mosaic_transects.py +0 -0
  49. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper.egg-info/SOURCES.txt +0 -0
  50. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper.egg-info/dependency_links.txt +0 -0
  51. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper.egg-info/requires.txt +0 -0
  52. {pingmapper-5.3.1 → pingmapper-5.3.3}/pingmapper.egg-info/top_level.txt +0 -0
  53. {pingmapper-5.3.1 → pingmapper-5.3.3}/pyproject.toml +0 -0
  54. {pingmapper-5.3.1 → pingmapper-5.3.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.3.1
3
+ Version: 5.3.3
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
@@ -1952,9 +1952,13 @@ class portstarObj(object):
1952
1952
 
1953
1953
  #=======================================================================
1954
1954
  def _detectShadow(self, remShadow, i, USE_GPU, doPlot=True, tileFile='.jpg'):
1955
+
1955
1956
  '''
1956
1957
 
1957
1958
  '''
1959
+ # Suppress Keras/TensorFlow verbose output (progress bar)
1960
+ quiet_tensorflow_warnings()
1961
+
1958
1962
  # Open model configuration file
1959
1963
  with open(self.configfile) as f:
1960
1964
  config = json.load(f)
@@ -2052,7 +2056,7 @@ class portstarObj(object):
2052
2056
  port_pix = self._getShadowPix(port_label, remShadow)
2053
2057
  star_pix = self._getShadowPix(star_label, remShadow)
2054
2058
 
2055
- del self.shadowModel, model
2059
+ del model
2056
2060
  gc.collect()
2057
2061
  return i, port_pix, star_pix
2058
2062
 
@@ -2101,48 +2105,24 @@ class portstarObj(object):
2101
2105
  del son.sonDat
2102
2106
 
2103
2107
  # Get rid of zeros along water column area
2108
+ # Vectorized: replace class-8 (water column) labels in non-WC area with 0
2109
+ dep_arr = np.asarray(son.bedPick, dtype=int) # (W,)
2110
+ row_idx = np.arange(label.shape[0])[:, np.newaxis] # (H, 1)
2111
+ below_wc = row_idx >= dep_arr[np.newaxis, :] # (H, W)
2112
+ label = np.where(below_wc & (label == 8), 0, label)
2113
+
2114
+ # Zero-fill per ping: propagate the class at the edge of any zero region
2115
+ # into that region. Zeros should form at most one contiguous block per ping
2116
+ # (the comment in the original confirms this). Skip pings with no zeros.
2104
2117
  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
2118
+ d = int(son.bedPick[p])
2119
+ ping_below = label[d:, p]
2120
+ zero_idx = np.where(ping_below == 0)[0]
2121
+ if len(zero_idx) == 0:
2122
+ continue
2123
+ f, l = int(zero_idx[0]), int(zero_idx[-1])
2124
+ if d + l + 1 < label.shape[0]:
2125
+ label[d + f : d + l + 1, p] = ping_below[l + 1]
2146
2126
 
2147
2127
  son.sonDat = label
2148
2128
 
@@ -2299,30 +2279,58 @@ class portstarObj(object):
2299
2279
  # # Do shadow and water column removal
2300
2280
 
2301
2281
  # 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]
2282
+ predStack = son.sonDat # (H, W, C)
2283
+ H_pred, W_pred, C_pred = predStack.shape
2308
2284
 
2309
- # Remove shadows
2310
- # Get mask
2311
- son._SHW_mask(chunk, son=False)
2312
-
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
2285
+ # Compute shadow mask ONCE — geometry depends only on son.shadow[chunk],
2286
+ # not on which class band we are processing.
2287
+ son.sonDat = predStack[:, :, 0]
2288
+ son._SHW_mask(chunk, son=False)
2289
+ shw_mask = son.shadowMask # (H, W)
2290
+ del son.shadowMask
2323
2291
 
2324
- # Store in son.sonDat
2325
- son.sonDat = predStack
2292
+ # Apply shadow mask to all classes in a single vectorised broadcast
2293
+ predStack = predStack * shw_mask[:, :, np.newaxis]
2294
+
2295
+ # Apply slant range correction (SRC) to all classes in one pass.
2296
+ # Original _WCR_SRC ran an O(H) Python inner loop for every (class, ping)
2297
+ # pair. Here we vectorise the H dimension with numpy and process all C
2298
+ # classes simultaneously, reducing Python iterations from C×W×H → W×C.
2299
+ bedPick = round(sonMeta['dep_m'] / sonMeta['pixM'], 0).astype(int).reset_index(drop=True)
2300
+ srcStack = np.zeros((H_pred, W_pred, C_pred), dtype=np.float32)
2301
+ for j in range(W_pred):
2302
+ depth = int(bedPick[j])
2303
+ if depth >= H_pred:
2304
+ continue
2305
+ dd = float(depth * depth)
2306
+ row_arr = np.arange(depth, H_pred)
2307
+ src_idx = np.round(
2308
+ np.sqrt(row_arr.astype(np.float64) ** 2 - dd)
2309
+ ).astype(int)
2310
+ valid = src_idx < H_pred
2311
+ rows_v = row_arr[valid]
2312
+ sidx_v = src_idx[valid]
2313
+ if len(sidx_v) == 0:
2314
+ continue
2315
+ data_extent = int(sidx_v[-1])
2316
+
2317
+ # Scatter all C classes at once, then zero past range extent
2318
+ pingStack = np.full((H_pred, C_pred), np.nan, dtype=np.float32)
2319
+ pingStack[sidx_v, :] = predStack[rows_v, j, :].astype(np.float32)
2320
+ pingStack[data_extent:, :] = 0
2321
+
2322
+ # Interpolate gaps — NaN pattern is geometry-driven, same for all classes
2323
+ nans = np.isnan(pingStack[:, 0])
2324
+ if nans.any():
2325
+ x = np.arange(H_pred)
2326
+ xnans, xvalid = x[nans], x[~nans]
2327
+ for c in range(C_pred):
2328
+ pingStack[nans, c] = np.interp(
2329
+ xnans, xvalid, pingStack[~nans, c]
2330
+ )
2331
+ srcStack[:, j, :] = pingStack
2332
+
2333
+ son.sonDat = srcStack
2326
2334
 
2327
2335
  # Iterate each class again and:
2328
2336
  for c in range(classes):
@@ -2411,6 +2419,20 @@ class portstarObj(object):
2411
2419
 
2412
2420
 
2413
2421
 
2422
+ #=======================================================================
2423
+ def _preloadRectifyCache(self):
2424
+ '''
2425
+ Pre-load the trackline and sonar-metadata CSVs that _rectify reads
2426
+ on every chunk call. Call this ONCE on the portstarObj in the main
2427
+ process before Parallel dispatch so that each serialised copy of the
2428
+ object already has the data, avoiding per-chunk disk reads.
2429
+ '''
2430
+ portTrkMetaFile = os.path.join(self.port.metaDir, "Trackline_Smth_"+self.port.beamName+".csv")
2431
+ starTrkMetaFile = os.path.join(self.star.metaDir, "Trackline_Smth_"+self.star.beamName+".csv")
2432
+ self.port._trkMetaDF = pd.read_csv(portTrkMetaFile)
2433
+ self.star._trkMetaDF = pd.read_csv(starTrkMetaFile)
2434
+ self.port._loadSonMeta() # populates self.port.sonMetaDF
2435
+
2414
2436
  #=======================================================================
2415
2437
  def _rectify(self, dat, chunk, imgOutPrefix, filt=50, wgs=False, return_rect=False):
2416
2438
  '''
@@ -2473,8 +2495,11 @@ class portstarObj(object):
2473
2495
 
2474
2496
  ###
2475
2497
  # 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
2498
+ # Cache the full CSV on self.port so we only read from disk once per portstarObj
2499
+ if not hasattr(self.port, '_trkMetaDF') or self.port._trkMetaDF is None:
2500
+ self.port._trkMetaDF = pd.read_csv(portTrkMetaFile)
2501
+ portTrkFull = self.port._trkMetaDF
2502
+ trkMeta = portTrkFull[portTrkFull['chunk_id']==chunk].reset_index(drop=False)
2478
2503
 
2479
2504
  # Get range (outer extent) coordinates [xR, yR] to transposed numpy arrays
2480
2505
  xTop, yTop = trkMeta[xRange].to_numpy().T, trkMeta[yRange].to_numpy().T
@@ -2482,8 +2507,11 @@ class portstarObj(object):
2482
2507
 
2483
2508
  ###
2484
2509
  # 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
2510
+ # Cache the full CSV on self.star so we only read from disk once per portstarObj
2511
+ if not hasattr(self.star, '_trkMetaDF') or self.star._trkMetaDF is None:
2512
+ self.star._trkMetaDF = pd.read_csv(starTrkMetaFile)
2513
+ starTrkFull = self.star._trkMetaDF
2514
+ trkMeta = starTrkFull[starTrkFull['chunk_id']==chunk].reset_index(drop=False)
2487
2515
 
2488
2516
  # Get range (outer extent) coordinates [xR, yR] to transposed numpy arrays
2489
2517
  xBot, yBot = trkMeta[xRange].to_numpy().T, trkMeta[yRange].to_numpy().T
@@ -2507,7 +2535,9 @@ class portstarObj(object):
2507
2535
 
2508
2536
  # Get pixel size
2509
2537
  # pix_m = self.port.pixM
2510
- self.port._loadSonMeta()
2538
+ # Cache sonMetaDF — _loadSonMeta reads the same CSV every call
2539
+ if not hasattr(self.port, 'sonMetaDF') or self.port.sonMetaDF is None:
2540
+ self.port._loadSonMeta()
2511
2541
  isChunk = self.port.sonMetaDF['chunk_id']==chunk
2512
2542
  sonMeta = self.port.sonMetaDF[isChunk].reset_index()
2513
2543
 
@@ -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
 
@@ -253,7 +253,9 @@ def initModel(weights, configfile, USE_GPU=False):
253
253
  model(dummy, training=False)
254
254
 
255
255
  model.load_weights(weights)
256
- # 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)
257
259
 
258
260
  return model, MODEL, N_DATA_BANDS
259
261
 
@@ -267,7 +269,8 @@ def doPredict(model, MODEL, arr, N_DATA_BANDS, NCLASSES, TARGET_SIZE, OTSU_THRES
267
269
  '''
268
270
  '''
269
271
 
270
- model = compile_models([model[0]], MODEL)
272
+ # Model is compiled once in initModel; just normalise to a list here.
273
+ model = [model[0]]
271
274
 
272
275
  # Read array into a cropped and resized tensor
273
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
@@ -372,6 +373,27 @@ def read_master_func(logfilename='',
372
373
  print('\n\nERROR!\n\nFile type {} not supported at this time.'.format(file_type))
373
374
  sys.exit()
374
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
+
375
397
  ####################
376
398
  # Create son objects
377
399
  ####################
@@ -1081,29 +1103,41 @@ def read_master_func(logfilename='',
1081
1103
  if _is_sidescan_beam(beam):
1082
1104
  portstar.append(son)
1083
1105
 
1084
- # Create portstarObj
1085
- psObj = portstarObj(portstar)
1086
-
1087
- chunks = []
1088
- for son in portstar:
1089
- # Get chunk id's, ignoring those with nodata
1090
- c = son._getChunkID()
1091
-
1092
- chunks.extend(c)
1093
- del c
1094
- del son
1106
+ psObj = None
1107
+ chunks = np.array([], dtype=int)
1095
1108
 
1096
- chunks = np.unique(chunks).astype(int)
1097
-
1098
- if len(chunks) == 0:
1109
+ if len(portstar) == 0:
1099
1110
  print(
1100
- '\n\nNo valid side-scan chunks available for depth processing. '\
1111
+ '\n\nNo recognized side-scan channels available for depth processing. '\
1101
1112
  'Continuing with down-looking beams only.'
1102
1113
  )
1103
1114
  print('Disabling side-scan-only operations (auto depth, bedpick plot, shadow removal).')
1104
1115
  detectDep = 0
1105
1116
  pltBedPick = False
1106
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
1107
1141
 
1108
1142
  # # Automatically estimate depth
1109
1143
  if detectDep > 0:
@@ -1166,7 +1200,7 @@ def read_master_func(logfilename='',
1166
1200
 
1167
1201
  if saveDepth:
1168
1202
 
1169
- if ss_chan_avail:
1203
+ if ss_chan_avail and psObj is not None:
1170
1204
  # Save detected depth to csv
1171
1205
  depDF = psObj._saveDepth(chunks, detectDep, smthDep, adjDep, instDepAvail)
1172
1206
  else:
@@ -1225,14 +1259,15 @@ def read_master_func(logfilename='',
1225
1259
  del depDF
1226
1260
 
1227
1261
  # Cleanup
1228
- psObj._cleanup()
1262
+ if psObj is not None:
1263
+ psObj._cleanup()
1229
1264
 
1230
1265
  print("\nDone!")
1231
1266
  print("Time (s):", round(time.time() - start_time, ndigits=1))
1232
1267
  printUsage()
1233
1268
 
1234
1269
  # Plot sonar depth and auto depth estimate (if available) on sonogram
1235
- if pltBedPick:
1270
+ if pltBedPick and psObj is not None and len(chunks) > 0:
1236
1271
  start_time = time.time()
1237
1272
 
1238
1273
  print("\n\nExporting bedpick plots to {}...".format(tileFile))
@@ -1243,7 +1278,8 @@ def read_master_func(logfilename='',
1243
1278
  printUsage()
1244
1279
 
1245
1280
  # Cleanup
1246
- psObj._cleanup()
1281
+ if psObj is not None:
1282
+ psObj._cleanup()
1247
1283
  del psObj, portstar
1248
1284
 
1249
1285
  for son in sonObjs:
@@ -1617,8 +1653,13 @@ def read_master_func(logfilename='',
1617
1653
  gc.collect()
1618
1654
  printUsage()
1619
1655
 
1620
- if len(ss_chan_avail) == 0:
1621
- return False
1622
- else:
1623
- 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
1624
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.3'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingmapper
3
- Version: 5.3.1
3
+ Version: 5.3.3
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.1'
File without changes
File without changes
File without changes
File without changes