sidescanpy.io.sdf.targets#

Klein/SonarWiz native target catalog reader (Target_<unix_ts>.tgt / .tgd pairs).

Reverse-engineered from real files; not vendor-documented. No independent reference exists to check field meanings against — validation consisted of rendering the raw window and checking that a candidate field lands on a visible sonar contact, not a byte-for-byte spec match. Every Target produced here gets position_confidence="unknown" unconditionally, regardless of which fields below are confirmed — that flag is about absolute geographic position (never found in this format at all, see below), not about whether the on-image box placement is trustworthy.

What’s confirmed (byte-exact, cross-checked across multiple real files):

  • .tgt is not a documented catalog format itself, but file(1) identifies it as a dBase-IV memo (.DBT) block file, and its content is consistent with that: a small fixed header, then a run of length-prefixed UTF-16LE string fields (each starting with the 3-byte marker FF FE FF then a 1-byte character count), then a fixed-layout binary trailer once the strings end.

  • The file’s own Unix timestamp (from its filename, Target_<ts>.tgt) appears verbatim as a little-endian int32 at byte offset 8 — confirms the header is being read correctly and the timestamp is the pick time, not an arbitrary filename.

  • .tgd is a fixed-size raw sonar window: an 8-byte header (int32 n_channels, int32 n_samples — read as (2, 2500) on the one real survey checked, matching that survey’s own converted .ss.h5 sample count exactly), followed by (n_pings, n_channels, n_samples) int16 amplitude — real backscatter data, confirmed by a plausible leading-saturation run (same shape as the leading-blanking-run artifact sidescanpy already knows about from JSF data) and by-eye inspection of the decoded window showing genuine seafloor texture.

What’s confirmed by visual cross-check:

  • window_row: the raw trailer int32 (ints[5]) is 2x the true row — this module halves it before returning. Confirmed against six real targets by rendering each one’s full decoded window with no box, locating the visible sonar contact by eye, and checking which candidate row landed on it. Five of six checks landed cleanly on a visible feature; one landed in a relatively low-contrast area (the column could be off for that target, or it is a weak contact). Why the field is 2x is unknown.

  • slant_range_m and a constant max_window_range_m (varies by survey file, e.g. 40.0 or 60.0 seen so far): a float32 slot that varies per target in a plausible tens-of-meters range, paired with a float32 slot that’s identical across every target in one file. max_window_range_m / n_samples gives a plausible meters-per-sample constant, used to convert slant_range_m into a window column index. Not contradicted by any of the six row-validation checks above (the column landed in the right vicinity in every case), but never independently verified the way window_row has been.

What’s inferred, not confirmed (best-effort field mapping in the binary trailer that follows the string fields, found by comparing several real files and noting which int32/float32 slots vary plausibly vs. stay constant):

  • channel (0/1): an int32 slot that’s always exactly 0 or 1; 0 = port, 1 = starboard. Confirmed on real catalogs: the .tgd window matches the survey’s own pings on that side.

  • The string fields ahead of the trailer are kept verbatim in extra_attributes["string_fields"]: ping time, ping number (both interpreted below), a decimal number (not interpreted), the target name, and a notes field. The notes field is the unedited placeholder text (“The operator places target notes here. Multiple lines OK.”) on every real target checked so far, not a real label.

Catalog ping time and number (confirmed on real catalogs by matching every .tgd window back to the survey’s own pings):

  • String field 0 is the date-time (M/D/YYYY h:mm:ss) and field 1 the ping number of the target’s own ping, both on the sonar’s own clock, the same clock as the pings in the .sdf files (which can be years off real time). Parsed into extra_attributes["catalog_ping_time"] (Unix seconds on that clock, 1 s resolution) and extra_attributes["catalog_ping_number"]. Ping counters can restart after a recording gap, so the number alone is not unique; the time disambiguates. Use sidescanpy.targets.correlate.resolve_targets_by_catalog_ping() to place targets: it needs no clock offset.

  • pick_timestamp (the Target_<unix> file name) is when the pick was saved, on the acquisition PC’s clock: typically 30-90 s after the target ping, and hours later for picks made during playback. Matching pings by it places targets tens to hundreds of metres along track from where they are.

  • The .tgd window holds the catalog channel of the high-frequency data, one row per second ping, newest ping first. window_row counts rows from the top, so its ping is newest - 2 * window_row. On real catalogs this lands on the catalog ping for about three quarters of the targets; for the rest it is several to ~100 pings older, so window_row is not a reliable target ping. sample (from the range field) has been consistent.

  • Some picks are made from the live display while logging is off; their catalog time falls in a recording gap and the .tgd is the only copy of that data.

No absolute latitude/longitude was found anywhere in .tgt (searched exhaustively for plausible float32/float64 pairs in the survey area’s lat/lon range — none found). Position must be resolved externally, e.g. by matching pick_timestamp against a converted survey’s own per-ping timestamps (see sidescanpy.targets.correlate.resolve_target_position) — this is a nearest-ping approximation, not a stored pick position, and does not upgrade position_confidence above "unknown".

Parsing only: position resolution and clock-offset calibration against converted .ss.h5 files live in sidescanpy.targets.correlate.

2026 rshomberg@uri.edu

Contents

read_klein_targets

Read every Target_<unix_ts>.tgt file in directory.

decode_target_window

Re-decode the raw (n_pings, n_samples) window for target.channel from its paired .tgd file.

sidescanpy.io.sdf.targets.read_klein_targets(directory)#

Read every Target_<unix_ts>.tgt file in directory.

Returns one Target per file, sorted by filename. Files that fail to parse are skipped with a warning rather than aborting the whole batch (matches io/jsf/targets.py: read_discover_targets’s convention). position_confidence is unconditionally "unknown" on every target returned — see the module docstring.

sidescanpy.io.sdf.targets.decode_target_window(target)#

Re-decode the raw (n_pings, n_samples) window for target.channel from its paired .tgd file.

Returns real int16 sonar amplitude (not a rendered/processed image), cast to float64. Rows are every second ping, newest first. Row target.extra_attributes["window_row"] is the catalog’s box row, which is not always the target’s own ping (see module docstring); column target.sample is the target’s sample within each ping.