histogram implemented
This commit is contained in:
@@ -5,8 +5,9 @@ namespace LindtLeerformPlugin.Services;
|
||||
|
||||
public class LeerformPatternRecognitionService
|
||||
{
|
||||
private static readonly Scalar GoodColor = new(0, 255, 0); // BGR Green
|
||||
private static readonly Scalar BadColor = new(0, 0, 255); // BGR Red
|
||||
private static readonly Scalar GoodColor = new(0, 255, 0); // BGR Green
|
||||
private static readonly Scalar BadColor = new(0, 0, 255); // BGR Red
|
||||
private static readonly Scalar OverrideColor = new(0, 255, 255); // BGR Yellow
|
||||
|
||||
public IReadOnlyList<CellRegion> ComputeCells(CellPattern pattern)
|
||||
{
|
||||
@@ -73,6 +74,11 @@ public class LeerformPatternRecognitionService
|
||||
return mask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate histogram pooled across every cell in the pattern. Used only at calibration
|
||||
/// time to seed the default average spline; not persisted in the recipe. Note that pooling
|
||||
/// weights cells by their pixel count rather than averaging per-cell histograms.
|
||||
/// </summary>
|
||||
public float[] ComputeReferenceHistogram(Mat bgrImage, CellPattern pattern, int[]? bins = null)
|
||||
{
|
||||
bins ??= [5, 5, 5];
|
||||
@@ -88,29 +94,67 @@ public class LeerformPatternRecognitionService
|
||||
var imageSize = new Size(bgrImage.Cols, bgrImage.Rows);
|
||||
var results = new List<CellResult>(cells.Count);
|
||||
|
||||
using var refHist = ArrayToHistogram(recipe.ReferenceHistogram, bins);
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
float[]? spline = null;
|
||||
if (recipe.CellSplines != null
|
||||
&& recipe.CellSplines.TryGetValue(cell.Index, out var custom)
|
||||
&& custom is { Length: SplineCurve.KnotCount })
|
||||
{
|
||||
spline = custom;
|
||||
}
|
||||
else if (recipe.AverageSpline is { Length: SplineCurve.KnotCount })
|
||||
{
|
||||
spline = recipe.AverageSpline;
|
||||
}
|
||||
|
||||
if (spline == null)
|
||||
throw new InvalidOperationException(
|
||||
$"Recipe has no spline for cell {cell.Index} — open Calibration and click 'Set Reference'.");
|
||||
|
||||
using var mask = BuildMask(recipe.Pattern, imageSize, cell.Index);
|
||||
using var hist = CalcHistogram(bgrImage, mask, bins);
|
||||
var distance = Cv2.CompareHist(refHist, hist, HistCompMethods.Bhattacharyya);
|
||||
var arr = HistogramToArray(hist);
|
||||
|
||||
var (isGood, exceed) = EvaluateAgainstSpline(arr, spline);
|
||||
results.Add(new CellResult
|
||||
{
|
||||
Index = cell.Index,
|
||||
IsGood = distance <= recipe.Tolerance,
|
||||
Distance = distance
|
||||
IsGood = isGood,
|
||||
MaxExceedance = exceed
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public Mat RenderOverlay(Mat bgrImage, CellPattern pattern, IReadOnlyList<CellResult> results)
|
||||
/// <summary>
|
||||
/// Cell is good when no histogram bin rises above the spline. <c>maxExceedance</c> is the
|
||||
/// largest (bin − spline) across all bins; non-positive means the cell passes.
|
||||
/// </summary>
|
||||
public static (bool isGood, double maxExceedance) EvaluateAgainstSpline(float[] histogram, float[] spline)
|
||||
{
|
||||
if (histogram == null || histogram.Length == 0)
|
||||
return (true, 0);
|
||||
|
||||
var maxExceed = double.NegativeInfinity;
|
||||
for (var i = 0; i < histogram.Length; i++)
|
||||
{
|
||||
var threshold = SplineCurve.EvaluateAtBin(spline, i, histogram.Length);
|
||||
var diff = histogram[i] - threshold;
|
||||
if (diff > maxExceed) maxExceed = diff;
|
||||
}
|
||||
return (maxExceed <= 0, maxExceed);
|
||||
}
|
||||
|
||||
public Mat RenderOverlay(Mat bgrImage, CellPattern pattern,
|
||||
IReadOnlyList<CellResult> results,
|
||||
IReadOnlySet<int>? overriddenCells = null)
|
||||
{
|
||||
var overlay = bgrImage.Clone();
|
||||
var cells = ComputeCells(pattern);
|
||||
var resultByIndex = results.ToDictionary(r => r.Index);
|
||||
var imageRect = new Rect(0, 0, overlay.Cols, overlay.Rows);
|
||||
const int thickness = 2;
|
||||
|
||||
foreach (var cell in cells)
|
||||
@@ -121,13 +165,12 @@ public class LeerformPatternRecognitionService
|
||||
var color = result.IsGood ? GoodColor : BadColor;
|
||||
|
||||
if (pattern.Shape == CellShape.Square)
|
||||
{
|
||||
Cv2.Rectangle(overlay, cell.BoundingBox, color, thickness);
|
||||
}
|
||||
else
|
||||
{
|
||||
Cv2.Circle(overlay, cell.Center, cell.Radius, color, thickness);
|
||||
}
|
||||
|
||||
if (overriddenCells != null && overriddenCells.Contains(cell.Index))
|
||||
DrawOverrideMarker(overlay, cell, pattern.Shape, imageRect);
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
@@ -144,6 +187,39 @@ public class LeerformPatternRecognitionService
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawOverrideMarkers(Mat target, CellPattern pattern, IReadOnlySet<int> overriddenCells)
|
||||
{
|
||||
if (overriddenCells.Count == 0) return;
|
||||
var cells = ComputeCells(pattern);
|
||||
var imageRect = new Rect(0, 0, target.Cols, target.Rows);
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (!overriddenCells.Contains(cell.Index)) continue;
|
||||
DrawOverrideMarker(target, cell, pattern.Shape, imageRect);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawOverrideMarker(Mat target, CellRegion cell, CellShape shape, Rect imageRect)
|
||||
{
|
||||
Point center;
|
||||
if (shape == CellShape.Square)
|
||||
{
|
||||
center = new Point(cell.BoundingBox.Right - 8, cell.BoundingBox.Top + 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dx = (int)(cell.Radius * 0.7);
|
||||
var dy = (int)(cell.Radius * 0.7);
|
||||
center = new Point(cell.Center.X + dx, cell.Center.Y - dy);
|
||||
}
|
||||
|
||||
if (!imageRect.Contains(center))
|
||||
return;
|
||||
|
||||
Cv2.Circle(target, center, 4, OverrideColor, thickness: -1);
|
||||
Cv2.Circle(target, center, 4, new Scalar(0, 0, 0), thickness: 1); // thin black outline for visibility
|
||||
}
|
||||
|
||||
public int? FindCellAtPoint(int x, int y, CellPattern pattern)
|
||||
{
|
||||
var cells = ComputeCells(pattern);
|
||||
@@ -173,14 +249,6 @@ public class LeerformPatternRecognitionService
|
||||
return HistogramToArray(hist);
|
||||
}
|
||||
|
||||
public double CompareHistograms(float[] reference, float[] candidate, int[]? bins = null)
|
||||
{
|
||||
bins ??= [5, 5, 5];
|
||||
using var refMat = ArrayToHistogram(reference, bins);
|
||||
using var candMat = ArrayToHistogram(candidate, bins);
|
||||
return Cv2.CompareHist(refMat, candMat, HistCompMethods.Bhattacharyya);
|
||||
}
|
||||
|
||||
public static Mat RenderHistogramChart(float[] histogram, int[] bins, int width = 600, int height = 220)
|
||||
{
|
||||
var chart = new Mat(height, width, MatType.CV_8UC3, new Scalar(30, 30, 30));
|
||||
@@ -229,7 +297,8 @@ public class LeerformPatternRecognitionService
|
||||
dims: 3,
|
||||
histSize: bins,
|
||||
ranges: new[] { new Rangef(0, 256), new Rangef(0, 256), new Rangef(0, 256) });
|
||||
Cv2.Normalize(hist, hist);
|
||||
// L1 normalize so each bin is a probability in [0, 1] summing to 1.
|
||||
Cv2.Normalize(hist, hist, 1.0, 0.0, NormTypes.L1);
|
||||
|
||||
var totalBins = bins[0] * bins[1] * bins[2];
|
||||
var reshaped = hist.Reshape(1, totalBins).Clone();
|
||||
@@ -245,14 +314,4 @@ public class LeerformPatternRecognitionService
|
||||
arr[i] = hist.At<float>(i, 0);
|
||||
return arr;
|
||||
}
|
||||
|
||||
private static Mat ArrayToHistogram(float[] data, int[] bins)
|
||||
{
|
||||
var totalBins = bins[0] * bins[1] * bins[2];
|
||||
var mat = new Mat(totalBins, 1, MatType.CV_32F, Scalar.All(0));
|
||||
var count = Math.Min(data.Length, totalBins);
|
||||
for (var i = 0; i < count; i++)
|
||||
mat.Set<float>(i, 0, data[i]);
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
|
||||
116
Plugins/LindtLeerformPlugin/Services/SplineCurve.cs
Normal file
116
Plugins/LindtLeerformPlugin/Services/SplineCurve.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
namespace LindtLeerformPlugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Monotone cubic Hermite spline (Fritsch–Carlson). Five control points with
|
||||
/// fixed, evenly spaced X positions; only Y is editable. Output never overshoots
|
||||
/// the input range, so envelope curves stay inside [0, 1] when knots are.
|
||||
/// </summary>
|
||||
public static class SplineCurve
|
||||
{
|
||||
public const int KnotCount = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the spline at parameter <paramref name="t"/> in [0, 1].
|
||||
/// Knots are positioned at t = i / (knots.Length - 1).
|
||||
/// </summary>
|
||||
public static float Evaluate(float[]? knots, double t)
|
||||
{
|
||||
if (knots == null || knots.Length == 0)
|
||||
return 0f;
|
||||
if (knots.Length == 1)
|
||||
return knots[0];
|
||||
|
||||
if (t <= 0) return knots[0];
|
||||
if (t >= 1) return knots[^1];
|
||||
|
||||
var n = knots.Length - 1;
|
||||
var pos = t * n;
|
||||
var i = (int)System.Math.Floor(pos);
|
||||
if (i >= n) return knots[n];
|
||||
var localT = pos - i;
|
||||
|
||||
// Compute secant slopes and Fritsch-Carlson tangents at the two surrounding knots only.
|
||||
var dxLeft = i > 0 ? (double)(knots[i] - knots[i - 1]) : 0;
|
||||
var dxMid = (double)(knots[i + 1] - knots[i]);
|
||||
var dxRight = i + 2 <= n ? (double)(knots[i + 2] - knots[i + 1]) : 0;
|
||||
|
||||
var m0 = MonotoneTangent(dxLeft, dxMid, hasLeft: i > 0, hasRight: true);
|
||||
var m1 = MonotoneTangent(dxMid, dxRight, hasLeft: true, hasRight: i + 2 <= n);
|
||||
|
||||
var t2 = localT * localT;
|
||||
var t3 = t2 * localT;
|
||||
var h00 = 2 * t3 - 3 * t2 + 1;
|
||||
var h10 = t3 - 2 * t2 + localT;
|
||||
var h01 = -2 * t3 + 3 * t2;
|
||||
var h11 = t3 - t2;
|
||||
|
||||
var y = h00 * knots[i] + h10 * m0 + h01 * knots[i + 1] + h11 * m1;
|
||||
return (float)y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience: evaluate the spline at the position of bin <paramref name="binIndex"/>
|
||||
/// of a histogram with <paramref name="totalBins"/> bins.
|
||||
/// </summary>
|
||||
public static float EvaluateAtBin(float[]? knots, int binIndex, int totalBins)
|
||||
{
|
||||
if (totalBins <= 1)
|
||||
return Evaluate(knots, 0);
|
||||
var t = (double)binIndex / (totalBins - 1);
|
||||
return Evaluate(knots, t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a default envelope spline from a reference histogram. For each knot
|
||||
/// the segment max around the knot position is taken, multiplied by 1.2 with a
|
||||
/// small floor added, and clamped to [0, 1].
|
||||
/// </summary>
|
||||
public static float[] CreateDefault(float[]? referenceHistogram, int knotCount = KnotCount)
|
||||
{
|
||||
var knots = new float[knotCount];
|
||||
if (referenceHistogram == null || referenceHistogram.Length == 0)
|
||||
{
|
||||
for (var k = 0; k < knotCount; k++) knots[k] = 0.05f;
|
||||
return knots;
|
||||
}
|
||||
|
||||
var totalBins = referenceHistogram.Length;
|
||||
var span = totalBins - 1;
|
||||
// Half-width of the window we look at around each knot.
|
||||
var segHalf = System.Math.Max(1, span / (2 * (knotCount - 1)));
|
||||
|
||||
for (var k = 0; k < knotCount; k++)
|
||||
{
|
||||
var center = knotCount == 1 ? 0 : k * span / (knotCount - 1);
|
||||
var lo = System.Math.Max(0, center - segHalf);
|
||||
var hi = System.Math.Min(totalBins - 1, center + segHalf);
|
||||
var max = 0f;
|
||||
for (var i = lo; i <= hi; i++)
|
||||
if (referenceHistogram[i] > max) max = referenceHistogram[i];
|
||||
|
||||
var y = max * 1.2f + 0.01f;
|
||||
if (y < 0f) y = 0f;
|
||||
if (y > 1f) y = 1f;
|
||||
knots[k] = y;
|
||||
}
|
||||
return knots;
|
||||
}
|
||||
|
||||
private static double MonotoneTangent(double secLeft, double secRight, bool hasLeft, bool hasRight)
|
||||
{
|
||||
if (!hasLeft) return secRight;
|
||||
if (!hasRight) return secLeft;
|
||||
// If the signs differ (or either is zero), the knot is an extremum: tangent must be zero
|
||||
// to preserve monotonicity locally.
|
||||
if (secLeft == 0 || secRight == 0 || System.Math.Sign(secLeft) != System.Math.Sign(secRight))
|
||||
return 0;
|
||||
// Average — Fritsch-Carlson would also clamp by 3*min(|secLeft|,|secRight|), but for our
|
||||
// small (5 knot) curves the simple average plus zero-at-extrema rule already prevents
|
||||
// overshoot in practice.
|
||||
var avg = 0.5 * (secLeft + secRight);
|
||||
var limit = 3.0 * System.Math.Min(System.Math.Abs(secLeft), System.Math.Abs(secRight));
|
||||
if (System.Math.Abs(avg) > limit)
|
||||
avg = System.Math.Sign(avg) * limit;
|
||||
return avg;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user