sidescanpy.processing.bottom#
Nadir and first-bottom-return detection, and the masks built on them.
Along each ping, a sidescan record runs through three zones: a leading nadir-noise burst (transmit ring-down; some hardware zeros it before writing the file, some doesn’t), a quiet span the beam never illuminates, and the real bottom return. The functions here find those boundaries per (ping, channel) and blank or mask the data before them.
detect_nadir_noise_extent()/blank_nadir_noise()— find and zero the leading noise burst (a processing step).detect_first_bottom_return()— amplitude-based onset of the real bottom return, optionally floored at the altitude sample.smooth_boundary_across_pings()— rolling median of a per-ping boundary.uniform_exclusion_boundary()— collapse a per-ping boundary to one conservative value for aggregate binning.mask_nadir_gap()— NaN everything before the first bottom return (display/export copy).mask_before_sample(),altitude_sample_index()— helpers.altitude_from_bottom_track()— fill missingaltitude_mfrom the first bottom return (the"bottom_track"correction step).
All detectors are vectorized over (ping, channel, sample) arrays and
return (ping, channel) float sample indices (NaN = not found).
Contents
Per-(ping, channel) sample index where the real bottom return starts. |
|
NaN-aware rolling median of a |
|
Per-(ping, channel) sample index where the leading nadir-noise burst ends. |
|
Return a copy of |
|
Copy of |
|
First finite sample of |
|
Collapse a per-(ping, channel) boundary to one conservative value. |
|
NaN out first-return boundaries far beyond their channel's usual one. |
|
Return a copy of |
|
Accepted |
|
Set |
- sidescanpy.processing.bottom.detect_first_bottom_return(value_arr, *, smooth_window=10, threshold_fraction=0.2, min_run=10, min_sample=None, min_sample_buffer=0.0, peak_percentile=100.0, min_run_fraction=1.0)#
Per-(ping, channel) sample index where the real bottom return starts.
Smooths each ping with a
smooth_window-sample rolling mean, then finds the first point where the smoothed value stays at or abovethreshold_fractionof that ping’s own peak formin_runconsecutive samples. The threshold is relative per ping and channel, since amplitude scale varies by channel and survey.This is not the altitude crossing (
slant_range_m > altitude_m): a real beam’s near edge is tilted away from vertical, so the first bottom return lands further out than altitude alone predicts.- Parameters:
- value_arrnp.ndarray
(ping, channel, sample)linear amplitude (not dB).- smooth_windowint, default 10
- threshold_fractionfloat, default 0.2
Fraction of the ping’s peak smoothed value.
- min_runint, default 10
Samples the rise must be sustained for.
- min_samplearray-like or float, optional
Per-(ping, channel) or scalar floor. Samples before it are ignored for both the peak and the onset search. Normally the altitude sample: the bottom can’t return before it. NaN entries mean “no floor” for that row.
- min_sample_bufferfloat, default 0.0
Added to
min_samplebefore use, to step past residual near-nadir noise.- peak_percentilefloat, default 100.0
Percentile of the smoothed values used as the peak reference (100 = max). Lower it to stop one bright residual sample from setting the threshold.
- min_run_fractionfloat, default 1.0
Fraction of the
min_runsamples that must be at or above the threshold (the onset sample itself always must be). Below 1 a long run tolerates gaps, such as shadows in a rough seabed return.
- Returns:
- np.ndarray
(ping, channel)float sample index, NaN where no sustained rise was found. Accurate to aboutsmooth_windowsamples.
- sidescanpy.processing.bottom.smooth_boundary_across_pings(boundary, *, window=15)#
NaN-aware rolling median of a
(ping, channel)boundary along pings.Per-ping detection is independent, so occasional pings jump far from their neighbours. A median (not mean) removes those without smearing real multi-ping trends.
- Parameters:
- boundarynp.ndarray
(ping, channel).- windowint, default 15
Window in pings, centred; an even value is bumped up by one.
- Returns:
- np.ndarray
Same shape. A ping whose whole window is NaN stays NaN.
- sidescanpy.processing.bottom.detect_nadir_noise_extent(value_arr, *, search_window=100, smooth_window=5, threshold_fraction=0.2, min_run=10, margin_samples=0, min_extent_samples=0.0)#
Per-(ping, channel) sample index where the leading nadir-noise burst ends.
Mirror image of
detect_first_bottom_return(): finds where an initially strong signal falls to a sustained low level. Some hardware (e.g. EdgeTech JSF) zeros this burst before writing the file; others (e.g. Klein SDF) leave it in, where it can fool the first-bottom-return detector.- Parameters:
- value_arrnp.ndarray
(ping, channel, sample)linear amplitude.- search_windowint, default 100
Only the first
search_windowsamples set the burst’s peak reference, so a bright bottom return further out can’t inflate the threshold.- smooth_window, threshold_fraction, min_run
As in
detect_first_bottom_return(), applied to a fall below the threshold.- margin_samplesint, default 0
Added to each detected extent (NaN rows stay NaN).
- min_extent_samplesfloat, default 0.0
Hard floor on the result, applied last; unlike
margin_samplesit also replaces NaN rows.
- Returns:
- np.ndarray
(ping, channel)float sample index. 0 where the leading samples are already quiet. NaN for pings too short to evaluate or with no sustained drop (unlessmin_extent_samplesis set).
- sidescanpy.processing.bottom.blank_nadir_noise(ds, *, value_vars=('backscatter_intensity',), detector_value_var='backscatter_intensity', search_window=100, smooth_window=5, threshold_fraction=0.2, min_run=10, margin_samples=0, min_extent_samples=0.0)#
Return a copy of
dswith each ping’s leading nadir-noise burst set to 0.Uses
detect_nadir_noise_extent()ondetector_value_varand zeros the same span in everyvalue_varsentry present inds(others are skipped). A no-op where the burst is already zero. Contrastmask_nadir_gap(), which NaNs the much larger span out to the first bottom return for display.The remaining parameters are passed to
detect_nadir_noise_extent().
- sidescanpy.processing.bottom.mask_before_sample(values, boundary, *, fill_value=nan)#
Copy of
values(..., sample)with samples beforeboundaryset tofill_value.boundarymatches the leading dims ofvalues. A NaN boundary entry masks nothing for that row. Returns float64.
- sidescanpy.processing.bottom.altitude_sample_index(ds, range_var='ground_range_m')#
First finite sample of
range_varper (ping, channel), as float.ground_range_mgoes finite where slant range first exceeds altitude, so this is altitude expressed in sample units. NaN where a row has no finite value.
- sidescanpy.processing.bottom.uniform_exclusion_boundary(per_ping_boundary, percentile=90.0, *, warn_factor=3.0)#
Collapse a per-(ping, channel) boundary to one conservative value.
For aggregate range binning, a per-ping boundary biases the near bins: only the pings whose boundary is earliest (usually the shallowest, brightest ones) contribute there. A single cutoff keeps every bin’s contributing pings the same. A tenth of pings with a far detection moves a 90th-percentile cutoff for all of them, so a WARNING is logged when the value is more than
warn_factortimes the median boundary.- Parameters:
- per_ping_boundarynp.ndarray
(ping, channel), e.g. a first-bottom-return array.- percentilefloat, default 90.0
Percentile across all finite entries.
- warn_factorfloat, default 3.0
- Returns:
- np.ndarray
Same shape, filled with that one value; all-NaN if the input has no finite entries.
- sidescanpy.processing.bottom.reject_late_boundaries(boundary, altitude_sample, *, factor=3.0)#
NaN out first-return boundaries far beyond their channel’s usual one.
With altitude, a channel’s usual boundary is the median ratio of boundary to altitude sample, times each ping’s altitude sample; without it, the channel’s median boundary. A boundary more than
factortimes the usual one is rejected. Only late boundaries are rejected: the failure this guards against is a bright far-range return setting the detection threshold, which pushes the boundary out, not in.- Parameters:
- boundarynp.ndarray
(ping, channel)sample index, NaN where nothing was detected.- altitude_samplenp.ndarray or None
(ping, channel)altitude in samples (altitude_sample_index()), orNone.- factorfloat, default 3.0
- Returns:
- boundarynp.ndarray
Copy with rejected entries set to NaN.
- usualnp.ndarray
(ping, channel)usual boundary, NaN where it can’t be formed (a channel with no detection, or a ping with no altitude).
- sidescanpy.processing.bottom.mask_nadir_gap(ds, *, value_vars=('backscatter_intensity',), detector_value_var='backscatter_intensity', range_var='ground_range_m', smooth_window=10, threshold_fraction=0.2, min_run=10, min_sample_buffer=0.0, peak_percentile=100.0, min_first_bottom_return_sample=None, boundary_smoothing_window=15, max_boundary_factor=3.0)#
Return a copy of
dswith the whole nadir gap set to NaN.Everything before
detect_first_bottom_return()’s per-ping boundary (noise burst plus un-illuminated span) is masked in eachvalue_varsentry present. For display and export copies; uses the exact per-ping boundary.- Parameters:
- value_varstuple of str, default (“backscatter_intensity”,)
Variables to mask; absent ones are skipped.
- detector_value_varstr, default “backscatter_intensity”
Variable the boundary is detected on (
backscatter_intensityis read uncorrected, so masking doesn’t depend on TVG state).- range_varstr, default “ground_range_m”
If present, its altitude sample floors the detection (the bottom can’t return before altitude).
- smooth_window, threshold_fraction, min_run, min_sample_buffer, peak_percentile
Passed to
detect_first_bottom_return().- min_first_bottom_return_samplefloat, optional
Extra scalar floor, combined with the altitude floor by element-wise max.
- boundary_smoothing_windowint, default 15
smooth_boundary_across_pings()window;<= 1disables.- max_boundary_factorfloat or None, default 3.0
Reject a ping’s boundary when it lies more than this factor beyond its channel’s usual one (see
reject_late_boundaries()), before smoothing. A bright far-range return can set a ping’s peak so high that the near seabed never reachesthreshold_fractionof it; runs of such pings are too long for the rolling median alone.Nonedisables.
- Returns:
- xr.Dataset
Copy of
ds; unmodified if none ofvalue_varsare present.
- sidescanpy.processing.bottom.BOTTOM_TRACK_REPLACE = ('missing', 'all')#
Accepted
replacevalues ofaltitude_from_bottom_track().
- sidescanpy.processing.bottom.altitude_from_bottom_track(ds, *, replace='missing', max_diff_m=None, min_altitude_m=0.5, min_return_m=5.0, min_return_fraction=0.7, smooth_window=10, threshold_fraction=0.2, min_run=10, smooth_pings=1)#
Set
altitude_mfrom the first bottom return (the"bottom_track"step).Many recordings carry no usable altitude: no altimeter, no bottom lock, or a recorder that writes 0. Without it the geometric step cannot compute ground range (NaN altitude) or computes it as if the towfish sat on the seabed (altitude 0). This step measures altitude from the data instead: per ping and channel, the onset of the bottom return (
detect_first_bottom_return()on the uncorrected amplitude, refined to the first sample reaching half the return level that follows it, since the detector’s rolling mean fires up tosmooth_windowsamples early), the nearer of the two channels, converted to metres of slant range.The first return comes from the beam’s near edge, which is tilted away from vertical, so on sloping or rough seabed the tracked value is the slant range to the nearest bottom point, at or slightly above the true altitude. Recorded altitude, when valid, is the better value; the default only fills pings that lack it.
- Parameters:
- dsxr.Dataset or SideScanSurvey
One frequency channel. Radiometric steps already applied are undone for detection only;
backscatter_intensityis not changed. ASideScanSurveyis corrected frequency by frequency with the same arguments; a new survey is returned and the input is not modified.- replace{“missing”, “all”}, default “missing”
"missing": replace pings whosealtitude_mis NaN or <= 0."all": replace every ping with a detection.- max_diff_mfloat, optional
With
"missing", also replace pings whose recorded altitude differs from the tracked one by more than this (a lost bottom lock that still writes a value).- min_altitude_mfloat, default 0.5
Detection ignores samples nearer than this.
- min_return_mfloat, default 5.0
Slant distance the return must stay above threshold, converted to samples and used as the detector’s
min_runwhen longer. A transmit burst (some recorders keep it; it can start after a quiet lead-in and be as bright as the seabed) lasts a metre or two; the seabed return lasts tens of metres.- min_return_fractionfloat, default 0.7
Fraction of that distance that must be above threshold (
min_run_fractionof the detector), so shadows and gaps in a rough seabed return do not push the onset further out.- smooth_window, threshold_fraction, min_run
Passed to
detect_first_bottom_return()(min_runin samples, raised tomin_return_mwhen that is longer).- smooth_pingsint, default 1
Rolling median of the tracked altitude over this many pings (
smooth_boundary_across_pings()); 1 = none. Smoothing looks across pings, so a chunkedcorrect_file()run can differ from an unchunked one withinsmooth_pings // 2pings of each chunk edge.
- Returns:
- xr.Dataset or SideScanSurvey
Same type as
ds:dswithaltitude_mreplaced where selected and a"bottom_track"history record. Pings with no detection keep their recorded value.fathometer_reading_mis not changed.