230 lines
9.1 KiB
C#
230 lines
9.1 KiB
C#
// ============================================================================
|
|
// 文件� VoidMeasurementProcessor.cs
|
|
// æ��è¿°: 空隙测é‡�ç®—å�
|
|
//
|
|
// 处��程:
|
|
// 1. 构建多边形ROI掩ç �,计算ROIé�¢ç§¯
|
|
// 2. 在ROI内进行�阈值分割��气泡区�
|
|
// 3. å½¢æ€�å¦è†¨èƒ€å�ˆå¹¶ç›¸é‚»æ°”泡
|
|
// 4. 轮廓检测,计算�个气泡�积
|
|
// 5. 计算空隙�= 总气泡��/ ROI�积
|
|
// ============================================================================
|
|
|
|
using Emgu.CV;
|
|
using Emgu.CV.CvEnum;
|
|
using Emgu.CV.Structure;
|
|
using Emgu.CV.Util;
|
|
using Serilog;
|
|
using System.Drawing;
|
|
using XP.ImageProcessing.Core;
|
|
|
|
namespace XP.ImageProcessing.Processors;
|
|
|
|
public class VoidMeasurementProcessor : ImageProcessorBase
|
|
{
|
|
private static readonly ILogger _logger = Log.ForContext<VoidMeasurementProcessor>();
|
|
|
|
public VoidMeasurementProcessor()
|
|
{
|
|
Name = LocalizationHelper.GetString("VoidMeasurementProcessor_Name");
|
|
Description = LocalizationHelper.GetString("VoidMeasurementProcessor_Description");
|
|
}
|
|
|
|
protected override void InitializeParameters()
|
|
{
|
|
// ── 多边形ROI(由UI注入,最�2个点�──
|
|
Parameters.Add("PolyCount", new ProcessorParameter("PolyCount", "PolyCount", typeof(int), 0, null, null, "") { IsVisible = false });
|
|
for (int i = 0; i < 32; i++)
|
|
{
|
|
Parameters.Add($"PolyX{i}", new ProcessorParameter($"PolyX{i}", $"PolyX{i}", typeof(int), 0, null, null, "") { IsVisible = false });
|
|
Parameters.Add($"PolyY{i}", new ProcessorParameter($"PolyY{i}", $"PolyY{i}", typeof(int), 0, null, null, "") { IsVisible = false });
|
|
}
|
|
|
|
// ── 气泡检测��──
|
|
Parameters.Add("MinThreshold", new ProcessorParameter(
|
|
"MinThreshold",
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_MinThreshold"),
|
|
typeof(int), 128, 0, 255,
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_MinThreshold_Desc")));
|
|
|
|
Parameters.Add("MaxThreshold", new ProcessorParameter(
|
|
"MaxThreshold",
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_MaxThreshold"),
|
|
typeof(int), 255, 0, 255,
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_MaxThreshold_Desc")));
|
|
|
|
Parameters.Add("MinVoidArea", new ProcessorParameter(
|
|
"MinVoidArea",
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_MinVoidArea"),
|
|
typeof(int), 10, 1, 100000,
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_MinVoidArea_Desc")));
|
|
|
|
Parameters.Add("MergeRadius", new ProcessorParameter(
|
|
"MergeRadius",
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_MergeRadius"),
|
|
typeof(int), 3, 0, 30,
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_MergeRadius_Desc")));
|
|
|
|
Parameters.Add("BlurSize", new ProcessorParameter(
|
|
"BlurSize",
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_BlurSize"),
|
|
typeof(int), 3, 1, 31,
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_BlurSize_Desc")));
|
|
|
|
Parameters.Add("VoidLimit", new ProcessorParameter(
|
|
"VoidLimit",
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_VoidLimit"),
|
|
typeof(double), 25.0, 0.0, 100.0,
|
|
LocalizationHelper.GetString("VoidMeasurementProcessor_VoidLimit_Desc")));
|
|
}
|
|
|
|
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
|
{
|
|
int minThresh = GetParameter<int>("MinThreshold");
|
|
int maxThresh = GetParameter<int>("MaxThreshold");
|
|
int minVoidArea = GetParameter<int>("MinVoidArea");
|
|
int mergeRadius = GetParameter<int>("MergeRadius");
|
|
int blurSize = GetParameter<int>("BlurSize");
|
|
double voidLimit = GetParameter<double>("VoidLimit");
|
|
|
|
if (blurSize % 2 == 0) blurSize++;
|
|
|
|
OutputData.Clear();
|
|
int w = inputImage.Width, h = inputImage.Height;
|
|
|
|
// ── 构建多边形ROI掩ç � ──
|
|
int polyCount = GetParameter<int>("PolyCount");
|
|
Image<Gray, byte>? roiMask = null;
|
|
Point[]? roiPoints = null;
|
|
|
|
if (polyCount >= 3)
|
|
{
|
|
roiPoints = new Point[polyCount];
|
|
for (int i = 0; i < polyCount; i++)
|
|
roiPoints[i] = new Point(GetParameter<int>($"PolyX{i}"), GetParameter<int>($"PolyY{i}"));
|
|
roiMask = new Image<Gray, byte>(w, h);
|
|
using var vop = new VectorOfPoint(roiPoints);
|
|
using var vvop = new VectorOfVectorOfPoint(vop);
|
|
CvInvoke.DrawContours(roiMask, vvop, 0, new MCvScalar(255), -1);
|
|
}
|
|
else
|
|
{
|
|
// æ— ROI时使用全å›?
|
|
roiMask = new Image<Gray, byte>(w, h);
|
|
roiMask.SetValue(new Gray(255));
|
|
}
|
|
|
|
int roiArea = CvInvoke.CountNonZero(roiMask);
|
|
|
|
_logger.Debug("VoidMeasurement: ROI area={Area}, Thresh=[{Min},{Max}], MergeR={MR}",
|
|
roiArea, minThresh, maxThresh, mergeRadius);
|
|
|
|
// ── 高斯模糊�噪 ──
|
|
var blurred = new Image<Gray, byte>(w, h);
|
|
CvInvoke.GaussianBlur(inputImage, blurred, new Size(blurSize, blurSize), 0);
|
|
|
|
// ── �阈值分割��气泡(亮区域) ──
|
|
var voidImg = new Image<Gray, byte>(w, h);
|
|
byte[,,] srcData = blurred.Data;
|
|
byte[,,] dstData = voidImg.Data;
|
|
byte[,,] maskData = roiMask.Data;
|
|
|
|
for (int y = 0; y < h; y++)
|
|
{
|
|
for (int x = 0; x < w; x++)
|
|
{
|
|
if (maskData[y, x, 0] > 0)
|
|
{
|
|
byte val = srcData[y, x, 0];
|
|
dstData[y, x, 0] = (val >= minThresh && val <= maxThresh) ? (byte)255 : (byte)0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── å½¢æ€�å¦è†¨èƒ€å�ˆå¹¶ç›¸é‚»æ°”泡 ──
|
|
if (mergeRadius > 0)
|
|
{
|
|
int kernelSize = mergeRadius * 2 + 1;
|
|
using var kernel = CvInvoke.GetStructuringElement(ElementShape.Ellipse,
|
|
new Size(kernelSize, kernelSize), new Point(-1, -1));
|
|
CvInvoke.Dilate(voidImg, voidImg, kernel, new Point(-1, -1), 1, BorderType.Default, new MCvScalar(0));
|
|
// 与ROI掩ç �å�–交集,防æ¢è†¨èƒ€è¶…出ROI
|
|
CvInvoke.BitwiseAnd(voidImg, roiMask, voidImg);
|
|
}
|
|
|
|
// ── 轮廓检�──
|
|
using var contours = new VectorOfVectorOfPoint();
|
|
using var hierarchy = new Mat();
|
|
CvInvoke.FindContours(voidImg, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
|
|
|
|
var voids = new List<VoidRegionInfo>();
|
|
int totalVoidArea = 0;
|
|
|
|
for (int i = 0; i < contours.Size; i++)
|
|
{
|
|
double area = CvInvoke.ContourArea(contours[i]);
|
|
if (area < minVoidArea) continue;
|
|
|
|
var moments = CvInvoke.Moments(contours[i]);
|
|
if (moments.M00 < 1) continue;
|
|
|
|
int intArea = (int)Math.Round(area);
|
|
totalVoidArea += intArea;
|
|
|
|
voids.Add(new VoidRegionInfo
|
|
{
|
|
Index = voids.Count + 1,
|
|
CenterX = moments.M10 / moments.M00,
|
|
CenterY = moments.M01 / moments.M00,
|
|
Area = intArea,
|
|
AreaPercent = roiArea > 0 ? area / roiArea * 100.0 : 0,
|
|
BoundingBox = CvInvoke.BoundingRectangle(contours[i]),
|
|
ContourPoints = contours[i].ToArray()
|
|
});
|
|
}
|
|
|
|
// 按�积从大到�排�
|
|
voids.Sort((a, b) => b.Area.CompareTo(a.Area));
|
|
for (int i = 0; i < voids.Count; i++) voids[i].Index = i + 1;
|
|
|
|
double voidRate = roiArea > 0 ? (double)totalVoidArea / roiArea * 100.0 : 0;
|
|
string classification = voidRate <= voidLimit ? "PASS" : "FAIL";
|
|
int maxVoidArea = voids.Count > 0 ? voids[0].Area : 0;
|
|
|
|
_logger.Information("VoidMeasurement: VoidRate={Rate:F1}%, Voids={Count}, MaxArea={Max}, {Class}",
|
|
voidRate, voids.Count, maxVoidArea, classification);
|
|
|
|
// ── 输出数� ──
|
|
OutputData["VoidMeasurementResult"] = true;
|
|
OutputData["RoiArea"] = roiArea;
|
|
OutputData["RoiPoints"] = roiPoints;
|
|
OutputData["TotalVoidArea"] = totalVoidArea;
|
|
OutputData["VoidRate"] = voidRate;
|
|
OutputData["VoidLimit"] = voidLimit;
|
|
OutputData["VoidCount"] = voids.Count;
|
|
OutputData["MaxVoidArea"] = maxVoidArea;
|
|
OutputData["Classification"] = classification;
|
|
OutputData["Voids"] = voids;
|
|
OutputData["ResultText"] = $"Void: {voidRate:F1}% | {classification} | {voids.Count} voids | ROI: {roiArea}px";
|
|
|
|
blurred.Dispose();
|
|
voidImg.Dispose();
|
|
roiMask.Dispose();
|
|
|
|
return inputImage.Clone();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// �个空隙区域信�
|
|
/// </summary>
|
|
public class VoidRegionInfo
|
|
{
|
|
public int Index { get; set; }
|
|
public double CenterX { get; set; }
|
|
public double CenterY { get; set; }
|
|
public int Area { get; set; }
|
|
public double AreaPercent { get; set; }
|
|
public Rectangle BoundingBox { get; set; }
|
|
public Point[] ContourPoints { get; set; } = Array.Empty<Point>();
|
|
} |