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);
}
}