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:
Thomas Ales [M S E]
2026-08-12 12:16:45 -05:00
parent 2937c017a7
commit 48d560fb00
4 changed files with 404 additions and 13 deletions
+26 -1
View File
@@ -1716,6 +1716,25 @@ class SrasViewerWindow(QMainWindow):
# Batch export view as images
# ------------------------------------------------------------------
def _view_figsize(self) -> tuple[float, float]:
"""The image canvas's current figure size in inches — handed to every
image export so the PNG has the shape the view is being read at.
The canvas draws with aspect="auto" and expands with the window, so
the map's proportions are the widget's, not the data's; a renderer
with its own hard-coded size necessarily distorts the export
relative to the screen.
Inches rather than pixels because that is what Figure takes, and
matplotlib's Qt canvas keeps size_inches in logical units (it scales
figure.dpi by the device pixel ratio and divides physical pixels by
it), so the same window gives the same export on a HiDPI display as
on a standard one. Read at trigger time: a resize mid-batch must not
change the shape of images later in the same run.
"""
w, h = self.image_canvas.figure.get_size_inches()
return (float(w), float(h))
def _on_batch_export_images(self):
if self._job_running(Jobs.BATCH):
return
@@ -1751,6 +1770,7 @@ class SrasViewerWindow(QMainWindow):
highlight_masked=self.chk_highlight_masked.isChecked(),
mode_str=mode_str, colorbar_label=colorbar_label,
mask_color=_MASKED_HIGHLIGHT_COLOR,
figsize=self._view_figsize(),
)
started = self._run_worker(
Jobs.BATCH, worker,
@@ -1858,7 +1878,12 @@ class SrasViewerWindow(QMainWindow):
cmap=self.combo_cmap.currentText(),
apply_bg_sub=self.chk_bg_sub.isChecked(),
dc_threshold_mv=self.spin_threshold_mv.value(),
n_fft=self._current_n_fft(), grating_um=self.spin_grating_um.value())
n_fft=self._current_n_fft(), grating_um=self.spin_grating_um.value(),
# Live, like every other display control here and like
# _stored_fft_image: the exported RF/Velocity map has to be the
# one on screen, floor included.
min_freq_mhz=self.spin_min_freq_mhz.value(),
figsize=self._view_figsize())
started = self._run_worker(
Jobs.EXPORT, worker,
connect=(