diff --git a/src/decoders/trx-wefax/src/decoder.rs b/src/decoders/trx-wefax/src/decoder.rs index a23f546..06c8bc2 100644 --- a/src/decoders/trx-wefax/src/decoder.rs +++ b/src/decoders/trx-wefax/src/decoder.rs @@ -43,11 +43,20 @@ const LINE_CORR_NOISE_THRESHOLD: f32 = 0.2; /// fldigi's line-to-line correlation check for automatic stop. const LINE_CORR_NOISE_LINES: u32 = 30; -/// Minimum image height (lines) to save. Anything shorter is assumed to be a -/// false-positive auto-start (variance detector tripping on tones, noise -/// bursts, or phasing leakage) and discarded silently. A real WEFAX chart is -/// at least several hundred lines long. -const MIN_IMAGE_LINES: u32 = 100; +/// Pearson correlation above which adjacent lines are considered good +/// evidence of real image content. Used to verify unverified auto-starts. +const LINE_CORR_IMAGE_THRESHOLD: f32 = 0.5; + +/// Number of consecutive well-correlated lines that verify an unverified +/// reception (i.e. an auto-start from variance detection). Low enough to +/// engage quickly on real imagery. +const VERIFY_HIGH_CORR_STREAK: u32 = 5; + +/// Maximum number of scan lines the verifier waits for before giving up on +/// an unverified reception. Roughly 20 s at 120 LPM. If no high-correlation +/// streak appears by then, the buffered content is dropped and we return +/// to Idle without saving anything. +const VERIFY_TIMEOUT_LINES: u32 = 40; /// WEFAX decoder output event. #[derive(Debug)] @@ -101,6 +110,14 @@ pub struct WefaxDecoder { /// `LINE_CORR_NOISE_LINES` the decoder auto-finalizes the in-progress /// image (carrier dropped / tx ended without an APT stop tone). low_corr_lines: u32, + /// `true` once the current reception has been confirmed to contain real + /// image content. Set immediately for phasing-driven entries (the APT + /// start tone + phasing pulses already proved the signal); set later + /// by the correlation verifier for variance-driven auto-starts. + verified: bool, + /// Rolling count of consecutive well-correlated lines, used to confirm + /// an unverified reception. + high_corr_streak: u32, /// Current rig dial frequency in Hz (for image filenames). freq_hz: u64, /// Current rig mode name (for image filenames). @@ -128,6 +145,8 @@ impl WefaxDecoder { signal_detect_count: 0, signal_detect_buf: Vec::with_capacity(INTERNAL_RATE as usize / 2), low_corr_lines: 0, + verified: false, + high_corr_streak: 0, freq_hz: 0, mode: String::new(), } @@ -235,7 +254,7 @@ impl WefaxDecoder { .as_millis() as i64, ); self.signal_detect_buf.clear(); - events.push(self.transition_to_receiving(ioc, lpm, 0)); + events.push(self.transition_to_receiving(ioc, lpm, 0, false)); break; } @@ -265,7 +284,7 @@ impl WefaxDecoder { if let Some(ref mut phasing) = self.phasing { if let Some(offset) = phasing.process(&luminance) { - events.push(self.transition_to_receiving(ioc, lpm, offset)); + events.push(self.transition_to_receiving(ioc, lpm, offset, true)); } } } @@ -281,36 +300,66 @@ impl WefaxDecoder { // Feed luminance to line slicer. let mut carrier_lost = false; + let mut verify_failed = false; if let Some(ref mut slicer) = self.slicer { let new_lines = slicer.process(&luminance); for line in new_lines { if let Some(ref mut image) = self.image { - // Line-to-line correlation watchdog: real imagery - // has highly correlated adjacent lines; pure noise - // does not. If correlation stays low for - // `LINE_CORR_NOISE_LINES` consecutive lines, the - // carrier is gone and we finalize (fldigi-style). + // Line-to-line Pearson correlation classifies the + // new line as image-like, noise-like, or flat. + // fldigi-style: real imagery has highly correlated + // adjacent lines; pure noise does not. if let Some(r) = image.correlation_with_last(&line) { - if r < LINE_CORR_NOISE_THRESHOLD { + if r >= LINE_CORR_IMAGE_THRESHOLD { + self.high_corr_streak += 1; + self.low_corr_lines = 0; + if !self.verified + && self.high_corr_streak >= VERIFY_HIGH_CORR_STREAK + { + self.verified = true; + debug!( + lines = image.line_count(), + "WEFAX: reception verified from line correlation" + ); + } + } else if r < LINE_CORR_NOISE_THRESHOLD { self.low_corr_lines += 1; + self.high_corr_streak = 0; trace!( r = format!("{:.3}", r), count = self.low_corr_lines, "WEFAX low line-correlation" ); } else { - self.low_corr_lines = 0; + // Middle zone — reset high streak, hold + // low-corr counter. + self.high_corr_streak = 0; } } // Flat lines (correlation == None) don't advance - // the counter but also don't reset it — an image - // with a solid band surrounded by noise still - // trips the watchdog once the noise resumes. + // either counter — solid bands in real imagery + // shouldn't be scored as noise OR as evidence. image.push_line(line); let count = image.line_count(); - if self.low_corr_lines >= LINE_CORR_NOISE_LINES { + // Unverified timeout: if we got here from a + // variance auto-start and line correlation never + // took hold, the "signal" wasn't real WEFAX. + // Abandon without saving. + if !self.verified && count >= VERIFY_TIMEOUT_LINES { + debug!( + lines = count, + "WEFAX: failed to verify image content — abandoning" + ); + verify_failed = true; + break; + } + + // Carrier-loss watchdog — only active once the + // reception has been verified (otherwise it + // double-counts with the verify timeout). + if self.verified && self.low_corr_lines >= LINE_CORR_NOISE_LINES { debug!( lines = count, "WEFAX: line correlation lost — auto-finalizing image" @@ -342,6 +391,15 @@ impl WefaxDecoder { } } + if verify_failed { + // Drop buffered content without saving — this was a + // false auto-start (tone, noise burst, etc.). + self.image = None; + self.reception_start_ms = None; + self.transition_to_idle(); + return events; + } + if carrier_lost { events.extend(self.finalize_image(ioc, lpm)); self.transition_to_idle(); @@ -380,6 +438,8 @@ impl WefaxDecoder { self.signal_detect_count = 0; self.signal_detect_buf.clear(); self.low_corr_lines = 0; + self.verified = false; + self.high_corr_streak = 0; events } @@ -433,13 +493,24 @@ impl WefaxDecoder { self.state_event("Phasing", ioc, lpm) } - fn transition_to_receiving(&mut self, ioc: u16, lpm: u16, phase_offset: usize) -> WefaxEvent { - debug!(ioc, lpm, phase_offset, "WEFAX: entering receiving"); + fn transition_to_receiving( + &mut self, + ioc: u16, + lpm: u16, + phase_offset: usize, + verified: bool, + ) -> WefaxEvent { + debug!( + ioc, + lpm, phase_offset, verified, "WEFAX: entering receiving" + ); let ppl = WefaxConfig::pixels_per_line(ioc) as usize; self.slicer = Some(LineSlicer::new(lpm, ioc, INTERNAL_RATE, phase_offset)); self.image = Some(ImageAssembler::new(ppl)); self.tone_detector.reset(); self.low_corr_lines = 0; + self.verified = verified; + self.high_corr_streak = 0; self.state = State::Receiving { ioc, lpm }; self.state_event("Receiving", ioc, lpm) } @@ -453,24 +524,15 @@ impl WefaxDecoder { self.signal_detect_count = 0; self.signal_detect_buf.clear(); self.low_corr_lines = 0; + self.verified = false; + self.high_corr_streak = 0; } fn finalize_image(&mut self, ioc: u16, lpm: u16) -> Vec { let mut events = Vec::new(); if let Some(ref image) = self.image { - let lines = image.line_count(); - if lines == 0 { - return events; - } - if lines < MIN_IMAGE_LINES { - debug!( - lines, - min = MIN_IMAGE_LINES, - "WEFAX: discarding short image (likely false auto-start)" - ); - self.image = None; - self.reception_start_ms = None; + if image.line_count() == 0 { return events; }