diff --git a/rfg_adc_plotter/gui/pyqtgraph_backend.py b/rfg_adc_plotter/gui/pyqtgraph_backend.py index db94ae6..13cdf9c 100644 --- a/rfg_adc_plotter/gui/pyqtgraph_backend.py +++ b/rfg_adc_plotter/gui/pyqtgraph_backend.py @@ -170,6 +170,30 @@ def resolve_visible_aux_curves(aux_curves: SweepAuxCurves, enabled: bool) -> Swe return aux_1_arr, aux_2_arr +def compute_background_subtracted_bscan_levels( + disp_fft_lin: np.ndarray, + disp_fft: np.ndarray, +) -> Optional[Tuple[float, float]]: + """Pick robust color levels from positive residuals after background subtraction.""" + if disp_fft_lin.size == 0 or disp_fft.size == 0: + return None + positive_mask = np.isfinite(disp_fft_lin) & (disp_fft_lin > 0.0) + if np.count_nonzero(positive_mask) < 2: + return None + vals = np.asarray(disp_fft[positive_mask], dtype=np.float64).reshape(-1) + vals = vals[np.isfinite(vals)] + if vals.size < 2: + return None + try: + vmin = float(np.nanpercentile(vals, 15.0)) + vmax = float(np.nanpercentile(vals, 99.7)) + except Exception: + return None + if not (np.isfinite(vmin) and np.isfinite(vmax)) or vmin >= vmax: + return None + return (vmin, vmax) + + def run_pyqtgraph(args) -> None: """Start the PyQtGraph GUI.""" peak_calibrate_mode = bool(getattr(args, "calibrate", False)) @@ -1330,15 +1354,8 @@ def run_pyqtgraph(args) -> None: levels = None if active_background is not None: - try: - p5 = float(np.nanpercentile(disp_fft, 5)) - p95 = float(np.nanpercentile(disp_fft, 95)) - span = max(abs(p5), abs(p95)) - if np.isfinite(span) and span > 0.0: - levels = (-span, span) - except Exception: - levels = None - else: + levels = compute_background_subtracted_bscan_levels(disp_fft_lin, disp_fft) + if levels is None: try: mean_spec = np.nanmean(disp_fft, axis=1) vmin_v = float(np.nanmin(mean_spec)) diff --git a/tests/test_processing.py b/tests/test_processing.py index 283aef4..5802ca0 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -9,6 +9,7 @@ from rfg_adc_plotter.constants import FFT_LEN, SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MI from rfg_adc_plotter.gui.pyqtgraph_backend import ( apply_working_range, apply_working_range_to_aux_curves, + compute_background_subtracted_bscan_levels, resolve_visible_aux_curves, ) from rfg_adc_plotter.processing.calibration import ( @@ -29,6 +30,7 @@ from rfg_adc_plotter.processing.fft import ( compute_distance_axis, compute_fft_mag_row, compute_fft_row, + fft_mag_to_db, ) from rfg_adc_plotter.processing.normalization import ( build_calib_envelopes, @@ -180,6 +182,30 @@ class ProcessingTests(unittest.TestCase): self.assertTrue(np.allclose(visible[0], aux[0])) self.assertTrue(np.allclose(visible[1], aux[1])) + def test_background_subtracted_bscan_levels_ignore_zero_floor(self): + disp_fft_lin = np.zeros((4, 8), dtype=np.float32) + disp_fft_lin[1, 2:6] = np.asarray([0.05, 0.1, 0.5, 2.0], dtype=np.float32) + disp_fft_lin[2, 1:6] = np.asarray([0.08, 0.2, 0.7, 3.0, 9.0], dtype=np.float32) + disp_fft = fft_mag_to_db(disp_fft_lin) + + levels = compute_background_subtracted_bscan_levels(disp_fft_lin, disp_fft) + + self.assertIsNotNone(levels) + positive_vals = disp_fft[disp_fft_lin > 0.0] + self.assertAlmostEqual(levels[0], float(np.nanpercentile(positive_vals, 15.0)), places=5) + self.assertAlmostEqual(levels[1], float(np.nanpercentile(positive_vals, 99.7)), places=5) + zero_floor = disp_fft[disp_fft_lin == 0.0] + self.assertLess(float(np.nanmax(zero_floor)), levels[0]) + + def test_background_subtracted_bscan_levels_fallback_when_residuals_too_sparse(self): + disp_fft_lin = np.zeros((3, 4), dtype=np.float32) + disp_fft_lin[1, 2] = 1.0 + disp_fft = fft_mag_to_db(disp_fft_lin) + + levels = compute_background_subtracted_bscan_levels(disp_fft_lin, disp_fft) + + self.assertIsNone(levels) + def test_fft_helpers_return_expected_shapes(self): sweep = np.sin(np.linspace(0.0, 4.0 * np.pi, 128)).astype(np.float32) freqs = np.linspace(3.3, 14.3, 128, dtype=np.float64)