Add min peak frequency floor (CACH v4) and Batch Export View as Images
Two strands of in-progress work, committed together because they overlap in sras_workers.py and main_window.py. Min peak frequency floor: - CACH tail bumped to version 4, adding u32 min_freq_khz provenance in fixed-point kHz (a float32 20.1 reads back as 20.10000038 and would report a spurious mismatch forever). v1-v3 tails read as no floor. - Stored FFT caches are accepted when the reader's floor is at or above the stored one, since a higher floor is re-applicable by masking. - Floor plumbed through compute_rf_image, BatchCacheWorker and the viewer. Batch Export View as Images: - New sras_render.py holds draw_view_image, shared by the Qt canvas and the headless exporter so a PNG cannot drift from what the GUI shows. Deliberately Qt-free so it is importable in a pool subprocess. - BatchExportImagesWorker renders the current view settings across many files, process-pooled with an inline fallback, reporting per-file output names so the caller can flag same-stem collisions. - _axes_extent extracted into sras_format for both render paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+31
-20
@@ -12,6 +12,7 @@ from PyQt6.QtGui import QKeyEvent
|
||||
from PyQt6.QtWidgets import QSizePolicy
|
||||
|
||||
from sras_format import CH1_IDX, CH3_IDX, CH4_IDX, CH_NAMES, SrasFile, adc_to_mv
|
||||
from sras_render import draw_view_image
|
||||
|
||||
def count_colormap(n_angles: int):
|
||||
"""(cmap, norm, ticks) for an integer "how many angles cover this pixel"
|
||||
@@ -182,24 +183,12 @@ class ImageCanvas(FigureCanvasQTAgg):
|
||||
self._extent = extent
|
||||
self._img_shape = img.shape
|
||||
|
||||
if bad_color is not None:
|
||||
cmap = (cmap if hasattr(cmap, "with_extremes")
|
||||
else mpl.colormaps[cmap]).with_extremes(bad=bad_color)
|
||||
|
||||
kw = ({"norm": norm} if norm is not None
|
||||
else {"vmin": vmin, "vmax": vmax})
|
||||
im = self.ax.imshow(
|
||||
img, aspect="auto", origin="upper",
|
||||
extent=extent, cmap=cmap, interpolation="nearest", **kw,
|
||||
)
|
||||
cb = self.figure.colorbar(im, ax=self.ax, fraction=0.046, pad=0.04,
|
||||
ticks=cb_ticks)
|
||||
if colorbar_label:
|
||||
cb.set_label(colorbar_label)
|
||||
|
||||
self.ax.set_xlabel(xlabel)
|
||||
self.ax.set_ylabel(ylabel)
|
||||
self.ax.set_title(title)
|
||||
# Shared with the headless batch image-export worker (sras_render.py)
|
||||
# so an exported PNG can never quietly drift from what this canvas
|
||||
# shows on screen for the same settings.
|
||||
draw_view_image(self.ax, self.figure, img, extent, cmap, vmin, vmax,
|
||||
xlabel, ylabel, title, colorbar_label, cb_ticks, norm,
|
||||
bad_color)
|
||||
|
||||
# Re-draw the ROI (if any) on top of the fresh image so it persists
|
||||
# unchanged across mode / angle / channel switches.
|
||||
@@ -440,13 +429,22 @@ class WaveformCanvas(FigureCanvasQTAgg):
|
||||
|
||||
def show_rf_waveform(self, sras: SrasFile, angle_idx: int,
|
||||
row_idx: int, frame_idx: int,
|
||||
apply_bg_sub: bool = True):
|
||||
apply_bg_sub: bool = True,
|
||||
min_freq_mhz: float = 0.0):
|
||||
"""CH1 RF: time-domain + FFT spectrum.
|
||||
|
||||
If apply_bg_sub is True and sras.background is not None, the background
|
||||
waveform is overlaid on the time-domain plot and the FFT is computed
|
||||
on the subtracted signal. The unsubtracted FFT is also shown faintly
|
||||
for comparison.
|
||||
|
||||
*min_freq_mhz* > 0 restricts the labeled peak to bins at or above
|
||||
it — the same floor the image's peak search uses, so the label
|
||||
explains the map pixel instead of contradicting it — and shades the
|
||||
excluded band on the spectrum. The spectrum curves themselves stay
|
||||
complete (they are the evidence for choosing the floor). The peak
|
||||
can still legitimately differ from a padded or row-averaged map:
|
||||
this panel is always a single waveform at natural resolution.
|
||||
"""
|
||||
data = sras.data[angle_idx]
|
||||
waveform = data[row_idx, CH1_IDX, frame_idx, :].astype(np.float32)
|
||||
@@ -487,7 +485,20 @@ class WaveformCanvas(FigureCanvasQTAgg):
|
||||
# FFT of the (possibly subtracted) waveform
|
||||
power_sub = np.abs(np.fft.rfft(waveform_plot)) ** 2
|
||||
power_sub[0] = 0.0
|
||||
peak_mhz = f_mhz[int(np.argmax(power_sub))]
|
||||
# First bin at or above the floor, exactly as the image peak search
|
||||
# picks it (bin 0 always excluded). If the floor excludes every bin,
|
||||
# fall back to the unrestricted peak rather than indexing past the
|
||||
# end — the label is informational, not a mask.
|
||||
lo = max(1, int(np.searchsorted(f_mhz, min_freq_mhz)))
|
||||
if lo < len(power_sub):
|
||||
peak_mhz = f_mhz[lo + int(np.argmax(power_sub[lo:]))]
|
||||
else:
|
||||
peak_mhz = f_mhz[int(np.argmax(power_sub))]
|
||||
|
||||
if min_freq_mhz > 0.0:
|
||||
self.ax_right.axvspan(0, min_freq_mhz, color="#888888",
|
||||
alpha=0.15, zorder=0,
|
||||
label=f"< {min_freq_mhz:g} MHz excluded")
|
||||
|
||||
if bg is not None:
|
||||
# Also show the unsubtracted FFT for reference
|
||||
|
||||
Reference in New Issue
Block a user