Add batch export of DC/RF/Velocity map images
Add Export -> Batch Export Images..., which renders and saves one PNG per angle for whichever of CH1 (RF), CH3 (DC-A), CH4 (DC-B), and Velocity the user selects, for the currently open file. Each channel gets its own fixed min/max colorbar range, entered once in the dialog and held constant across every exported angle, so the resulting images are directly comparable to each other instead of each auto-scaling to its own data (today's live-view default). BatchExportDialog (sras_viewer.py) collects the output folder, file prefix, and per-channel checkbox + range, seeded from whatever's already cached (session DC/FFT caches, or the file's own v7 cache blocks) so opening the dialog never triggers a fresh compute. Export runs on a background thread via a new BatchExportWorker (sras_workers.py), which computes each angle's channel image with the existing dc_image_mv/compute_rf_image helpers (reusing one FFT per angle for both RF and Velocity) and renders it with a headless matplotlib Agg canvas. Also includes several small correctness/robustness fixes that were already staged in the working tree: an int16-vs-float32 accumulator mismatch in sras_average.py's row averaging, a DC4-mask cache reuse and atomic sidecar write in sras_compute.py, a dc3/dc4 pairing guard in sras_format.py's v7 cache writer, and matching updates to the tools/ test fixtures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,16 @@ Usage:
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
HDR_FMT_V6 = ">4sBHfffffffIdBB"
|
||||
GEO_FMT_V6 = ">ffIH"
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
# Packed straight from sras_format.py's own constants (not a hand-copy) so
|
||||
# this generator can never silently drift from what the real parser expects.
|
||||
from sras_format import GEO_FMT_V6, HDR_FMT as HDR_FMT_LEGACY, HDR_FMT_V6 # noqa: E402
|
||||
|
||||
# Per-angle (n_rows, n_frames) — deliberately different per angle so ragged
|
||||
# geometry handling is actually exercised.
|
||||
@@ -125,9 +129,6 @@ def write(path: Path, n_angles: int = 3, seed: int = 0,
|
||||
return meta
|
||||
|
||||
|
||||
HDR_FMT_LEGACY = ">4sBHHffffIIdBB"
|
||||
|
||||
|
||||
def write_legacy(path: Path, version: int = 4, n_angles: int = 2,
|
||||
n_rows: int = 4, n_frames: int = 10,
|
||||
samples_per_frame: int = 32, seed: int = 0) -> dict:
|
||||
|
||||
+14
-7
@@ -288,23 +288,30 @@ def test_legacy_parse_and_average(scratch: Path):
|
||||
check("background preserved",
|
||||
np.array_equal(avg.background, SrasFile(str(src)).background))
|
||||
src_data = meta["data"]
|
||||
expect0 = src_data[0][:, :, 0:4, :].astype(np.float32).mean(axis=2).astype(np.int16)
|
||||
# int16 (not float32) before .mean(): matches average_rows' own
|
||||
# float64-accumulator behavior for integer input, so this doesn't
|
||||
# drift from what average_rows actually guarantees.
|
||||
expect0 = src_data[0][:, :, 0:4, :].astype(np.int16).mean(axis=2).astype(np.int16)
|
||||
check("first averaged group equals the mean of its 4 source frames",
|
||||
np.array_equal(np.asarray(avg.data[0])[:, :, 0, :], expect0))
|
||||
|
||||
# Remainder handling: 12 frames / 5 -> 2 full groups + 1 partial.
|
||||
dst2 = scratch / "legacy_v4_avg5.sras"
|
||||
subprocess.run([sys.executable, str(repo / "sras_average.py"),
|
||||
str(src), str(dst2), "--n", "5"],
|
||||
capture_output=True, text=True, cwd=repo)
|
||||
proc2 = subprocess.run([sys.executable, str(repo / "sras_average.py"),
|
||||
str(src), str(dst2), "--n", "5"],
|
||||
capture_output=True, text=True, cwd=repo)
|
||||
check("sras_average ran (--n 5)", proc2.returncode == 0,
|
||||
(proc2.stderr or proc2.stdout).strip()[-200:])
|
||||
if dst2.exists():
|
||||
check("partial trailing group kept by default",
|
||||
list(SrasFile(str(dst2)).n_frames) == [3, 3],
|
||||
f"{list(SrasFile(str(dst2)).n_frames)}")
|
||||
dst3 = scratch / "legacy_v4_avg5d.sras"
|
||||
subprocess.run([sys.executable, str(repo / "sras_average.py"),
|
||||
str(src), str(dst3), "--n", "5", "--discard-remainder"],
|
||||
capture_output=True, text=True, cwd=repo)
|
||||
proc3 = subprocess.run([sys.executable, str(repo / "sras_average.py"),
|
||||
str(src), str(dst3), "--n", "5", "--discard-remainder"],
|
||||
capture_output=True, text=True, cwd=repo)
|
||||
check("sras_average ran (--n 5 --discard-remainder)", proc3.returncode == 0,
|
||||
(proc3.stderr or proc3.stdout).strip()[-200:])
|
||||
if dst3.exists():
|
||||
check("--discard-remainder drops the partial group",
|
||||
list(SrasFile(str(dst3)).n_frames) == [2, 2],
|
||||
|
||||
Reference in New Issue
Block a user