Implement row-averaged FFT feature for same-row SNR cleanup
Add row-averaged FFT feature with configurable window size (row_avg_n parameter) for improved signal-to-noise ratio on noisy scans. Includes: - Gaussian-weighted same-row neighbor averaging (never crosses rows) - Masked/renormalized convolution handling for edge cases and masked samples - Cache format v2 with row_avg_n tracking to prevent silent cache mismatches - GUI dialog option for row-average window configuration - Comprehensive tests validating kernel properties, background subtraction invariance, and cache dispatch Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ from .common import (
|
||||
_CSS_BUSY, _axes_extent, _CSS_HINT, _CSS_INFO, _CSS_MUTED, _CSS_WARN, _LEFT_PANEL_W,
|
||||
_RIGHT_PANEL_W, Jobs, _form, _group, _make_dspin, _scroll_panel, _wrap_label,
|
||||
)
|
||||
from .dialogs import FftOptionsDialog, ManualAlignmentDialog
|
||||
from .dialogs import FftOptionsDialog, ManualAlignmentDialog, RowAverageFftOptionsDialog
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main window
|
||||
@@ -73,6 +73,11 @@ class SrasViewerWindow(QMainWindow):
|
||||
except (TypeError, ValueError):
|
||||
pad = 1
|
||||
self._fft_pad_factor: int = max(1, min(256, pad)) # 1 = no padding
|
||||
try:
|
||||
row_avg_n = int(self._settings.value("fft/row_avg_n", 3))
|
||||
except (TypeError, ValueError):
|
||||
row_avg_n = 3
|
||||
self._pending_row_avg_n: int = max(1, min(50, row_avg_n))
|
||||
|
||||
# Convert menu: batch DC/FFT compute-and-store (v6 -> v7)
|
||||
self._batch_errors: list[str] = []
|
||||
@@ -459,6 +464,17 @@ class SrasViewerWindow(QMainWindow):
|
||||
self._batch_fft_act.triggered.connect(lambda: self._on_batch_compute("fft"))
|
||||
convert_menu.addAction(self._batch_fft_act)
|
||||
|
||||
self._batch_fft_rowavg_act = QAction(
|
||||
"Batch Compute Row-A&veraged FFT and Store…", self)
|
||||
self._batch_fft_rowavg_act.setStatusTip(
|
||||
"Select .sras files and compute+store a same-row, distance-weighted "
|
||||
"smoothed FFT peak-frequency image for every angle — improves SNR "
|
||||
"on noisy regions. DC images and raw waveform data are never "
|
||||
"touched; the raw (unsmoothed) FFT is always a recompute away. "
|
||||
"Converts v6 files to v7 in place.")
|
||||
self._batch_fft_rowavg_act.triggered.connect(self._on_batch_compute_row_avg)
|
||||
convert_menu.addAction(self._batch_fft_rowavg_act)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Drag-and-drop
|
||||
# ------------------------------------------------------------------
|
||||
@@ -598,9 +614,12 @@ class SrasViewerWindow(QMainWindow):
|
||||
n_fft = sum(1 for x in s.precomputed_freq_mhz if x is not None)
|
||||
if n_dc or n_fft:
|
||||
bg_note = " (bg-sub)" if s.precomputed_bg_sub else " (no bg-sub)"
|
||||
avg_note = (f", row-averaged n={s.precomputed_row_avg_n}"
|
||||
if s.precomputed_row_avg_n else "")
|
||||
notes.append(
|
||||
f"Cached images: DC {n_dc}/{s.n_angles} angles, "
|
||||
f"FFT {n_fft}/{s.n_angles} angles{bg_note if n_fft else ''} "
|
||||
f"FFT {n_fft}/{s.n_angles} angles{bg_note if n_fft else ''}"
|
||||
f"{avg_note if n_fft else ''} "
|
||||
"— display is instant for cached angles")
|
||||
elif s.version == 7:
|
||||
notes.append("v7 format: no cache blocks stored yet")
|
||||
@@ -640,6 +659,7 @@ class SrasViewerWindow(QMainWindow):
|
||||
can_batch = not self._job_running(Jobs.BATCH)
|
||||
self._batch_dc_act.setEnabled(can_batch)
|
||||
self._batch_fft_act.setEnabled(can_batch)
|
||||
self._batch_fft_rowavg_act.setEnabled(can_batch)
|
||||
|
||||
self._alignment_act.setEnabled(
|
||||
has_file and s.n_angles > 1 and not self._job_running(Jobs.ALIGN))
|
||||
@@ -1178,10 +1198,53 @@ class SrasViewerWindow(QMainWindow):
|
||||
|
||||
self._batch_dc_act.setEnabled(False)
|
||||
self._batch_fft_act.setEnabled(False)
|
||||
self._batch_fft_rowavg_act.setEnabled(False)
|
||||
self._show_progress(
|
||||
Jobs.BATCH, f"Batch computing {label} for {len(paths)} file(s)…",
|
||||
maximum=100)
|
||||
|
||||
def _on_batch_compute_row_avg(self):
|
||||
if self._job_running(Jobs.BATCH):
|
||||
return
|
||||
dlg = RowAverageFftOptionsDialog(
|
||||
self, current_n=self._pending_row_avg_n,
|
||||
current_threshold_mv=self.spin_threshold_mv.value(),
|
||||
pixel_x_mm=self._sras.pixel_x_mm if self._sras is not None else None)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
n, threshold_mv = dlg.get_half_width(), dlg.get_threshold_mv()
|
||||
self._pending_row_avg_n = n
|
||||
self._settings.setValue("fft/row_avg_n", n)
|
||||
|
||||
paths, _ = QFileDialog.getOpenFileNames(
|
||||
self, "Select .sras files to batch-compute Row-Averaged FFT", "",
|
||||
"SRAS files (*.sras);;All files (*)")
|
||||
if not paths:
|
||||
return
|
||||
|
||||
self._batch_errors = []
|
||||
worker = BatchCacheWorker(paths, "fft_rowavg", self.chk_bg_sub.isChecked(),
|
||||
dc_threshold_mv=threshold_mv, row_avg_n=n)
|
||||
started = self._run_worker(
|
||||
Jobs.BATCH, worker,
|
||||
connect=(
|
||||
("progress", lambda pct: self._set_progress(Jobs.BATCH, pct)),
|
||||
("file_done", self._on_batch_file_done),
|
||||
("finished", lambda p=paths: self._on_batch_finished(p)),
|
||||
),
|
||||
on_done=self._after_batch,
|
||||
)
|
||||
if not started:
|
||||
return # a second trigger snuck in while a dialog was open
|
||||
|
||||
self._batch_dc_act.setEnabled(False)
|
||||
self._batch_fft_act.setEnabled(False)
|
||||
self._batch_fft_rowavg_act.setEnabled(False)
|
||||
self._show_progress(
|
||||
Jobs.BATCH,
|
||||
f"Batch computing row-averaged FFT (n={n}, threshold={threshold_mv:.1f} mV) "
|
||||
f"for {len(paths)} file(s)…", maximum=100)
|
||||
|
||||
def _on_batch_file_done(self, path: str, err: str):
|
||||
if err:
|
||||
self._batch_errors.append(f"{Path(path).name} — {err}")
|
||||
@@ -1209,6 +1272,7 @@ class SrasViewerWindow(QMainWindow):
|
||||
def _after_batch(self):
|
||||
self._batch_dc_act.setEnabled(True)
|
||||
self._batch_fft_act.setEnabled(True)
|
||||
self._batch_fft_rowavg_act.setEnabled(True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Fusion: angle alignment
|
||||
|
||||
Reference in New Issue
Block a user