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:
Thomas Ales [M S E]
2026-08-10 18:30:00 -05:00
parent b0bfe6e8c6
commit 6976cbf767
7 changed files with 507 additions and 43 deletions
+9 -4
View File
@@ -54,8 +54,13 @@ def average_rows(block: np.ndarray, n: int, discard_remainder: bool) -> np.ndarr
"""Average every N frames of one angle's (n_rows, n_ch, n_frames, spf)
block. Returns int16 of shape (n_rows, n_ch, n_out, spf).
Averaging is done in float32 and rounded on cast, matching numpy's mean
followed by an int16 cast in the original implementation.
Cast to native int16 (not float32) before calling .mean(): numpy's mean()
uses a float64 accumulator by default for integer input, matching the
original implementation exactly (which kept the whole file as int16 and
called .mean() directly). A float32 cast here would use a float32
accumulator instead — for large group sizes that can round the sum
differently than float64 and, after the int16 cast below, occasionally
land on a value 1 ADC count away from the original tool's output.
"""
n_frames = block.shape[2]
n_full = n_frames // n
@@ -63,11 +68,11 @@ def average_rows(block: np.ndarray, n: int, discard_remainder: bool) -> np.ndarr
parts = []
if n_full:
full = block[:, :, :n_full * n, :].astype(np.float32)
full = block[:, :, :n_full * n, :].astype(np.int16)
full = full.reshape(block.shape[0], block.shape[1], n_full, n, block.shape[3])
parts.append(full.mean(axis=3).astype(np.int16))
if remainder and not discard_remainder:
tail = block[:, :, n_full * n:, :].astype(np.float32)
tail = block[:, :, n_full * n:, :].astype(np.int16)
parts.append(tail.mean(axis=2, keepdims=True).astype(np.int16))
if not parts: