histogram implemented

This commit is contained in:
EugeneTes
2026-04-08 13:10:34 +02:00
parent 931de47164
commit 2be13501fa
11 changed files with 805 additions and 125 deletions

View File

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