namespace OcrDaemon; static class SignalProcessing { /// /// Find the dominant period in a signal using autocorrelation. /// Returns (period, score) where score is the autocorrelation strength. /// public static (int period, double score) FindPeriodWithScore(double[] signal, int minPeriod, int maxPeriod) { int n = signal.Length; if (n < minPeriod * 3) return (-1, 0); double mean = signal.Average(); double variance = 0; for (int i = 0; i < n; i++) variance += (signal[i] - mean) * (signal[i] - mean); if (variance < 1.0) return (-1, 0); int maxLag = Math.Min(maxPeriod, n / 3); double[] ac = new double[maxLag + 1]; for (int lag = minPeriod; lag <= maxLag; lag++) { double sum = 0; for (int i = 0; i < n - lag; i++) sum += (signal[i] - mean) * (signal[i + lag] - mean); ac[lag] = sum / variance; } // Find the first significant peak — this is the fundamental period. // Using "first" avoids picking harmonics (2x, 3x) or unrelated larger patterns. for (int lag = minPeriod + 1; lag < maxLag; lag++) { if (ac[lag] > 0.01 && ac[lag] >= ac[lag - 1] && ac[lag] >= ac[lag + 1]) return (lag, ac[lag]); } return (-1, 0); } /// /// Find contiguous segments where values are ABOVE threshold. /// Used to find grid panel regions by density of very dark pixels. /// Allows brief gaps (up to 5px) to handle grid borders. /// public static List<(int start, int end)> FindDarkDensitySegments(double[] profile, double threshold, int minLength) { var segments = new List<(int start, int end)>(); int n = profile.Length; int curStart = -1; int maxGap = 5; int gapCount = 0; for (int i = 0; i < n; i++) { if (profile[i] >= threshold) { if (curStart < 0) curStart = i; gapCount = 0; } else { if (curStart >= 0) { gapCount++; if (gapCount > maxGap) { int end = i - gapCount; if (end - curStart >= minLength) segments.Add((curStart, end)); curStart = -1; gapCount = 0; } } } } if (curStart >= 0) { int end = gapCount > 0 ? n - gapCount : n; if (end - curStart >= minLength) segments.Add((curStart, end)); } return segments; } /// /// Find the extent of the grid in a 1D profile using local autocorrelation /// at the specific detected period. Only regions where the signal actually /// repeats at the given period will score high — much more precise than variance. /// public static (int start, int end) FindGridExtent(double[] signal, int period) { int n = signal.Length; int halfWin = period * 2; // window radius: 2 periods each side if (n < halfWin * 2 + period) return (-1, -1); // Compute local AC at the specific lag=period in a sliding window double[] localAc = new double[n]; for (int center = halfWin; center < n - halfWin; center++) { int wStart = center - halfWin; int wEnd = center + halfWin; int count = wEnd - wStart; // Local mean double sum = 0; for (int i = wStart; i < wEnd; i++) sum += signal[i]; double mean = sum / count; // Local variance double varSum = 0; for (int i = wStart; i < wEnd; i++) varSum += (signal[i] - mean) * (signal[i] - mean); if (varSum < 1.0) continue; // AC at the specific lag=period double acSum = 0; for (int i = wStart; i < wEnd - period; i++) acSum += (signal[i] - mean) * (signal[i + period] - mean); localAc[center] = Math.Max(0, acSum / varSum); } // Find the longest contiguous run above threshold double maxAc = 0; for (int i = 0; i < n; i++) if (localAc[i] > maxAc) maxAc = localAc[i]; if (maxAc < 0.02) return (-1, -1); double threshold = maxAc * 0.25; int bestStart = -1, bestEnd = -1, bestLen = 0; int curStartPos = -1; for (int i = 0; i < n; i++) { if (localAc[i] > threshold) { if (curStartPos < 0) curStartPos = i; } else { if (curStartPos >= 0) { int len = i - curStartPos; if (len > bestLen) { bestLen = len; bestStart = curStartPos; bestEnd = i; } curStartPos = -1; } } } // Handle run extending to end of signal if (curStartPos >= 0) { int len = n - curStartPos; if (len > bestLen) { bestStart = curStartPos; bestEnd = n; } } if (bestStart < 0) return (-1, -1); // Small extension to include cell borders at edges bestStart = Math.Max(0, bestStart - period / 4); bestEnd = Math.Min(n - 1, bestEnd + period / 4); return (bestStart, bestEnd); } }