Dedup format layer; public accessors replace private reach-throughs
- SrasFile: _parse_v6 now retains per-angle x_delta and the verbatim preamble/background byte spans, and gains public data_offset, y_pos_per_angle, and iter_angle_blocks() (which now owns the ragged block-offset walk used three separate places before). - sras_edit_scans: the 70-line re-parse of the v6 header sections (_read_v6_sections/_reread_span/_v6_angle_offsets) collapses into a _write_v6 that consumes SrasFile directly — verified byte-identical round-trip on v6 int8/int16 and legacy files. Eight print-and-exit pairs become _die(). - tools/make_test_sras imports the struct layouts from sras_format and the rotation matrix from sras_compute instead of re-declaring them (byte assembly stays independent of the reader). - sras_compute: block_mean_2d, pixel_pitch_mm, nominal_delta_deg made public (they were GUI-facing); registration_workers() and default_max_workers() wrap the remaining private reach-throughs from sras_workers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+36
-112
@@ -25,15 +25,17 @@ import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from sras_format import (
|
||||
GEO_FMT_V6, GEO_SIZE_V6, HDR_FMT, HDR_FMT_V6, HDR_SIZE, HDR_SIZE_V6,
|
||||
SrasFile,
|
||||
)
|
||||
from sras_format import GEO_FMT_V6, HDR_FMT, HDR_FMT_V6, HDR_SIZE, SrasFile
|
||||
|
||||
_LEGACY_VERSIONS = (2, 3, 4, 5)
|
||||
_V6_VERSIONS = (6, 7)
|
||||
|
||||
|
||||
def _die(msg: str):
|
||||
print(f"Error: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def parse_args():
|
||||
p = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
@@ -106,7 +108,7 @@ def _write_legacy(sras: SrasFile, keep: list[int], out_path: Path):
|
||||
angle_table_size = sras.n_angles * 4
|
||||
with open(sras.path, "rb") as f:
|
||||
f.seek(HDR_SIZE + angle_table_size)
|
||||
shared_mid = f.read(sras._data_offset - (HDR_SIZE + angle_table_size))
|
||||
shared_mid = f.read(sras.data_offset - (HDR_SIZE + angle_table_size))
|
||||
|
||||
angle_bytes = n_rows * n_ch * n_frames * spf * bps
|
||||
|
||||
@@ -115,128 +117,55 @@ def _write_legacy(sras: SrasFile, keep: list[int], out_path: Path):
|
||||
fout.write(sras.angles_deg[keep].astype(">f4").tobytes())
|
||||
fout.write(shared_mid)
|
||||
for a in keep:
|
||||
_copy_range(fin, fout, sras._data_offset + a * angle_bytes, angle_bytes)
|
||||
_copy_range(fin, fout, sras.data_offset + a * angle_bytes, angle_bytes)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# v6/v7: per-angle geometry, ragged waveform blocks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _read_v6_sections(path: Path) -> dict:
|
||||
"""Raw, low-level read of everything before the waveform data.
|
||||
|
||||
SrasFile._parse_v6 reads and discards each angle's x_delta (it's not
|
||||
part of the display-facing geometry it exposes), so a round-trip through
|
||||
SrasFile would silently drop that field. Re-parsing here keeps every
|
||||
byte of the Per-Angle Geometry Table intact.
|
||||
"""
|
||||
with open(path, "rb") as f:
|
||||
hdr_raw = f.read(HDR_SIZE_V6)
|
||||
(magic, ver, n_angles_declared, x_start_nom, y_start_nom, x_delta_nom,
|
||||
y_delta_nom, row_spacing, vel, freq, spf, sr, bps,
|
||||
n_ch) = struct.unpack(HDR_FMT_V6, hdr_raw)
|
||||
|
||||
angles_deg = list(struct.unpack(f">{n_angles_declared}f",
|
||||
f.read(n_angles_declared * 4)))
|
||||
|
||||
geo = [struct.unpack(GEO_FMT_V6, f.read(GEO_SIZE_V6))
|
||||
for _ in range(n_angles_declared)] # (x_start, x_delta, n_frames, n_rows)
|
||||
|
||||
row_table = [f.read(geo[a][3] * 4) for a in range(n_angles_declared)]
|
||||
|
||||
preamble_start = f.tell()
|
||||
for _ in range(n_ch):
|
||||
(length,) = struct.unpack(">H", f.read(2))
|
||||
f.read(length)
|
||||
preambles_raw = _reread_span(f, preamble_start)
|
||||
|
||||
bg_start = f.tell()
|
||||
(n_bg,) = struct.unpack(">I", f.read(4))
|
||||
f.read(n_bg)
|
||||
background_raw = _reread_span(f, bg_start)
|
||||
|
||||
data_offset = f.tell()
|
||||
|
||||
return {
|
||||
"version": ver, "n_angles_declared": n_angles_declared,
|
||||
"x_start_nom": x_start_nom, "y_start_nom": y_start_nom,
|
||||
"x_delta_nom": x_delta_nom, "y_delta_nom": y_delta_nom,
|
||||
"row_spacing": row_spacing, "vel": vel, "freq": freq,
|
||||
"spf": spf, "sr": sr, "bps": bps, "n_ch": n_ch,
|
||||
"angles_deg": angles_deg, "geo": geo, "row_table": row_table,
|
||||
"preambles_raw": preambles_raw, "background_raw": background_raw,
|
||||
"data_offset": data_offset,
|
||||
}
|
||||
|
||||
|
||||
def _reread_span(f, start: int) -> bytes:
|
||||
end = f.tell()
|
||||
f.seek(start)
|
||||
span = f.read(end - start)
|
||||
f.seek(end)
|
||||
return span
|
||||
|
||||
|
||||
def _v6_angle_offsets(sections: dict, file_size: int) -> list[tuple[int, int]]:
|
||||
"""(offset, nbytes) of each declared angle's waveform block, stopping at
|
||||
the first angle whose data isn't fully on disk (aborted scan)."""
|
||||
n_ch, spf, bps = sections["n_ch"], sections["spf"], sections["bps"]
|
||||
offsets = []
|
||||
offset = sections["data_offset"]
|
||||
for xs, xd, nf, nr in sections["geo"]:
|
||||
nbytes = nr * n_ch * nf * spf * bps
|
||||
if offset + nbytes > file_size:
|
||||
break
|
||||
offsets.append((offset, nbytes))
|
||||
offset += nbytes
|
||||
return offsets
|
||||
|
||||
|
||||
def _write_v6(in_path: Path, sections: dict, keep: list[int], out_path: Path):
|
||||
file_size = in_path.stat().st_size
|
||||
offsets = _v6_angle_offsets(sections, file_size)
|
||||
geo = sections["geo"]
|
||||
|
||||
def _write_v6(sras: SrasFile, keep: list[int], out_path: Path):
|
||||
header = struct.pack(
|
||||
HDR_FMT_V6, b"SRAS", sections["version"], len(keep),
|
||||
sections["x_start_nom"], sections["y_start_nom"],
|
||||
sections["x_delta_nom"], sections["y_delta_nom"],
|
||||
sections["row_spacing"], sections["vel"], sections["freq"],
|
||||
sections["spf"], sections["sr"], sections["bps"], sections["n_ch"],
|
||||
HDR_FMT_V6, b"SRAS", sras.version, len(keep),
|
||||
sras.x_start_nominal_mm, sras.y_start_nominal_mm,
|
||||
sras.x_delta_nominal_mm, sras.y_delta_nominal_mm,
|
||||
sras.row_spacing_mm, sras.velocity_mm_s, sras.laser_freq_hz,
|
||||
sras.samples_per_frame, sras.sample_rate_hz,
|
||||
sras.bytes_per_sample, sras.n_channels,
|
||||
)
|
||||
|
||||
with open(in_path, "rb") as fin, open(out_path, "wb") as fout:
|
||||
blocks = {a: (offset, nbytes) for a, offset, nbytes in sras.iter_angle_blocks()}
|
||||
with open(sras.path, "rb") as fin, open(out_path, "wb") as fout:
|
||||
fout.write(header)
|
||||
fout.write(struct.pack(f">{len(keep)}f",
|
||||
*[sections["angles_deg"][i] for i in keep]))
|
||||
fout.write(sras.angles_deg[keep].astype(">f4").tobytes())
|
||||
for i in keep:
|
||||
fout.write(struct.pack(GEO_FMT_V6, *geo[i]))
|
||||
fout.write(struct.pack(
|
||||
GEO_FMT_V6, float(sras.x_start_mm[i]),
|
||||
float(sras.x_delta_mm_per_angle[i]),
|
||||
int(sras.n_frames[i]), int(sras.n_rows[i])))
|
||||
for i in keep:
|
||||
fout.write(sections["row_table"][i])
|
||||
fout.write(sections["preambles_raw"])
|
||||
fout.write(sections["background_raw"])
|
||||
fout.write(sras.y_pos_per_angle[i].astype(">f4").tobytes())
|
||||
fout.write(sras.preambles_raw)
|
||||
fout.write(sras.background_raw)
|
||||
for i in keep:
|
||||
off, nbytes = offsets[i]
|
||||
_copy_range(fin, fout, off, nbytes)
|
||||
offset, nbytes = blocks[i]
|
||||
_copy_range(fin, fout, offset, nbytes)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
in_path = Path(args.input)
|
||||
if not in_path.exists():
|
||||
print(f"Error: input file not found: {in_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_die(f"input file not found: {in_path}")
|
||||
|
||||
print(f"Reading {in_path} ...", flush=True)
|
||||
try:
|
||||
sras = SrasFile(str(in_path))
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_die(str(e))
|
||||
|
||||
if sras.version not in (*_LEGACY_VERSIONS, *_V6_VERSIONS):
|
||||
print(f"Error: unsupported .sras version: {sras.version}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_die(f"unsupported .sras version: {sras.version}")
|
||||
|
||||
aborted_note = " (scan aborted; trailing angle(s) already excluded)" if sras.scan_aborted else ""
|
||||
print(f" Version : v{sras.version}", flush=True)
|
||||
@@ -247,16 +176,13 @@ def main():
|
||||
return
|
||||
|
||||
if not args.output:
|
||||
print("Error: output path required unless --list is given.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_die("output path required unless --list is given.")
|
||||
if not (args.drop or args.keep):
|
||||
print("Error: specify --drop or --keep (see --list for indices).", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_die("specify --drop or --keep (see --list for indices).")
|
||||
|
||||
out_path = Path(args.output)
|
||||
if out_path.resolve() == in_path.resolve():
|
||||
print("Error: output path must differ from input path.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_die("output path must differ from input path.")
|
||||
|
||||
try:
|
||||
if args.drop:
|
||||
@@ -265,12 +191,10 @@ def main():
|
||||
else:
|
||||
keep = sorted(_parse_index_list(args.keep, sras.n_angles))
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_die(str(e))
|
||||
|
||||
if not keep:
|
||||
print("Error: at least one angle must remain.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_die("at least one angle must remain.")
|
||||
|
||||
dropped = [a for a in range(sras.n_angles) if a not in keep]
|
||||
print(f"\nDropping angle(s): {dropped}")
|
||||
@@ -280,7 +204,7 @@ def main():
|
||||
if sras.version in _LEGACY_VERSIONS:
|
||||
_write_legacy(sras, keep, out_path)
|
||||
else:
|
||||
_write_v6(in_path, _read_v6_sections(in_path), keep, out_path)
|
||||
_write_v6(sras, keep, out_path)
|
||||
|
||||
in_mb = in_path.stat().st_size / 1024**2
|
||||
out_mb = out_path.stat().st_size / 1024**2
|
||||
|
||||
Reference in New Issue
Block a user