Files
meelstorm 745a15b162 2.0.10
2025-10-20 09:30:08 +02:00

664 lines
28 KiB
C#

using CandyboxPlugin.Detectors;
using CandyboxPlugin.Geometry;
using CandyboxPlugin.Voronoi;
using OpenCvSharp;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.Text.Json;
using Lindt.Candybox.Demo;
using VisionBuilder.UI.Common.NullClasses;
using Point = System.Drawing.Point;
using Size = OpenCvSharp.Size;
namespace CandyboxPlugin.Recipe
{
//todo: update camera settings according to learned parameters
public class HistoRecipe
{
private readonly string _recipeName;
private CandyDetector _learner;
private BlisterDetector _blister;
private DefaultRectangle2Algorithm _rectangle2;
private LearningParameters _learnedParameters;
private ColorHeatMap _colorHeatMap;
private Size AnalysisSize { get; }= new Size(512, 512);
Size ThumbnailSize { get; } = new Size(256, 200);
public const string RECIPES_DIRECTORY = "..\\Data\\Recipes";
public HistoRecipe(string recipeName)
{
_recipeName = recipeName;
_configurationPath = Path.GetFullPath(Path.Combine(RECIPES_DIRECTORY, _recipeName + ".json"));
_learner = new CandyDetector();
_blister = new BlisterDetector();
_rectangle2 = new DefaultRectangle2Algorithm();
_learnedParameters = new LearningParameters();
_colorHeatMap = new ColorHeatMap(255);
Reload();
}
public void Reload()
{
lock (_lockObject)
{
if (File.Exists(_configurationPath))
{
_learnedParameters =
JsonSerializer.Deserialize<LearningParameters>(File.ReadAllText(_configurationPath));
}
}
}
public void LearnContours(OpenCvSharp.Point[][] contours, Mat image, List<VectorLine> cuts, bool relearn)
{
lock (_lockObject)
{
_learnedParameters = relearn ? new LearningParameters() : _learnedParameters;
// Convert cuts to be relative to blister center
_learnedParameters.Cuts = cuts;
if (_learnedParameters.CandyParameters.Count == 0)//initial learning
{
_learnedParameters.BlisterAngle = _blisterAngle;
_learnedParameters.BlisterPosition = _blisterCenter;
_learnedParameters.ImageWidth = (int)(_blisterEdge / 512f * 1800f);
for (int i = 0; i < contours.Length; i++)
{
var hist = CalcHist(contours, i, _resizedMat, out var outerRadius, out var innerRadius);
var moments = Cv2.Moments(contours[i]);
var cX = (int)(moments.M10 / moments.M00);
var cY = (int)(moments.M01 / moments.M00);
hist.GetArray(out float[] histData);
var p = new CandyParameter()
{
Position = new Point(cX, cY),
Histogram = new Queue<float[]>(),
OuterRadius = (int)outerRadius,
InnerRadius = (int)innerRadius
};
p.Histogram.Enqueue(histData);
_learnedParameters.CandyParameters.Add(p);
}
}
else
{
foreach (CandyParameter parameter in _learnedParameters.CandyParameters)
{
parameter.ResetPoint();
parameter.RotatePoint(Point.Empty, _blisterAngle - _learnedParameters.BlisterAngle);
}
for (int i = 0; i < contours.Length; i++)
{
var hist = CalcHist(contours, i, _resizedMat, out var outerRadius, out var innerRadius);
var moments = Cv2.Moments(contours[i]);
var cX = (int)(moments.M10 / moments.M00);
var cY = (int)(moments.M01 / moments.M00);
hist.GetArray(out float[] histData);
var point = ClosestPoint(_blisterCenter, cX, cY, (int)outerRadius, (int)innerRadius);
point.Histogram.Enqueue(histData);
if (point.Histogram.Count > _learnedParameters.Configuration.MemoryBuffer)
{
point.Histogram.Dequeue();
}
}
foreach (CandyParameter parameter in _learnedParameters.CandyParameters)
{
parameter.ResetPoint();
}
}
File.WriteAllText(_configurationPath, JsonSerializer.Serialize(_learnedParameters, new JsonSerializerOptions() { WriteIndented = true }));
var fullSamplePath = Path.GetFullPath(Path.Combine("..\\Data\\Samples", _recipeName + ".bmp"));
Directory.CreateDirectory(Path.GetDirectoryName(fullSamplePath));
image.Resize(ThumbnailSize).SaveImage(fullSamplePath);
}
}
public HistoRecipe Clone()
{
var clone = new HistoRecipe(_recipeName);
return clone;
}
public Point BlisterCenter => _blisterCenter;
public ProcessingResult ProcessImage(Mat image)
{
lock (_lockObject)
{
var sw = Stopwatch.StartNew();
try
{
var framedImage = new Mat(new Size(1800, 1408),MatType.CV_8UC3,Scalar.FromRgb(36, 67, 182));
// draw the image in top left corner of framed image
// this does not actually create new Mat. it just creates a header for the region of interest
image.CopyTo(framedImage[0, image.Height, 0, image.Width]);
// add alpha channel
var rgb = framedImage;
framedImage = framedImage.CvtColor(ColorConversionCodes.BGR2BGRA);
var resized = rgb.Resize(AnalysisSize);
_resizedMat = resized;
var sw1 = Stopwatch.StartNew();
Mat res = _learner.Eval(resized);
Mat resBlister = _blister.Eval(resized);
var element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(50, 50));
Mat blisterOpened = resBlister.Threshold(128,255,ThresholdTypes.Binary).MorphologyEx(MorphTypes.Open,element);
resBlister.Dispose();
resBlister = blisterOpened;
sw1.Stop();
Console.WriteLine("AI Time:" + sw1.ElapsedMilliseconds);
Mat inverted = null;
Mat overlayMat = new Mat(res.Width, res.Height, MatType.CV_8UC4, new Scalar(0, 0, 0, 0));
bool wasError = false;
List<int> errorPoints = new List<int>();
OpenCvSharp.Point[] blisterRectangle;
Mat numbersOverlay = new Mat(512, 512, MatType.CV_8UC4, new Scalar(0, 0, 0, 0));
List<EErrorReason> errorReason = new List<EErrorReason>();
using (var tracker = new ResourcesTracker())
{
blisterRectangle = FindBlister(resBlister, out _blisterAngle, out _blisterCenter,
out _blisterEdge);
if (_learnedParameters.CandyParameters.Count > 0)
{
DelaunayTriangulator triangulator = new DelaunayTriangulator();
triangulator.GenerateBorder(512, 512);
var points=_learnedParameters.CandyParameters
.Select((x, i) =>
new VoronoiPoint(
x.Position.X + _blisterCenter.X - _learnedParameters.BlisterPosition.X,
x.Position.Y + _blisterCenter.Y - _learnedParameters.BlisterPosition.Y
))
.ToList();
var triangles = triangulator.BowyerWatson(_learnedParameters.CandyParameters
.Select((x,i) =>
new VoronoiPoint(
x.Position.X + _blisterCenter.X - _learnedParameters.BlisterPosition.X,
x.Position.Y + _blisterCenter.Y - _learnedParameters.BlisterPosition.Y
))
.ToList());
var edges = Voronoi.Voronoi.GenerateEdgesFromDelaunay(triangles);
foreach (Edge edge in edges)
{
Cv2.Line(
res,
new OpenCvSharp.Point(edge.Point1.X, edge.Point1.Y),
new OpenCvSharp.Point(edge.Point2.X, edge.Point2.Y),
new Scalar(0, 0, 0), // Black color in BGR
3 // Thickness
);
}
}
foreach (VectorLine cut in _learnedParameters.Cuts)
{
// Convert from relative to absolute coordinates
var absoluteStart = new OpenCvSharp.Point(
cut.StartPoint.X + _blisterCenter.X,
cut.StartPoint.Y + _blisterCenter.Y
);
var absoluteEnd = new OpenCvSharp.Point(
cut.EndPoint.X + _blisterCenter.X,
cut.EndPoint.Y + _blisterCenter.Y
);
Cv2.Line(res, absoluteStart, absoluteEnd, new Scalar(0, 0, 0), 3);
}
var mat = res;
Cv2.Threshold(mat, mat, 128, 255, ThresholdTypes.Binary);
OpenCvSharp.Point[][] contours;
contours = mat.FindContoursAsArray(RetrievalModes.External,
ContourApproximationModes.ApproxNone);
contours = contours.Where(x => Cv2.ContourArea(x) > 500).ToArray();
var sw2 = Stopwatch.StartNew();
LastContours = contours;
int c = 0;
foreach (CandyParameter parameter in _learnedParameters.CandyParameters)
{
parameter.ResetPoint();
parameter.RotatePoint(Point.Empty, _blisterAngle - _learnedParameters.BlisterAngle);
var coordX = parameter.Position.X +
(_blisterCenter.X - _learnedParameters.BlisterPosition.X);
var coordY = parameter.Position.Y +
(_blisterCenter.Y - _learnedParameters.BlisterPosition.Y);
// Draw filled white circle (ellipse)
Cv2.Circle(numbersOverlay, new OpenCvSharp.Point(coordX, coordY), 10, new Scalar(255, 255, 255, 255), thickness: -1);
// Draw black ellipse outline
Cv2.Ellipse(numbersOverlay, new OpenCvSharp.Point(coordX, coordY), new Size(10, 10), 0, 0, 360, new Scalar(0, 0, 0, 255), thickness: 1);
// Draw the number as text (centered, adjust offset as needed)
Cv2.PutText(
numbersOverlay,
c.ToString(),
new OpenCvSharp.Point(coordX - 7, coordY + 7), // Y offset for baseline alignment
HersheyFonts.HersheySimplex,
0.4, // Font scale
new Scalar(0,0,0,255),
1,
LineTypes.AntiAlias
);
bool found = false;
for (int i = 0; i < contours.Length; i++)
{
if (Cv2.PointPolygonTest(contours[i],
new Point2f(coordX, coordY), false) > 0)
{
var hist = CalcHist(contours, i, _resizedMat, out var outerRadius,
out var innerRadius);
var correl = 0;
foreach (float[] h in parameter.Histogram)
{
Mat histC = new Mat(new Size(1, 125), MatType.CV_32FC1);
histC.SetArray(h);
var correl_0 = Cv2.CompareHist(histC, hist, HistCompMethods.Correl) * 100;
if (correl_0 > correl)
{
correl = (int) correl_0;
}
}
if (_learnedParameters.Configuration.IgnoreCorrelation.Contains(c))
correl = 100;
if (correl > _learnedParameters.Configuration.SimilarityTolerance)
{
var min = _learnedParameters.Configuration.SimilarityTolerance;
var max = 100;
var value = correl;
value = value < min ? min : value;
var value2 = (int) Map(value, min, max, 0, 255);
var color = _colorHeatMap.GetColorForValue(value2, 255);
Cv2.DrawContours(overlayMat, contours, i,
new Scalar(color.B, color.G, color.R, color.A));
found = true;
}
else
{
Cv2.DrawContours(overlayMat, contours, i, OpaqueScalar(Scalar.Red));
Console.WriteLine($"{c} wrong type. similarity is {correl}%");
errorReason.Add(EErrorReason.WrongType);
ErrorTextHelper.DrawErrorText(overlayMat, new OpenCvSharp.Point(coordX, coordY),
EErrorReason.WrongType);
errorPoints.Add(c);
found = true;
wasError = true;
break;
}
var outerDiff = parameter.OuterRadius - outerRadius;
if (Math.Abs(outerDiff / parameter.OuterRadius) * 100 >
_learnedParameters.Configuration.OuterTolerance && outerDiff < 0)
{
Cv2.DrawContours(overlayMat, contours, i, OpaqueScalar(Scalar.Red));
errorReason.Add(EErrorReason.WrongSize);
ErrorTextHelper.DrawErrorText(overlayMat, new OpenCvSharp.Point(coordX, coordY),
EErrorReason.WrongSize);
Console.WriteLine(
$"{c} wrong outer size. expected {parameter.OuterRadius}, got {outerRadius} ({Math.Abs(outerDiff / parameter.OuterRadius) * 100f})");
errorPoints.Add(c);
wasError = true;
break;
}
var innerDiff = parameter.InnerRadius - innerRadius;
if (Math.Abs(innerDiff / parameter.InnerRadius) * 100 >
_learnedParameters.Configuration.InnerTolerance && innerDiff < 0)
{
Cv2.DrawContours(overlayMat, contours, i, OpaqueScalar(Scalar.Red));
Console.WriteLine(
$"{c} wrong inner size. expected {parameter.InnerRadius}, got {innerRadius} ({Math.Abs(innerDiff / parameter.InnerRadius) * 100f})");
errorReason.Add(EErrorReason.WrongSize);
ErrorTextHelper.DrawErrorText(overlayMat, new OpenCvSharp.Point(coordX, coordY),
EErrorReason.WrongSize);
errorPoints.Add(c);
wasError = true;
break;
}
break;
}
}
if (!found)
{
wasError = true;
errorReason.Add(EErrorReason.Missing);
errorPoints.Add(c);
Cv2.Circle(overlayMat, coordX, coordY, 10, OpaqueScalar(Scalar.Red), Cv2.FILLED);
ErrorTextHelper.DrawErrorText(overlayMat, new OpenCvSharp.Point(coordX, coordY),
EErrorReason.Missing);
}
c++;
}
if (wasError)
{
Cv2.DrawContours(overlayMat, new[] {blisterRectangle}, 0, OpaqueScalar(Scalar.Red),
thickness: 5);
}
else
{
Cv2.DrawContours(overlayMat, new[] {blisterRectangle}, 0, OpaqueScalar(Scalar.Pink),
thickness: 1);
}
inverted = overlayMat;
sw2.Stop();
Console.WriteLine("CVTime:" + sw2.ElapsedMilliseconds);
}
sw.Stop();
Console.WriteLine("Time:" + sw.ElapsedMilliseconds);
inverted = Overlap(numbersOverlay, inverted);
var final = inverted.Resize(framedImage.Size());
final = Overlap(final,framedImage);
if (_learnedParameters.ImageWidth > 0)
{
float cut = _learnedParameters.ImageWidth;
Cv2.Line(final, new OpenCvSharp.Point(cut, 0), new OpenCvSharp.Point(cut, 1408), Scalar.Red, 1);
}
if(_learnedParameters.CandyParameters.Count==0)errorReason.Add(EErrorReason.NotLearned);
Thread.Sleep(50);
return new ProcessingResult(framedImage, final,
_learnedParameters.CandyParameters.Count == 0 || wasError, (int) sw.ElapsedMilliseconds,
errorReason)
{
BlisterRectangle = blisterRectangle.Select(x => new Point(x.X, x.Y)).ToList(),
ErrorPoints = errorPoints
};
}
catch(Exception ex)
{
Thread.Sleep(50);
return new ProcessingResult(image, image,
true, (int)sw.ElapsedMilliseconds,
new List<EErrorReason>() {EErrorReason.Unknown})
{
};
}
}
}
public OpenCvSharp.Point[][] LastContours { get; set; }
public LearningParameters LearnedParameters => _learnedParameters;
private CandyParameter ClosestPoint(Point blisterCenter, int cX, int cY, int outerRadius,
int innerRadius)
{
CandyParameter closest = null;
double minDistance = double.MaxValue;
foreach (CandyParameter parameter in _learnedParameters.CandyParameters)
{
var coordX = parameter.Position.X +
(blisterCenter.X - _learnedParameters.BlisterPosition.X);
var coordY = parameter.Position.Y +
(blisterCenter.Y - _learnedParameters.BlisterPosition.Y);
var distance = Distance(coordX - cX, coordY - cY);
if ( distance< minDistance)
{
minDistance = distance;
closest = parameter;
}
}
return closest;
}
private static double Distance(int x, int y)
{
return Math.Sqrt(x * x + y * y);
}
static void ShowHistogram(Mat hist)
{
Mat render = new Mat(new Size(125, 125), MatType.CV_8UC3, Scalar.All(255));
double minVal, maxVal;
Cv2.MinMaxLoc(hist, out minVal, out maxVal);
Scalar color = Scalar.All(100);
// Scales and draws histogram
hist = hist * (maxVal != 0 ? 125 / maxVal : 0.0);
hist.GetArray(out float[] histArr);
for (int j = 0; j < 125; ++j)
{
render.Rectangle(
new OpenCvSharp.Point(j , render.Rows - (int)histArr[j]),
new OpenCvSharp.Point(j + 1 , render.Rows),
color,
-1);
}
Cv2.ImShow("hist",render);
Cv2.WaitKey(1);
}
public static double Map (double value, double fromSource, double toSource, double fromTarget, double toTarget)
{
return (value - fromSource) / (toSource - fromSource) * (toTarget - fromTarget) + fromTarget;
}
private static object _lockObject = new object();
private float _blisterAngle;
private Point _blisterCenter;
private int _blisterEdge;
private Mat _resizedMat;
private readonly string _configurationPath;
private static Scalar OpaqueScalar(Scalar scalar)
{
return new Scalar(scalar.Val0, scalar.Val1, scalar.Val2, 255);
}
private static Mat CalcHist(OpenCvSharp.Point[][] contours, int i, Mat resizedMat,out float outerRadius,out float innerRadius)
{
var c = new Mat(new Size(512, 512), MatType.CV_8UC1,Scalar.Black);
Cv2.DrawContours(c, contours, i, Scalar.White, thickness: Cv2.FILLED);
Cv2.MinEnclosingCircle(contours[i],out var center,out outerRadius);
var distance = c.DistanceTransform(DistanceTypes.L2, DistanceTransformMasks.Mask5);
distance.MinMaxIdx(out _,out var innerRadiusD);
innerRadius = (float) innerRadiusD;
var hist = new Mat();
Cv2.CalcHist(new[] {resizedMat}, new[] {0, 1, 2}, c, hist, 3, new[] {5, 5, 5},
new[] {new Rangef(0, 256), new Rangef(0, 256), new Rangef(0, 256)});
Cv2.Normalize(hist, hist);
hist = hist.Reshape(1, 125, 1);
//if (i == 22)
//{
// ShowHistogram(hist);
//}
return hist;
}
private OpenCvSharp.Point[] FindBlister(Mat resBlister,out float blisterAngle,out Point center, out int blisterEdge)
{
var blisterMat = resBlister.Threshold(128, 255, ThresholdTypes.Binary);
var blisterContours = blisterMat.FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxNone);
var cnt = blisterContours.Length;
int? blisterArea = null;
blisterEdge = 0;
for (int i = 0; i < cnt; i++)
{
var area = Cv2.ContourArea(blisterContours[i]);
if (area>5000)
{
var max= blisterContours[i].Max(x => x.X);
if (max>blisterEdge)
{
blisterEdge = max;
}
var rect2 = Cv2.MinAreaRect(blisterContours[i]);
var tmp = rect2.Angle;
if (tmp > 15)
{
tmp = -(90 - tmp);
}
blisterAngle = tmp/180f*3.14f;
center = new Point((int)rect2.Center.X, (int)rect2.Center.Y);
Console.WriteLine("blister angle:"+blisterAngle);
//var c = blisterMat.CvtColor(ColorConversionCodes.GRAY2BGR);
//Cv2.Rectangle(c,rect2.BoundingRect(),Scalar.Red);
//Cv2.ImShow("test",c);
//Cv2.WaitKey(0);
return rect2.Points().Select(x=>new OpenCvSharp.Point(x.X,x.Y)).ToArray();
}
}
throw new Exception("Blister not found");
}
public static Bitmap SetImageOpacity(Bitmap image, float opacity)
{
try
{
//create a Bitmap the size of the image provided
var bmp = new Bitmap(image.Width, image.Height);
//create a graphics object from the image
using (var gfx = Graphics.FromImage(bmp))
{
//create a color matrix object
var matrix = new ColorMatrix();
//set the opacity
matrix.Matrix33 = opacity;
//create image attributes
var attributes = new ImageAttributes();
//set the color(opacity) of the image
attributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
//now draw the image
gfx.DrawImage(image, new Rectangle(0, 0, bmp.Width, bmp.Height), 0, 0, image.Width, image.Height,
GraphicsUnit.Pixel, attributes);
}
return bmp;
}
catch (Exception ex)
{
//MessageBox.Show(ex.Message);
throw ex;
//return null;
}
}
public static Mat Overlap(Mat source1, Mat source2)
{
var target = new Mat(source2.Size(),source2.Type());
Cv2.Split(source1, out Mat[] src2Channels);
var alpha = src2Channels[3];
source2.CopyTo(target);
source1.CopyTo(target,alpha);
return target;
}
public int GetCameraWidth()
{
return _learnedParameters.ImageWidth;
}
}
}