Make both batch image exports reproduce the view on screen
Two strands, committed together because they touch the same four files and answer the same question: an exported PNG must be the image the user was looking at when they triggered the export. Dropped view settings (bug fix): - BatchExportWorker forwarded the DC threshold, bg-sub, pad and row- averaging to compute_rf_image but never min_freq_mhz, so every RF and Velocity map was exported with no min peak frequency floor. The pixels the floor exists to reject came back at their pre-floor peaks. - That also decided *which source* the image came from: requesting floor 0 makes a stored v7 cache look like a match (cache_mismatch_reasons only rejects a higher stored floor), so a batch-computed file exported the cached peak-frequency block verbatim - numbers from an earlier Batch Compute run, while the screen showed that same cache re-masked against the live floor. The symptom was a newly loaded file exporting a stale-looking velocity map, since the floor is a display control that survives a load while the window's own FFT cache does not. - export_view_image had the same class of defect with a different parameter: it dropped row_avg_n, so a file whose stored cache is row- averaged displayed from the store but exported as a full raw recompute - a different, and much slower, image. It now reads sras.precomputed_row_avg_n per file, mirroring _stored_fft_image and _start_compute, and derived per file for the same reason n_fft is. Export at the live canvas size: - Both exporters now take the canvas's current figure size instead of a hard-coded 7x5. draw_view_image uses aspect="auto", so the figure box is what sets the map's proportions - a view sized wide on screen was being squeezed into a different shape on disk. Read at trigger time, in inches, so a resize mid-batch cannot change images later in the same run and a HiDPI display exports like a standard one. - sanitize_figsize clamps a degenerate size (collapsed splitter pane, minimized window): a wrong-looking aspect ratio must never be the reason a batch loses an image. Tests: BatchExportWorker had no coverage at all. Four new tests cover the floor being applied, a stored cache never being served unfloored, the menu-to-worker wiring, and the row-averaged cache case; each was verified to fail against the unfixed code. 166 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+29
-7
@@ -22,7 +22,7 @@ import sras_compute as compute
|
||||
from sras_align_export import write_aligned_sras
|
||||
from sras_compute import cache_file, compute_rf_image, dc_image_mv
|
||||
from sras_format import CH1_IDX, CH3_IDX, CH4_IDX, SrasFile
|
||||
from sras_render import export_view_image
|
||||
from sras_render import export_view_image, sanitize_figsize
|
||||
|
||||
# Concurrency caps. Batch conversion runs one process per file, and each of
|
||||
# those processes threads internally, so the two must be divided rather than
|
||||
@@ -344,11 +344,16 @@ class ExportChannel:
|
||||
|
||||
def _render_map_png(img: np.ndarray, extent: list[float], *, cmap: str,
|
||||
vmin: float, vmax: float, title: str, colorbar_label: str,
|
||||
out_path: str):
|
||||
out_path: str, figsize: tuple[float, float] | None = None):
|
||||
"""Render one map image to *out_path* with a fixed vmin/vmax, using a
|
||||
headless Agg canvas so this never touches the GUI thread's interactive
|
||||
matplotlib backend. Layout mirrors ImageCanvas.show_image."""
|
||||
fig = Figure(figsize=(7, 5), tight_layout=True)
|
||||
matplotlib backend. Layout mirrors ImageCanvas.show_image.
|
||||
|
||||
*figsize* is the live canvas's size in inches, so the PNG comes out at
|
||||
the shape the view was being read at instead of a fixed 7x5 -- with
|
||||
aspect="auto" below, the figure box is what sets the map's proportions.
|
||||
None falls back to sras_render.DEFAULT_FIGSIZE."""
|
||||
fig = Figure(figsize=sanitize_figsize(figsize), tight_layout=True)
|
||||
FigureCanvasAgg(fig)
|
||||
ax = fig.add_subplot(111)
|
||||
im = ax.imshow(img, aspect="auto", origin="upper", extent=extent,
|
||||
@@ -372,6 +377,18 @@ class BatchExportWorker(CancellableWorker):
|
||||
not an arbitrary batch of files, so there is no need to reopen it in a
|
||||
subprocess the way BatchCacheWorker does.
|
||||
|
||||
Every setting that decides what an FFT-derived pixel *is* has to arrive
|
||||
here explicitly, because compute_rf_image defaults each one to "off" and
|
||||
an omitted argument is therefore not a no-op -- it silently exports a
|
||||
different image than the one on screen. *min_freq_mhz* is the one with
|
||||
teeth: dropping it does not just skip a mask, it also makes this file's
|
||||
stored cache (written by Batch Compute FFT at some earlier floor) look
|
||||
like a match, so the export hands back the pre-floor peaks the user
|
||||
raised the floor to get rid of. It mirrors
|
||||
SrasViewerWindow._stored_fft_image, which takes the floor live for the
|
||||
same reason -- re-masking a stored image against a *higher* floor is
|
||||
free, so the displayed map always honors the spin box.
|
||||
|
||||
Emits progress(int) (0-100 over all angle x channel pairs), file_done(str,
|
||||
str) (output path, error message or ""), and finished() -- same shape as
|
||||
BatchCacheWorker.
|
||||
@@ -383,7 +400,9 @@ class BatchExportWorker(CancellableWorker):
|
||||
def __init__(self, sras: SrasFile, channels: list[ExportChannel],
|
||||
output_dir: str, prefix: str, *, cmap: str,
|
||||
apply_bg_sub: bool, dc_threshold_mv: float,
|
||||
n_fft: int | None, grating_um: float):
|
||||
n_fft: int | None, grating_um: float,
|
||||
min_freq_mhz: float = 0.0,
|
||||
figsize: tuple[float, float] | None = None):
|
||||
super().__init__()
|
||||
self._sras = sras
|
||||
self._channels = channels
|
||||
@@ -394,6 +413,8 @@ class BatchExportWorker(CancellableWorker):
|
||||
self._dc_threshold_mv = dc_threshold_mv
|
||||
self._n_fft = n_fft
|
||||
self._grating_um = grating_um
|
||||
self._min_freq_mhz = min_freq_mhz
|
||||
self._figsize = figsize
|
||||
|
||||
def _angle_extent(self, angle_idx: int) -> list[float]:
|
||||
s = self._sras
|
||||
@@ -423,7 +444,8 @@ class BatchExportWorker(CancellableWorker):
|
||||
freq_mhz = compute_rf_image(
|
||||
s, angle_idx, dc_threshold_mv=self._dc_threshold_mv,
|
||||
apply_bg_sub=self._apply_bg_sub, n_fft=self._n_fft,
|
||||
should_stop=self._stopped, row_avg_n=s.precomputed_row_avg_n)
|
||||
should_stop=self._stopped, row_avg_n=s.precomputed_row_avg_n,
|
||||
min_freq_mhz=self._min_freq_mhz)
|
||||
|
||||
for channel in self._channels:
|
||||
if self._stop:
|
||||
@@ -444,7 +466,7 @@ class BatchExportWorker(CancellableWorker):
|
||||
img, extent, cmap=self._cmap,
|
||||
vmin=channel.vmin, vmax=channel.vmax,
|
||||
title=title, colorbar_label=colorbar_label,
|
||||
out_path=str(out_path))
|
||||
out_path=str(out_path), figsize=self._figsize)
|
||||
self.file_done.emit(str(out_path), "")
|
||||
except Exception as exc:
|
||||
self.file_done.emit(str(out_path), str(exc))
|
||||
|
||||
Reference in New Issue
Block a user