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:
+52
-1
@@ -22,6 +22,36 @@ from matplotlib.figure import Figure
|
||||
from sras_compute import compute_rf_image, dc_image_mv
|
||||
from sras_format import CH4_IDX, CH_NAMES, SrasFile, _axes_extent
|
||||
|
||||
# Figure size (inches) to fall back on when a caller has no live view to
|
||||
# match: the size ImageCanvas is constructed at, so a headless render with
|
||||
# no canvas behind it still looks like the viewer's starting layout.
|
||||
DEFAULT_FIGSIZE = (7.0, 5.0)
|
||||
|
||||
# The canvas size travels from the GUI as-is, so it can be degenerate (a
|
||||
# collapsed splitter pane, a window minimized mid-batch) or, on a very wide
|
||||
# multi-monitor window, big enough that width * dpi approaches matplotlib's
|
||||
# 2**16 pixel limit. Clamp rather than fail: an export's aspect ratio is a
|
||||
# presentation detail and must never be the reason a batch loses an image.
|
||||
_MIN_FIG_IN = 1.0
|
||||
_MAX_FIG_IN = 100.0
|
||||
|
||||
|
||||
def sanitize_figsize(figsize) -> tuple[float, float]:
|
||||
"""(width, height) in inches, clamped to something renderable.
|
||||
|
||||
*figsize* is None (use DEFAULT_FIGSIZE) or any 2-sequence of numbers --
|
||||
including the float64 pair Figure.get_size_inches returns, which is how
|
||||
the GUI hands over the live canvas's current size.
|
||||
"""
|
||||
try:
|
||||
w, h = float(figsize[0]), float(figsize[1])
|
||||
except (TypeError, ValueError, IndexError, KeyError):
|
||||
return DEFAULT_FIGSIZE
|
||||
if not (np.isfinite(w) and np.isfinite(h)):
|
||||
return DEFAULT_FIGSIZE
|
||||
return (min(max(w, _MIN_FIG_IN), _MAX_FIG_IN),
|
||||
min(max(h, _MIN_FIG_IN), _MAX_FIG_IN))
|
||||
|
||||
|
||||
def draw_view_image(ax, fig, img: np.ndarray, extent: list[float], cmap,
|
||||
vmin: float, vmax: float, xlabel: str, ylabel: str,
|
||||
@@ -67,6 +97,7 @@ def export_view_image(path: str, *, out_dir: str, angle_idx: int, ch_idx: int,
|
||||
highlight_masked: bool, mode_str: str,
|
||||
colorbar_label: str, mask_color: str = "magenta",
|
||||
max_workers: int | None = None,
|
||||
figsize: tuple[float, float] | None = None,
|
||||
dpi: int = 150) -> tuple[str, str]:
|
||||
"""One file's contribution to Batch Export View as Images: renders
|
||||
(angle_idx, ch_idx) at the given display settings to a PNG under
|
||||
@@ -80,11 +111,30 @@ def export_view_image(path: str, *, out_dir: str, angle_idx: int, ch_idx: int,
|
||||
own data, not a cache conversion, so any version SrasFile can open
|
||||
works, with no v6/v7 precondition.
|
||||
|
||||
*figsize* is the on-screen ImageCanvas's current size in inches, so the
|
||||
PNG carries the aspect ratio the view was actually being read at. It
|
||||
matters more here than it would for a plot with fixed data aspect:
|
||||
draw_view_image uses aspect="auto", so the image stretches to whatever
|
||||
box it is given -- rendering a view the user has sized wide into a
|
||||
hard-coded 7x5 squeezes the map into a different shape than the one
|
||||
they judged it by. Passing the full size, not just the ratio, also
|
||||
keeps titles, tick labels and the colorbar in the same proportion to
|
||||
the map as on screen; *dpi* alone then sets the output resolution.
|
||||
|
||||
*pad_factor* (not n_fft) travels across files deliberately: n_fft
|
||||
depends on samples_per_frame, which can differ between files in the
|
||||
same batch, so n_fft is derived per file, here, from *this* file's own
|
||||
value -- the same reason sras_compute.cache_file does the same thing.
|
||||
|
||||
row_avg_n is per file for the same reason and so is not a parameter at
|
||||
all: it comes from *this* file's stored cache, mirroring
|
||||
SrasViewerWindow._stored_fft_image and _start_compute, which both ask at
|
||||
the window size the store was written at. Leaving it at compute_rf_image's
|
||||
"raw per-pixel" default would make a row-averaged cache a mismatch, so a
|
||||
file the viewer displays from its store would export as a full raw
|
||||
recompute instead -- a different (and much slower) image than the one the
|
||||
batch was triggered to reproduce.
|
||||
|
||||
Returns (error, out_name). error is "" on success. out_name is the
|
||||
filename this call targeted -- set as soon as it's known, even on most
|
||||
failures -- so the caller can flag same-stem collisions across the
|
||||
@@ -105,6 +155,7 @@ def export_view_image(path: str, *, out_dir: str, angle_idx: int, ch_idx: int,
|
||||
freq = compute_rf_image(
|
||||
sras, angle_idx, dc_threshold_mv=dc_threshold_mv,
|
||||
apply_bg_sub=apply_bg_sub, n_fft=n_fft,
|
||||
row_avg_n=sras.precomputed_row_avg_n,
|
||||
min_freq_mhz=min_freq_mhz, max_workers=max_workers)
|
||||
img = freq * grating_um if is_velocity else freq
|
||||
else:
|
||||
@@ -141,7 +192,7 @@ def export_view_image(path: str, *, out_dir: str, angle_idx: int, ch_idx: int,
|
||||
title = (f"{CH_NAMES[ch_idx]} | {mode_str} | "
|
||||
f"{sras.angles_deg[angle_idx]:.1f}°")
|
||||
|
||||
fig = Figure(figsize=(7, 5), tight_layout=True)
|
||||
fig = Figure(figsize=sanitize_figsize(figsize), tight_layout=True)
|
||||
FigureCanvasAgg(fig) # Agg-only: never registered with pyplot
|
||||
ax = fig.add_subplot(111)
|
||||
draw_view_image(ax, fig, display_img, extent, cmap, v0, v1,
|
||||
|
||||
Reference in New Issue
Block a user