Eight wrecks at one scale#

70 m windows, 0.15 m cells, track up, port left, Dynamo, East Oaze, Erna Boldt, SS Storm, Amethyst, Dynamo, another pass, Ash, Aisha (2008)

Wrecks from a 2007-2008 survey of the approaches to the Thames, each shown in the same 70 m window at 0.15 m cells, so their sizes compare directly. The frame is the sonar’s own: along-track distance up, ground range across (port left, starboard right), the way the towfish saw each wreck, without placing it on a map.

Each window comes from one call, extract_geometric_window_griddata(), which interpolates the samples (linearly, with scipy’s griddata) onto a regular grid in that frame. Before it, three steps:

  • No TVG. The recorder applied its own gain, so a second one would over-brighten far range. The samples are only converted to dB.

  • Smoothed navigation. These files record a position at every ping, but it runs ahead of each GPS fix and jumps back by 1 to 3 m when the next fix arrives, once a second. Gridded as recorded, the jumps leave blank rows and a smeared wreck. smooth_navigation() replaces each position with a straight-line fit over 5 s, which removes the jumps.

  • Bottom track and ground range. The files record altitude, but altitude_from_bottom_track() fills any ping without it, then compute_geometric_ranges() turns slant range into ground range.

Data: BGS London Gateway, Klein 3000 at 400 kHz, Open Government Licence v3.0, contains data supplied by UKRI (see Example data).

Usage: python examples/gallery/plot_wreck_gallery.py [output_dir]

# 2026 rshomberg@uri.edu

import sys
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from sidescanpy import datasets
from sidescanpy.export.geometric_window import extract_geometric_window_griddata
from sidescanpy.export.windows.levels import with_db_var
from sidescanpy.io import read_and_convert
from sidescanpy.log import configure_logging
from sidescanpy.processing import (
    altitude_from_bottom_track,
    compute_geometric_ranges,
    interpolate_aux_onto_pings,
    smooth_navigation,
)
from sidescanpy.processing.navigation import along_track_distance_m

out = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
out.mkdir(parents=True, exist_ok=True)
# The readers warn about these files' known quirks (sample scaling, sound
# velocity, the position offset); see the XTF format page.
configure_logging("ERROR")

The wrecks#

One line per wreck, with the ping nearest its middle and its ground range from the track (m, port negative), read off the waterfalls.

WRECKS = {
    "Dynamo": ("bgs_london_gateway_dynamo_78.xtf", 1718, 29.9),
    "East Oaze": ("bgs_london_gateway_eastoaze_31.xtf", 984, -11.0),
    "Erna Boldt": ("bgs_london_gateway_ernaboldt_13.xtf", 555, -32.4),
    "SS Storm": ("bgs_london_gateway_storm_04.xtf", 515, 40.8),
    "Amethyst": ("bgs_london_gateway_amethyst_25.xtf", 615, -13.0),
    "Dynamo, another pass": ("bgs_london_gateway_dynamo_80.xtf", 236, 21.0),
    "Ash": ("bgs_london_gateway_ash_11.xtf", 542, -34.7),
    "Aisha (2008)": ("bgs_london_gateway_aisha_05.xtf", 1321, 9.4),
}
WINDOW_M, CELL_M = 70.0, 0.15
SIZE_PX = int(round(WINDOW_M / CELL_M))

One wreck, step by step#

Read the line, interpolate the navigation onto the pings and smooth it, keep the pings around the wreck, add ground range and grid a window centred on the wreck.

def wreck_window(name, ping, across_m):
    survey = read_and_convert([datasets.fetch(name)], fmt="xtf", nav_crs="EPSG:32631")
    survey = smooth_navigation(interpolate_aux_onto_pings(survey), window_s=5.0)
    ds = survey.get_frequency("hz_400000").dataset
    first = max(ping - 500, 0)
    ds = ds.isel(ping=slice(first, ping + 500)).load()
    ds = with_db_var(compute_geometric_ranges(altitude_from_bottom_track(ds)))
    along = along_track_distance_m(ds, fill_nav_stalls=True)
    image, _ = extract_geometric_window_griddata(
        ds, SIZE_PX, resolution_m=CELL_M, center_along_m=float(along[ping - first]),
        center_across_m=across_m, channel_idx=None, backscatter_var="backscatter_db",
    )
    return image


windows = {title: wreck_window(*spec) for title, spec in WRECKS.items()}
for title, image in windows.items():
    print(f"{title:22s} {image.shape[0]} x {image.shape[1]} cells, {np.isfinite(image).mean():.0%} filled")
Dynamo                 467 x 467 cells, 99% filled
East Oaze              467 x 467 cells, 99% filled
Erna Boldt             467 x 467 cells, 100% filled
SS Storm               467 x 467 cells, 100% filled
Amethyst               467 x 467 cells, 97% filled
Dynamo, another pass   467 x 467 cells, 100% filled
Ash                    467 x 467 cells, 100% filled
Aisha (2008)           467 x 467 cells, 100% filled