sidescanpy.export.geometric_window#
Geometric (along-track x across-track meters) windowed export.
Resamples a bounded region directly at an explicit (resolution_m,
size_px), centered on a given real-world point — a survey-local frame
(no CRS/UTM projection, unlike export.georef_window), using the
per-sample across_track_m that
processing/geometric.py::compute_geometric_ranges already attaches,
and the per-ping cumulative along-track distance from
processing.navigation.along_track_distance_m.
2026 rshomberg@uri.edu
Contents
Resample a |
|
Same contract as |
|
Same contract as |
|
Interpolate scattered samples at query points with |
|
Translate a real-world |
- sidescanpy.export.geometric_window.extract_geometric_window(channel_ds, size_px, *, resolution_m, center_along_m, center_across_m, channel_idx=0, backscatter_var='backscatter_intensity', search_radius_m=None, along_search_radius_m=None, across_search_radius_m=None, avoid_edge_gaps=False, along_resolution_m=None, across_resolution_m=None, along_size_px=None, across_size_px=None)#
Resample a
size_px x size_pxsquare region, centered on(center_along_m, center_across_m), at exactlyresolution_mmeters/pixel on both axes.Both axes always share the same
resolution_mandsize_pxby construction — no derived-from-extent resolution, no separate resize step, so the result is never stretched relative to the real world (a previous version computedresolution_mfrom the larger of the window’s own along/across extents but still forced both axes tosize_pxsamples — the smaller-extent axis came out visibly stretched; fixed by making resolution and size the only inputs, extent purely a consequence of them). This is still the default —along_resolution_m/across_resolution_m/along_size_px/across_size_pxbelow are opt-in, and every existing caller that doesn’t pass them gets byte-identical square-pixel output.channel_idx=Nonepools samples from both channels into the same point cloud instead of one side only — unlike the raw-pixelexport.windowsprimitives, this needs no explicit mirroring:across_track_mis already a real-world coordinate (port negative, starboard positive), so both sides combine correctly by construction, giving a single image spanning the full swath width with the nadir gap naturally in the middle (whereveracross_track_mis NaN/near zero on both sides).- Parameters:
- channel_dsxr.Dataset
Already ping-sliced to a neighborhood comfortably containing the requested
size_px * resolution_mextent around the center — this function does not itself bound how much source data it searches; a much wider slice than needed just costs more KDTree build time for no benefit.- size_pxint
Output side length in pixels.
- resolution_mfloat
Meters per pixel, both axes. Pick small (near the channel’s own sample interval) for a maximum-detail close-up; pick
real_swath_width_m / size_pxto fit an entire swath width (both channels) into the frame.- center_along_m, center_across_mfloat
Real-world center of the requested window, in the same along-track-distance /
across_track_munits the survey’s own per-sample geometry uses (e.g. a target’s own position, read directly fromacross_track_mat its ping/sample/channel).- channel_idxint or None, default 0
Restrict to one physical channel (0=port, 1=starboard), or
Noneto pool both.- search_radius_mfloat, optional
Isotropic KDTree query radius (same in both directions); defaults to
1.5 * resolution_m. A survey whose along-track position updates at a much lower rate than its ping rate (real case: a Klein SDF survey whose GPS fixes only every ~8 pings, jumping ~2m each time) has real gaps between along-track fixes wider than a resolution chosen from the average distance/ping can bridge at the default radius — the result grid shows periodic empty stripes at exactly the fix-to-fix interval, since most cells’ nearest real point sits outside1.5 * resolution_m. Passing a wider radius (e.g. the real fix-to-fix jump distance) bridges those gaps without also needing a coarser (more zoomed-out)resolution_m. Ignored if either of the two params below is given.- along_search_radius_m, across_search_radius_mfloat, optional
Independent along-track / across-track search radii — an ellipse instead of a circle, in real (along_m, across_m) space. Each axis is rescaled by its own radius before the
cKDTreeis built/queried (then queried with a unit ball), which is exactly equivalent to an elliptical search with no custom distance metric needed. Default (None) for either falls back tosearch_radius_m(or its own default) — fully backward compatible with every existing caller.Real motivation (2026-09-28): a swath’s along-track (ping spacing) and across-track (sample interval) native resolutions can differ by an order of magnitude or more. The single isotropic
search_radius_mthis function used to have had to be wide enough to bridge real along-track gaps — which meant every cell’s average also pooled far more across-track real detail than the data’s own resolution supports, regardless of how fineresolution_mwas set. A caller requesting a finerresolution_mfor more real detail got more pixels of an equally over-smoothed image, not more detail — confirmed directly on real data as the cause of a “chips are blurry” report. Passing a narrowacross_search_radius_m(sized to the real sample interval) alongside a widealong_search_radius_m(still sized to bridge real ping gaps) fixes this: real across-track texture is preserved instead of averaged away, without reintroducing along-track gaps.- avoid_edge_gapsbool, default False
When True and the requested center is close enough to the edge of this channel’s own recorded extent that the window would otherwise include genuine no-data cells (e.g. a target near the max-range edge of the swath), shift the center inward just enough to keep the full window inside real data instead – same tradeoff
extract_chipalready makes for raw-pixel windows near an array edge. The target itself is then no longer necessarily at the window’s exact center;locate_pointagainst the original requested point still reports its true (now off-center) position correctly. DefaultFalsekeeps the window exactly centered on the requested point regardless (this function’s original, unconditional behavior) – opt in per caller, since not every use of this function is a target-crop where “no data” is worse than “off-center”.- along_resolution_m, across_resolution_mfloat, optional
Independent along-track / across-track meters-per-pixel — real, non-square output pixels. Default (
None) for either falls back toresolution_m— fully backward compatible, every existing caller gets exactly the prior square-pixel behavior.Real motivation (2026-09-29): even with anisotropic search radii (
along_search_radius_m/across_search_radius_mabove), a single sharedresolution_mstill forces the SAME pixel density on both axes. On a survey whose along-track native resolution is much coarser than across-track (ping spacing an order of magnitude or more wider than sample interval — one real survey measured at ~16:1), the along-track search radius floor (tied to real ping-to-ping gaps, independent ofresolution_m— seealong_search_radius_mabove) ends up wide relative to a fine shared pixel spacing, visible in the output as elliptical averaging footprints — confirmed directly, real user report (“you can clearly see the ellipses which appear wide… that is not an issue w/ the radiometric”). No amount of tuning a single sharedresolution_m/search radius eliminates this, because a plain pixel-space resize (which is what “radiometric” looks like) never averages a real neighborhood at all, so it has no footprint to show — it isn’t solving the same problem. Settingalong_resolution_mcoarser thanacross_resolution_m(close to the real ping spacing) lets each axis’s pixel size match its OWN real resolving power, eliminating the visible-footprint artifact without averaging away real across-track detail that IS there to resolve.- along_size_px, across_size_pxint, optional
Independent output row / column counts. Default (
None) for either falls back tosize_px. Combined withalong_resolution_m/across_resolution_m, this is what actually makes the output array non-square in shape, not just in real-world meters-per-pixel — e.g.along_size_px=20atalong_resolution_m=0.4covers the same real 8m asacross_size_px=320atacross_resolution_m=0.025, and the returned array is genuinely(20, 320), not square. A caller displaying this gets correct proportions for free from most plotting libraries’ default aspect handling (e.g. matplotlibimshow’saspect="equal", driven by array shape alone, with noextentneeded) — it is not a stretched square image, it is a genuinely shorter/narrower real array.
- Returns:
- (grid, meta)
grid:(along_size_px, across_size_px)array (both default tosize_px, reproducing the prior square shape unless overridden). NaN where no sample fell within the search radius of a cell.meta: dict withcenter_row/center_col(the requested center’s own pixel position ingrid— always the exact middle, since the grid is built centered on it by construction), the resolvedalong_min/across_minbounds,along_resolution_m/across_resolution_m(the resolved per-axis values, always present even when isotropic), andresolution_m— a legacy key kept for backward compatibility, equal toalong_resolution_m; new code should read the per-axis keys instead, since this key is ambiguous once the two differ.
- Raises:
- ValueError
If no finite
across_track_m/backscatter samples exist inchannel_dsat all (e.g. entirely water column).
- sidescanpy.export.geometric_window.extract_geometric_window_binned(channel_ds, size_px, *, resolution_m, center_along_m, center_across_m, channel_idx=0, backscatter_var='backscatter_intensity', avoid_edge_gaps=False, along_resolution_m=None, across_resolution_m=None, along_size_px=None, across_size_px=None)#
Same contract as
extract_geometric_window()(same params, same(grid, meta)shape, samemetakeys, drop-in interchangeable withlocate_point()) but resamples viascipy.stats.binned_statistic_2dinstead of a hand-writtencKDTree.query_ball_pointloop over every output cell.2026-09-29, real user request, following this file’s own established-libraries-first convention (see the “Rules” section of
AGENTS.md): once per-axis pixel resolution existed (along_resolution_m/across_resolution_mabove), each output cell is really just “the mean of every real sample whose (along, across) position falls inside this cell’s rectangle” — an ordinary 2D histogram-style binning, not a radius search. scipy already ships exactly that as a single vectorized call, so a custom per-cell loop wasn’t buying anything.Not a superset of the KDTree version — a genuinely different resampling philosophy, not just a faster implementation of the same one:
No
search_radius_m/along_search_radius_m/across_search_radius_m— a point either falls inside a cell’s exact rectangle or it doesn’t, there is no overlap/gap-bridging radius to tune. A cell with zero real samples inside its own rectangle is NaN, even if a real sample sits just across the boundary in the neighboring cell. This means a coarsealong_resolution_mchosen to bridge real ping-to-ping gaps (the KDTree version’salong_search_radius_mrole) still works here — a wide-enough bin naturally contains the next ping’s point — but analong_resolution_mfiner than the real ping-to-ping spacing will show real empty cells between pings instead of the KDTree version’s radius-bridged fill.Strictly faster on real data (one vectorized scipy call instead of one Python-level
query_ball_pointcall per output pixel), but not chosen for speed — chosen because “one real neighborhood per pixel, no radius tuning” is a more direct match for whatalong_resolution_m/across_resolution_mwere already designed to express once they exist per-axis. Seeexport/geometric_window.py’s module tests (TestExtractGeometricWindowBinned) for a real-data comparison against the KDTree version at matching resolution.
- Raises:
- ValueError
If no finite
across_track_m/backscatter samples exist inchannel_dsat all (e.g. entirely water column).
- sidescanpy.export.geometric_window.extract_geometric_window_griddata(channel_ds, size_px, *, resolution_m, center_along_m, center_across_m, channel_idx=0, backscatter_var='backscatter_intensity', avoid_edge_gaps=False, along_resolution_m=None, across_resolution_m=None, along_size_px=None, across_size_px=None, interp_method='linear', max_fill_distance_m=None)#
Same contract as
extract_geometric_window()/extract_geometric_window_binned()(same params, same(grid, meta)shape, samemetakeys, drop-in interchangeable withlocate_point()) but resamples viascipy.interpolate.griddata– full scattered-data interpolation across the whole requested grid, not a radius search (KDTree) or strict rectangle membership (binned).2026-09-29, real user request/direction: an established-library resampling option with no NaN gaps by construction – unlike
extract_geometric_window_binned(), every output cell gets a value regardless of how sparse the real along-track sampling is relative toresolution_m, becausegriddatainterpolates (and, for the small edge fraction outside the real data’s convex hull, nearest-neighbor-extrapolates – see below) rather than reporting “no real sample here.”This is a genuinely different tradeoff than the other two functions, not a strict improvement — say so plainly: at a resolution much finer than the real along-track sampling, the large majority of a griddata window’s pixels are constructed by interpolation, not measured by the sensor. Real-data check (two real targets,
resolution_m=0.03m, ~60-66% of cells with no real sample at that resolution): griddata never shows a gap, but most of what fills those gaps is triangulated from real points several real ping-widths away, not a resolved local measurement. Compareextract_geometric_window()(zero fabrication, real radius averaging) andextract_geometric_window_binned()(zero fabrication, real gaps shown honestly).interp_method:"linear"(default),"cubic", or"nearest"– passed straight through toscipy.interpolate. griddata’s ownmethod."linear"/"cubic"leave NaN outside the real data’s convex hull (no point to triangulate against there); those cells are automatically filled with a second,method="nearest"pass so this function never returns a gap regardless ofinterp_method–"nearest"itself is already gap-free after the first pass.max_fill_distance_m: cells farther than this from every real sample are NaN – outside the recorded swath (past max range, the nadir gap, the excluded channel of a single-channel window), where either pass above would otherwise extend the nearest edge sample into smeared wedges/streaks of invented seafloor (seen on chips wider than the swath).None(default) derives it from the data: 1.5x the larger of the 95th-percentile along-track ping gap, the median across-track sample step, and the output resolution – so gaps between real pings/samples still interpolate.np.infrestores the old never-NaN behavior.- Raises:
- ValueError
If no finite
across_track_m/backscatter samples exist inchannel_dsat all (e.g. entirely water column).
- sidescanpy.export.geometric_window.interpolate_scattered(points, values, query_points, *, interp_method='linear', max_fill_distance_m=inf)#
Interpolate scattered samples at query points with
scipy.interpolate.griddata.The resampler behind the
"griddata"windows, geometric and georeferenced. Query points outside the samples’ convex hull get the nearest sample’s value; query points farther thanmax_fill_distance_mfrom every sample are NaN, so the edge of the recorded swath (and the nadir gap) is not extended into invented seabed. Coordinates are shifted to the query points’ centre first, so projected (UTM) coordinates triangulate as accurately as local ones.- Parameters:
- pointsnp.ndarray
(n, 2)sample positions in meters (any planar frame).- valuesnp.ndarray
(n,)finite sample values.- query_pointsnp.ndarray
(m, 2)positions to interpolate at, same frame.- interp_method{“linear”, “cubic”, “nearest”}, default “linear”
griddata’smethod.- max_fill_distance_mfloat, default inf
See above. Only samples within twice this distance of the query points’ bounding box are triangulated, which keeps a small window fast when
pointscovers a large neighborhood.
- Returns:
- np.ndarray
(m,)float64.
- sidescanpy.export.geometric_window.locate_point(along_m, across_m, meta)#
Translate a real-world
(along_m, across_m)point into pixel(row, col)coordinates within a grid produced byextract_geometric_window(), using that call’s own returnedmeta— for marking a different point than the one the window was centered on (e.g. a second target sharing the same frame).Reads
meta’s per-axisalong_resolution_m/across_resolution_m(not the legacyresolution_mkey) — correct for both the isotropic/square case (where they’re equal anyway) and a real anisotropic-pixel window.