From f7fad076c28bd5db43b77ec9a5d2d31a01049a04 Mon Sep 17 00:00:00 2001 From: Smittix Date: Thu, 19 Feb 2026 11:04:06 +0000 Subject: [PATCH] fix: Expand Scottie sync deviation search window to fix under-correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The slant correction was severely under-correcting because bwd=50 caused the sync deviation measurements to saturate after only ~25 lines (for a 2-sample/line SDR clock drift). Lines 25-256 all reported deviation=-50, pulling the linear regression slope toward zero. Increase bwd and fwd to 800 samples each — sufficient to track cumulative drift from up to ~±200 ppm SDR clock offset across the full 256-line image. Also use a full-sync-length (432-sample) Goertzel window instead of 1/3 length, giving ~111 Hz frequency resolution to cleanly separate the 1200 Hz sync tone from 1500 Hz pixel data. Search is stepped at 5 samples (~0.1 ms) for efficiency, keeping the goertzel_batch batch size at ~320 windows/line. Co-Authored-By: Claude Sonnet 4.6 --- utils/sstv/image_decoder.py | 39 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/utils/sstv/image_decoder.py b/utils/sstv/image_decoder.py index f4ddd88..d60c8cc 100644 --- a/utils/sstv/image_decoder.py +++ b/utils/sstv/image_decoder.py @@ -246,21 +246,33 @@ class SSTVImageDecoder: # slant correction without touching pos or consumed — # so a noisy/false measurement never corrupts the decode. r_samples = self._channel_samples[-1] - bwd = min(50, pos) - fwd = max(0, len(self._buffer) - pos - - self._sync_samples - self._porch_samples - - r_samples) - fwd = min(fwd, self._sync_samples) + # Large window to cover cumulative SDR clock drift over + # the full image. bwd=50 saturates after ~25 lines for + # a 2-sample/line drift; 800 samples covers ±200 ppm. + bwd = min(800, pos) + remaining = len(self._buffer) - pos + fwd = min(800, max( + 0, + remaining - self._sync_samples + - self._porch_samples - r_samples)) deviation: float | None = None if bwd + fwd > 0: region_start = pos - bwd sync_region = self._buffer[ region_start: pos + self._sync_samples + fwd] - win = max(20, self._sync_samples // 3) - n_win = len(sync_region) - win + 1 - if n_win > 0: - windows = np.lib.stride_tricks.sliding_window_view( - sync_region, win) + # Full-sync-length window gives best freq resolution + # (~111 Hz at 48 kHz) to cleanly separate 1200 Hz + # sync from 1500 Hz pixel data. + win = self._sync_samples + n_raw = len(sync_region) - win + 1 + if n_raw > 0: + # Step 5 samples (~0.1 ms) — enough resolution + # for pixel-level drift, keeps batch size small. + step = 5 + all_windows = ( + np.lib.stride_tricks.sliding_window_view( + sync_region, win)) + windows = all_windows[::step] energies = goertzel_batch( windows, np.array([FREQ_SYNC, FREQ_BLACK]), @@ -269,9 +281,10 @@ class SSTVImageDecoder: black_e = energies[:, 1] valid_mask = sync_e > black_e * 2 if valid_mask.any(): - fine_best = int( - np.argmax(np.where(valid_mask, sync_e, 0.0))) - deviation = float(fine_best - bwd) + fine_idx = int( + np.argmax( + np.where(valid_mask, sync_e, 0.0))) + deviation = float(fine_idx * step - bwd) self._sync_deviations.append(deviation) pos += self._sync_samples + self._porch_samples elif self._separator_samples > 0: