177 lines
5.7 KiB
C#
177 lines
5.7 KiB
C#
namespace OcrDaemon;
|
|
|
|
static class SignalProcessing
|
|
{
|
|
/// <summary>
|
|
/// Find the dominant period in a signal using autocorrelation.
|
|
/// Returns (period, score) where score is the autocorrelation strength.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|