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):
.tgtis not a documented catalog format itself, butfile(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 markerFF FE FFthen 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..tgdis 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.h5sample 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 artifactsidescanpyalready 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_mand a constantmax_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_samplesgives a plausible meters-per-sample constant, used to convertslant_range_minto 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 waywindow_rowhas 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.tgdwindow 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.sdffiles (which can be years off real time). Parsed intoextra_attributes["catalog_ping_time"](Unix seconds on that clock, 1 s resolution) andextra_attributes["catalog_ping_number"]. Ping counters can restart after a recording gap, so the number alone is not unique; the time disambiguates. Usesidescanpy.targets.correlate.resolve_targets_by_catalog_ping()to place targets: it needs no clock offset.pick_timestamp(theTarget_<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
.tgdwindow holds the catalog channel of the high-frequency data, one row per second ping, newest ping first.window_rowcounts rows from the top, so its ping isnewest - 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, sowindow_rowis 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
.tgdis 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 every |
|
Re-decode the raw |
- sidescanpy.io.sdf.targets.read_klein_targets(directory)#
Read every
Target_<unix_ts>.tgtfile indirectory.Returns one
Targetper file, sorted by filename. Files that fail to parse are skipped with a warning rather than aborting the whole batch (matchesio/jsf/targets.py: read_discover_targets’s convention).position_confidenceis 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 fortarget.channelfrom its paired.tgdfile.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); columntarget.sampleis the target’s sample within each ping.