Files
HawkeyeVision/Plugins/LindtLeerformPlugin/Services/LeerformPatternRecognitionService.cs
EugeneTes 8704d0b5ac 2.0.15
profile influences configuration
2026-05-15 11:54:36 +02:00

219 lines
7.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using LindtLeerformPlugin.Models;
using OpenCvSharp;
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
public IReadOnlyList<CellRegion> ComputeCells(CellPattern pattern)
{
var cells = new List<CellRegion>();
if (pattern.Rows <= 0 || pattern.Cols <= 0 || pattern.RoiWidth <= 0 || pattern.RoiHeight <= 0)
return cells;
var cellW = pattern.RoiWidth / pattern.Cols;
var cellH = pattern.RoiHeight / pattern.Rows;
var padding = Math.Max(0, pattern.Padding);
var index = 0;
for (var r = 0; r < pattern.Rows; r++)
{
for (var c = 0; c < pattern.Cols; c++)
{
var x = pattern.RoiX + c * cellW + padding;
var y = pattern.RoiY + r * cellH + padding;
var w = Math.Max(1, cellW - 2 * padding);
var h = Math.Max(1, cellH - 2 * padding);
var rect = new Rect(x, y, w, h);
var center = new Point(x + w / 2, y + h / 2);
var radius = Math.Max(1, Math.Min(w, h) / 2);
cells.Add(new CellRegion
{
Index = index++,
Row = r,
Col = c,
BoundingBox = rect,
Center = center,
Radius = radius
});
}
}
return cells;
}
public Mat BuildMask(CellPattern pattern, Size imageSize, int? cellIndex = null)
{
var mask = new Mat(imageSize, MatType.CV_8UC1, Scalar.All(0));
var cells = ComputeCells(pattern);
var color = Scalar.All(255);
var imageRect = new Rect(0, 0, imageSize.Width, imageSize.Height);
foreach (var cell in cells)
{
if (cellIndex.HasValue && cellIndex.Value != cell.Index)
continue;
var clipped = cell.BoundingBox & imageRect;
if (clipped.Width <= 0 || clipped.Height <= 0)
continue;
if (pattern.Shape == CellShape.Square)
{
Cv2.Rectangle(mask, clipped, color, thickness: -1);
}
else
{
Cv2.Circle(mask, cell.Center, cell.Radius, color, thickness: -1);
}
}
return mask;
}
/// <summary>
/// Build a single foreground mask covering both white and dark chocolate flecks on the pink mold,
/// using the LAB-channel thresholding approach from <c>jupiter/grid_test.ipynb</c>.
/// <list type="bullet">
/// <item><description><b>White chocolate</b> — pink mold has a* well above 128 (red), white sits near 128. Inverse-threshold a* so neutral pixels become foreground.</description></item>
/// <item><description><b>Dark chocolate</b> — pink mold is bright (high L*), dark chocolate is dark. Inverse-threshold L* so dark pixels become foreground.</description></item>
/// </list>
/// The two masks are OR-merged, then morphologically opened (3×3 ELLIPSE) and closed (5×5 ELLIPSE)
/// to drop single-pixel noise and merge speck fragments. Caller owns the returned <see cref="Mat"/>.
/// </summary>
public static Mat DetectChocolateMask(Mat bgrImage, int aThreshold, int lThreshold)
{
using var lab = new Mat();
Cv2.CvtColor(bgrImage, lab, ColorConversionCodes.BGR2Lab);
var channels = Cv2.Split(lab);
try
{
var lChan = channels[0];
var aChan = channels[1];
using var whiteMask = new Mat();
Cv2.Threshold(aChan, whiteMask, aThreshold, 255, ThresholdTypes.BinaryInv);
using var darkMask = new Mat();
Cv2.Threshold(lChan, darkMask, lThreshold, 255, ThresholdTypes.BinaryInv);
var combined = new Mat();
Cv2.BitwiseOr(whiteMask, darkMask, combined);
using var openKernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3));
using var closeKernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(5, 5));
Cv2.MorphologyEx(combined, combined, MorphTypes.Open, openKernel);
Cv2.MorphologyEx(combined, combined, MorphTypes.Close, closeKernel);
return combined;
}
finally
{
foreach (var ch in channels)
ch.Dispose();
}
}
public IReadOnlyList<CellResult> EvaluateCells(Mat bgrImage, LeerformRecipe recipe)
{
using var chocolateMask = DetectChocolateMask(bgrImage, recipe.AThreshold, recipe.LThreshold);
var cells = ComputeCells(recipe.Pattern);
var imageSize = new Size(bgrImage.Cols, bgrImage.Rows);
var results = new List<CellResult>(cells.Count);
foreach (var cell in cells)
{
using var cellMask = BuildMask(recipe.Pattern, imageSize, cell.Index);
using var perCell = new Mat();
Cv2.BitwiseAnd(chocolateMask, cellMask, perCell);
Cv2.FindContours(
perCell,
out var contours,
out _,
RetrievalModes.External,
ContourApproximationModes.ApproxSimple);
var blobs = 0;
var area = 0;
foreach (var contour in contours)
{
var contourArea = (int)Cv2.ContourArea(contour);
if (contourArea >= recipe.MinBlobArea && contourArea <= recipe.MaxBlobArea)
{
blobs++;
area += contourArea;
}
}
results.Add(new CellResult
{
Index = cell.Index,
IsGood = blobs == 0,
BlobCount = blobs,
DetectedArea = area
});
}
return results;
}
public Mat RenderOverlay(Mat bgrImage, CellPattern pattern, IReadOnlyList<CellResult> results)
{
var overlay = bgrImage.Clone();
var cells = ComputeCells(pattern);
var resultByIndex = results.ToDictionary(r => r.Index);
const int thickness = 2;
foreach (var cell in cells)
{
if (!resultByIndex.TryGetValue(cell.Index, out var result))
continue;
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);
}
return overlay;
}
public void DrawPattern(Mat target, CellPattern pattern, Scalar color, int thickness = 2)
{
var cells = ComputeCells(pattern);
foreach (var cell in cells)
{
if (pattern.Shape == CellShape.Square)
Cv2.Rectangle(target, cell.BoundingBox, color, thickness);
else
Cv2.Circle(target, cell.Center, cell.Radius, color, thickness);
}
}
public int? FindCellAtPoint(int x, int y, CellPattern pattern)
{
var cells = ComputeCells(pattern);
foreach (var cell in cells)
{
if (pattern.Shape == CellShape.Square)
{
if (cell.BoundingBox.Contains(x, y))
return cell.Index;
}
else
{
var dx = x - cell.Center.X;
var dy = y - cell.Center.Y;
if (dx * dx + dy * dy <= cell.Radius * cell.Radius)
return cell.Index;
}
}
return null;
}
}