规范类名及命名空间名称
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// ============================================================================
|
||||
// 文件� AngleMeasurementProcessor.cs
|
||||
// æ��è¿°: 角度测é‡�ç®—å� â€?共端点的两æ�¡ç›´çº¿å¤¹è§’
|
||||
// 功能:
|
||||
// - 用户定义三个点:端点(顶点)�射�终点�射�终点
|
||||
// - 计算两�射线之间的夹角(0°~180°�
|
||||
// - 在图åƒ�上绘制两æ�¡å°„线ã€�è§’åº¦å¼§çº¿å’Œæ ‡æ³¨
|
||||
// ============================================================================
|
||||
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.Structure;
|
||||
using Serilog;
|
||||
using System.Drawing;
|
||||
using XP.ImageProcessing.Core;
|
||||
|
||||
namespace XP.ImageProcessing.Processors;
|
||||
|
||||
public class AngleMeasurementProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<AngleMeasurementProcessor>();
|
||||
|
||||
public AngleMeasurementProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("AngleMeasurementProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("AngleMeasurementProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
// 三个点å��æ ‡ï¼ˆç”±äº¤äº’æŽ§ä»¶æ³¨å…¥ï¼Œä½¿ç”¨ double é�¿å…�å�–整误差ï¼?
|
||||
Parameters.Add("VX", new ProcessorParameter("VX", "VX", typeof(double), 250.0, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("VY", new ProcessorParameter("VY", "VY", typeof(double), 250.0, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("AX", new ProcessorParameter("AX", "AX", typeof(double), 100.0, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("AY", new ProcessorParameter("AY", "AY", typeof(double), 250.0, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("BX", new ProcessorParameter("BX", "BX", typeof(double), 250.0, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("BY", new ProcessorParameter("BY", "BY", typeof(double), 100.0, null, null, "") { IsVisible = false });
|
||||
}
|
||||
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
double vx = GetParameter<double>("VX"), vy = GetParameter<double>("VY");
|
||||
double ax = GetParameter<double>("AX"), ay = GetParameter<double>("AY");
|
||||
double bx = GetParameter<double>("BX"), by = GetParameter<double>("BY");
|
||||
|
||||
OutputData.Clear();
|
||||
|
||||
// �� VA �VB
|
||||
double vax = ax - vx, vay = ay - vy;
|
||||
double vbx = bx - vx, vby = by - vy;
|
||||
|
||||
double lenA = Math.Sqrt(vax * vax + vay * vay);
|
||||
double lenB = Math.Sqrt(vbx * vbx + vby * vby);
|
||||
|
||||
double angleDeg = 0;
|
||||
if (lenA > 0.001 && lenB > 0.001)
|
||||
{
|
||||
double dot = vax * vbx + vay * vby;
|
||||
double cosAngle = Math.Clamp(dot / (lenA * lenB), -1.0, 1.0);
|
||||
angleDeg = Math.Acos(cosAngle) * 180.0 / Math.PI;
|
||||
}
|
||||
|
||||
// 计算角度弧的起始角和扫过角(用于绘制弧线�
|
||||
double angleA = Math.Atan2(vay, vax) * 180.0 / Math.PI;
|
||||
double angleB = Math.Atan2(vby, vbx) * 180.0 / Math.PI;
|
||||
|
||||
// 确��angleA �angleB 的扫过方�是较�的夹�
|
||||
double sweep = angleB - angleA;
|
||||
if (sweep > 180) sweep -= 360;
|
||||
if (sweep < -180) sweep += 360;
|
||||
|
||||
string angleText = $"{angleDeg:F2} deg";
|
||||
|
||||
OutputData["AngleMeasurementResult"] = true;
|
||||
OutputData["Vertex"] = new Point((int)Math.Round(vx), (int)Math.Round(vy));
|
||||
OutputData["PointA"] = new Point((int)Math.Round(ax), (int)Math.Round(ay));
|
||||
OutputData["PointB"] = new Point((int)Math.Round(bx), (int)Math.Round(by));
|
||||
OutputData["AngleDeg"] = angleDeg;
|
||||
OutputData["ArcStartAngle"] = angleA;
|
||||
OutputData["ArcSweepAngle"] = sweep;
|
||||
OutputData["AngleText"] = angleText;
|
||||
|
||||
_logger.Information("AngleMeasurement: Angle={Angle}, V=({VX},{VY}), A=({AX},{AY}), B=({BX},{BY})",
|
||||
angleText, vx, vy, ax, ay, bx, by);
|
||||
|
||||
return inputImage.Clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件� BgaVoidRateProcessor.cs
|
||||
// æ��è¿°: BGA 空洞率检测算å�(两æ¥è‡ªåŠ¨æ£€æµ‹æ³•ï¼?
|
||||
//
|
||||
// 处��程:
|
||||
// 第一æ?â€?焊ç�ƒå®šä½�: 高斯模糊 â†?Otsuå��å�‘二值化 â†?é—è¿�ç®?â†?轮廓检æµ?â†?圆度过滤 â†?æ¤åœ†æ‹Ÿå�ˆ
|
||||
// 第二æ?â€?气泡检æµ? 焊ç�ƒè½®å»“掩ç � â†?å�Œé˜ˆå€¼åˆ†å‰?â†?轮廓检æµ?â†?é�¢ç§¯è¿‡æ»¤ â†?气泡率计ç®?
|
||||
//
|
||||
// 支æŒ�多边形ROIé™�定检测区域,支æŒ�IPC-7095æ ‡å‡†PASS/FAIL判定
|
||||
// æ£ç‰‡æ¨¡å¼�:焊ç�?暗区域,气泡=亮区åŸ?
|
||||
//
|
||||
// 作� �伟 wei.lw.li@hexagon.com
|
||||
// ============================================================================
|
||||
|
||||
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 BgaVoidRateProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<BgaVoidRateProcessor>();
|
||||
|
||||
public BgaVoidRateProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("BgaVoidRateProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("BgaVoidRateProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
// ── ROI�定区域 ──
|
||||
Parameters.Add("RoiMode", new ProcessorParameter(
|
||||
"RoiMode",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_RoiMode"),
|
||||
typeof(string), "None", null, null,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_RoiMode_Desc"),
|
||||
new string[] { "None", "Polygon" }));
|
||||
|
||||
// 多边形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 });
|
||||
}
|
||||
|
||||
// ── 第一æ¥ï¼šBGA定ä½�å�‚æ•° ──
|
||||
Parameters.Add("BgaMinArea", new ProcessorParameter(
|
||||
"BgaMinArea",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaMinArea"),
|
||||
typeof(int), 500, 10, 1000000,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaMinArea_Desc")));
|
||||
|
||||
Parameters.Add("BgaMaxArea", new ProcessorParameter(
|
||||
"BgaMaxArea",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaMaxArea"),
|
||||
typeof(int), 500000, 100, 10000000,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaMaxArea_Desc")));
|
||||
|
||||
Parameters.Add("BgaBlurSize", new ProcessorParameter(
|
||||
"BgaBlurSize",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaBlurSize"),
|
||||
typeof(int), 5, 1, 31,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaBlurSize_Desc")));
|
||||
|
||||
Parameters.Add("BgaCircularity", new ProcessorParameter(
|
||||
"BgaCircularity",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaCircularity"),
|
||||
typeof(double), 0.5, 0.0, 1.0,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaCircularity_Desc")));
|
||||
|
||||
// ── 第二æ¥ï¼šæ°”泡检测å�‚æ•?──
|
||||
Parameters.Add("MinThreshold", new ProcessorParameter(
|
||||
"MinThreshold",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_MinThreshold"),
|
||||
typeof(int), 128, 0, 255,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_MinThreshold_Desc")));
|
||||
|
||||
Parameters.Add("MaxThreshold", new ProcessorParameter(
|
||||
"MaxThreshold",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_MaxThreshold"),
|
||||
typeof(int), 255, 0, 255,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_MaxThreshold_Desc")));
|
||||
|
||||
Parameters.Add("MinVoidArea", new ProcessorParameter(
|
||||
"MinVoidArea",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_MinVoidArea"),
|
||||
typeof(int), 10, 1, 10000,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_MinVoidArea_Desc")));
|
||||
|
||||
Parameters.Add("VoidLimit", new ProcessorParameter(
|
||||
"VoidLimit",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_VoidLimit"),
|
||||
typeof(double), 25.0, 0.0, 100.0,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_VoidLimit_Desc")));
|
||||
|
||||
Parameters.Add("Thickness", new ProcessorParameter(
|
||||
"Thickness",
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_Thickness"),
|
||||
typeof(int), 2, 1, 10,
|
||||
LocalizationHelper.GetString("BgaVoidRateProcessor_Thickness_Desc")));
|
||||
}
|
||||
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
string roiMode = GetParameter<string>("RoiMode");
|
||||
int bgaMinArea = GetParameter<int>("BgaMinArea");
|
||||
int bgaMaxArea = GetParameter<int>("BgaMaxArea");
|
||||
int bgaBlurSize = GetParameter<int>("BgaBlurSize");
|
||||
double bgaCircularity = GetParameter<double>("BgaCircularity");
|
||||
int minThresh = GetParameter<int>("MinThreshold");
|
||||
int maxThresh = GetParameter<int>("MaxThreshold");
|
||||
int minVoidArea = GetParameter<int>("MinVoidArea");
|
||||
double voidLimit = GetParameter<double>("VoidLimit");
|
||||
int thickness = GetParameter<int>("Thickness");
|
||||
|
||||
// ç¡®ä¿�æ¨¡ç³Šæ ¸ä¸ºå¥‡æ•°
|
||||
if (bgaBlurSize % 2 == 0) bgaBlurSize++;
|
||||
|
||||
OutputData.Clear();
|
||||
int w = inputImage.Width, h = inputImage.Height;
|
||||
|
||||
// 构建ROI掩ç �(é™�定检测区域)
|
||||
Image<Gray, byte>? roiMask = null;
|
||||
if (roiMode == "Polygon")
|
||||
{
|
||||
int polyCount = GetParameter<int>("PolyCount");
|
||||
if (polyCount >= 3)
|
||||
{
|
||||
var pts = new Point[polyCount];
|
||||
for (int i = 0; i < polyCount; i++)
|
||||
pts[i] = new Point(GetParameter<int>($"PolyX{i}"), GetParameter<int>($"PolyY{i}"));
|
||||
roiMask = new Image<Gray, byte>(w, h);
|
||||
using var vop = new VectorOfPoint(pts);
|
||||
using var vvop = new VectorOfVectorOfPoint(vop);
|
||||
CvInvoke.DrawContours(roiMask, vvop, 0, new MCvScalar(255), -1);
|
||||
_logger.Debug("ROI Polygon: {Count} points", polyCount);
|
||||
}
|
||||
}
|
||||
|
||||
OutputData["RoiMode"] = roiMode;
|
||||
OutputData["RoiMask"] = roiMask;
|
||||
|
||||
_logger.Debug("BgaVoidRate ä¸¤æ¥æ³? BgaArea=[{Min},{Max}], Blur={Blur}, Circ={Circ}, Thresh=[{TMin},{TMax}]",
|
||||
bgaMinArea, bgaMaxArea, bgaBlurSize, bgaCircularity, minThresh, maxThresh);
|
||||
|
||||
// ================================================================
|
||||
// 第一æ¥ï¼šè‡ªåŠ¨æ£€æµ‹BGA焊ç�ƒä½�ç½®
|
||||
// ================================================================
|
||||
var bgaResults = DetectBgaBalls(inputImage, bgaBlurSize, bgaMinArea, bgaMaxArea, bgaCircularity, roiMask);
|
||||
|
||||
_logger.Information("第一æ¥å®Œæˆ? 检测到 {Count} 个BGA焊ç�ƒ", bgaResults.Count);
|
||||
|
||||
if (bgaResults.Count == 0)
|
||||
{
|
||||
OutputData["BgaVoidResult"] = true;
|
||||
OutputData["BgaCount"] = 0;
|
||||
OutputData["BgaBalls"] = bgaResults;
|
||||
OutputData["VoidRate"] = 0.0;
|
||||
OutputData["Classification"] = "N/A";
|
||||
OutputData["ResultText"] = "No BGA detected";
|
||||
OutputData["Thickness"] = thickness;
|
||||
OutputData["VoidLimit"] = voidLimit;
|
||||
OutputData["TotalBgaArea"] = 0;
|
||||
OutputData["TotalVoidArea"] = 0;
|
||||
OutputData["TotalVoidCount"] = 0;
|
||||
roiMask?.Dispose();
|
||||
return inputImage.Clone();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 第二æ¥ï¼šåœ¨æ¯�个焊ç�ƒåŒºåŸŸå†…检测气æ³?
|
||||
// ================================================================
|
||||
int totalBgaArea = 0;
|
||||
int totalVoidArea = 0;
|
||||
int totalVoidCount = 0;
|
||||
|
||||
foreach (var bga in bgaResults)
|
||||
{
|
||||
DetectVoidsInBga(inputImage, bga, minThresh, maxThresh, minVoidArea);
|
||||
totalBgaArea += bga.BgaArea;
|
||||
totalVoidArea += bga.VoidPixels;
|
||||
totalVoidCount += bga.Voids.Count;
|
||||
}
|
||||
|
||||
double overallVoidRate = totalBgaArea > 0 ? (double)totalVoidArea / totalBgaArea * 100.0 : 0;
|
||||
string classification = overallVoidRate <= voidLimit ? "PASS" : "FAIL";
|
||||
|
||||
// 检查æ¯�个焊ç�ƒæ˜¯å�¦å�•独超æ ?
|
||||
foreach (var bga in bgaResults)
|
||||
{
|
||||
bga.Classification = bga.VoidRate <= voidLimit ? "PASS" : "FAIL";
|
||||
}
|
||||
|
||||
_logger.Information("第二æ¥å®Œæˆ? 总气泡率={VoidRate:F1}%, 气泡æ•?{Count}, 判定={Class}",
|
||||
overallVoidRate, totalVoidCount, classification);
|
||||
|
||||
// 输出数�
|
||||
OutputData["BgaVoidResult"] = true;
|
||||
OutputData["BgaCount"] = bgaResults.Count;
|
||||
OutputData["BgaBalls"] = bgaResults;
|
||||
OutputData["VoidRate"] = overallVoidRate;
|
||||
OutputData["FillRate"] = 100.0 - overallVoidRate;
|
||||
OutputData["TotalBgaArea"] = totalBgaArea;
|
||||
OutputData["TotalVoidArea"] = totalVoidArea;
|
||||
OutputData["TotalVoidCount"] = totalVoidCount;
|
||||
OutputData["VoidLimit"] = voidLimit;
|
||||
OutputData["Classification"] = classification;
|
||||
OutputData["Thickness"] = thickness;
|
||||
OutputData["ResultText"] = $"Void: {overallVoidRate:F1}% | {classification} | BGA×{bgaResults.Count}";
|
||||
|
||||
roiMask?.Dispose();
|
||||
return inputImage.Clone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 第一æ¥ï¼šè‡ªåŠ¨æ£€æµ‹BGA焊ç�ƒä½�ç½®
|
||||
/// 使用Otsu二值化 + 轮廓检æµ?+ 圆度过滤 + æ¤åœ†æ‹Ÿå�ˆ
|
||||
/// </summary>
|
||||
private List<BgaBallInfo> DetectBgaBalls(Image<Gray, byte> input, int blurSize, int minArea, int maxArea, double minCircularity, Image<Gray, byte>? roiMask)
|
||||
{
|
||||
var results = new List<BgaBallInfo>();
|
||||
int w = input.Width, h = input.Height;
|
||||
|
||||
// 高斯模糊�噪
|
||||
var blurred = new Image<Gray, byte>(w, h);
|
||||
CvInvoke.GaussianBlur(input, blurred, new Size(blurSize, blurSize), 0);
|
||||
|
||||
// Otsu自动二值化(X-Rayæ£ç‰‡ï¼šç„Šç�?暗区域)
|
||||
var binary = new Image<Gray, byte>(w, h);
|
||||
CvInvoke.Threshold(blurred, binary, 0, 255, ThresholdType.Otsu | ThresholdType.BinaryInv);
|
||||
|
||||
// 如果有ROI掩ç �,å�ªä¿�ç•™ROI区域内的二值化结果
|
||||
if (roiMask != null)
|
||||
{
|
||||
CvInvoke.BitwiseAnd(binary, roiMask, binary);
|
||||
}
|
||||
|
||||
// å½¢æ€�å¦é—è¿�ç®—å¡«å……å°�唿´ž
|
||||
var kernel = CvInvoke.GetStructuringElement(ElementShape.Ellipse, new Size(5, 5), new Point(-1, -1));
|
||||
CvInvoke.MorphologyEx(binary, binary, MorphOp.Close, kernel, new Point(-1, -1), 2, BorderType.Default, new MCvScalar(0));
|
||||
|
||||
// 查找轮廓
|
||||
using var contours = new VectorOfVectorOfPoint();
|
||||
using var hierarchy = new Mat();
|
||||
CvInvoke.FindContours(binary, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
|
||||
|
||||
int bgaIndex = 0;
|
||||
for (int i = 0; i < contours.Size; i++)
|
||||
{
|
||||
double area = CvInvoke.ContourArea(contours[i]);
|
||||
if (area < minArea || area > maxArea) continue;
|
||||
|
||||
// 圆度过滤: circularity = 4π × area / perimeter²
|
||||
double perimeter = CvInvoke.ArcLength(contours[i], true);
|
||||
if (perimeter < 1) continue;
|
||||
double circularity = 4.0 * Math.PI * area / (perimeter * perimeter);
|
||||
if (circularity < minCircularity) continue;
|
||||
|
||||
// 需è¦�至å°?个点æ‰�能拟å�ˆæ¤åœ†
|
||||
if (contours[i].Size < 5) continue;
|
||||
|
||||
var ellipse = CvInvoke.FitEllipse(contours[i]);
|
||||
var moments = CvInvoke.Moments(contours[i]);
|
||||
if (moments.M00 < 1) continue;
|
||||
|
||||
bgaIndex++;
|
||||
results.Add(new BgaBallInfo
|
||||
{
|
||||
Index = bgaIndex,
|
||||
CenterX = moments.M10 / moments.M00,
|
||||
CenterY = moments.M01 / moments.M00,
|
||||
FittedEllipse = ellipse,
|
||||
ContourPoints = contours[i].ToArray(),
|
||||
BgaArea = (int)area,
|
||||
Circularity = circularity
|
||||
});
|
||||
}
|
||||
|
||||
// 按�积从大到�排�
|
||||
results.Sort((a, b) => b.BgaArea.CompareTo(a.BgaArea));
|
||||
for (int i = 0; i < results.Count; i++) results[i].Index = i + 1;
|
||||
|
||||
blurred.Dispose();
|
||||
binary.Dispose();
|
||||
kernel.Dispose();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 第二æ¥ï¼šåœ¨å�•个BGA焊ç�ƒåŒºåŸŸå†…检测气æ³?
|
||||
/// 使用焊ç�ƒè½®å»“作为掩ç �,å�Œé˜ˆå€¼åˆ†å‰²æ°”泡区åŸ?
|
||||
/// </summary>
|
||||
private void DetectVoidsInBga(Image<Gray, byte> input, BgaBallInfo bga, int minThresh, int maxThresh, int minVoidArea)
|
||||
{
|
||||
int w = input.Width, h = input.Height;
|
||||
|
||||
// 创建该焊ç�ƒçš„æŽ©ç �
|
||||
var mask = new Image<Gray, byte>(w, h);
|
||||
using (var vop = new VectorOfPoint(bga.ContourPoints))
|
||||
using (var vvop = new VectorOfVectorOfPoint(vop))
|
||||
{
|
||||
CvInvoke.DrawContours(mask, vvop, 0, new MCvScalar(255), -1);
|
||||
}
|
||||
|
||||
int bgaPixels = CvInvoke.CountNonZero(mask);
|
||||
bga.BgaArea = bgaPixels;
|
||||
|
||||
// å�Œé˜ˆå€¼åˆ†å‰²ï¼ˆæ£ç‰‡æ¨¡å¼�:气æ³?亮,ç�°åº¦åœ¨[minThresh, maxThresh]范围内判为气泡)
|
||||
var voidImg = new Image<Gray, byte>(w, h);
|
||||
byte[,,] srcData = input.Data;
|
||||
byte[,,] dstData = voidImg.Data;
|
||||
byte[,,] maskData = mask.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int voidPixels = CvInvoke.CountNonZero(voidImg);
|
||||
bga.VoidPixels = voidPixels;
|
||||
bga.VoidRate = bgaPixels > 0 ? (double)voidPixels / bgaPixels * 100.0 : 0;
|
||||
|
||||
// 检测�个气泡的轮廓
|
||||
using var contours = new VectorOfVectorOfPoint();
|
||||
using var hierarchy = new Mat();
|
||||
CvInvoke.FindContours(voidImg, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
|
||||
|
||||
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;
|
||||
|
||||
bga.Voids.Add(new VoidInfo
|
||||
{
|
||||
Index = bga.Voids.Count + 1,
|
||||
CenterX = moments.M10 / moments.M00,
|
||||
CenterY = moments.M01 / moments.M00,
|
||||
Area = area,
|
||||
AreaPercent = bgaPixels > 0 ? area / bgaPixels * 100.0 : 0,
|
||||
BoundingBox = CvInvoke.BoundingRectangle(contours[i]),
|
||||
ContourPoints = contours[i].ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
// 按�积从大到�排�
|
||||
bga.Voids.Sort((a, b) => b.Area.CompareTo(a.Area));
|
||||
for (int i = 0; i < bga.Voids.Count; i++) bga.Voids[i].Index = i + 1;
|
||||
|
||||
mask.Dispose();
|
||||
voidImg.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// �个BGA焊�信�
|
||||
/// </summary>
|
||||
public class BgaBallInfo
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public double CenterX { get; set; }
|
||||
public double CenterY { get; set; }
|
||||
public RotatedRect FittedEllipse { get; set; }
|
||||
public Point[] ContourPoints { get; set; } = Array.Empty<Point>();
|
||||
public int BgaArea { get; set; }
|
||||
public double Circularity { get; set; }
|
||||
public int VoidPixels { get; set; }
|
||||
public double VoidRate { get; set; }
|
||||
public string Classification { get; set; } = "N/A";
|
||||
public List<VoidInfo> Voids { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// �个气泡信�
|
||||
/// </summary>
|
||||
public class VoidInfo
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public double CenterX { get; set; }
|
||||
public double CenterY { get; set; }
|
||||
public double Area { get; set; }
|
||||
public double AreaPercent { get; set; }
|
||||
public Rectangle BoundingBox { get; set; }
|
||||
public Point[] ContourPoints { get; set; } = Array.Empty<Point>();
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// ============================================================================
|
||||
// Copyright 穢 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// ��辣�? ContourProcessor.cs
|
||||
// �讛膩: 頧桀��交𪄳蝞堒�嚗𣬚鍂鈭擧�瘚见�����曉�銝剔�頧桀�
|
||||
// �蠘�:
|
||||
// - 璉�瘚见㦛�譍葉����刻蔭撱?
|
||||
// - �寞旿�Y妖��凒餈�誘頧桀�
|
||||
// - 霈∠�頧桀����雿閧鸌敺���Y妖��𪂹�踴��葉敹�����亦畆敶Y�嚗?
|
||||
// - 颲枏枂頧桀�靽⊥�靘𥕦�蝏剖���蝙�?
|
||||
// 蝞埈�: �箔�OpenCV��蔭撱𤘪�瘚讠�瘜?
|
||||
// 雿𡏭�? �𦒘� wei.lw.li@hexagon.com
|
||||
// ============================================================================
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 頧桀��交𪄳蝞堒�
|
||||
/// </summary>
|
||||
public class ContourProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<ContourProcessor>();
|
||||
|
||||
public ContourProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("ContourProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("ContourProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
Parameters.Add("TargetColor", new ProcessorParameter(
|
||||
"TargetColor",
|
||||
LocalizationHelper.GetString("ContourProcessor_TargetColor"),
|
||||
typeof(string),
|
||||
"White",
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("ContourProcessor_TargetColor_Desc"),
|
||||
new string[] { "White", "Black" }));
|
||||
|
||||
Parameters.Add("UseThreshold", new ProcessorParameter(
|
||||
"UseThreshold",
|
||||
LocalizationHelper.GetString("ContourProcessor_UseThreshold"),
|
||||
typeof(bool),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("ContourProcessor_UseThreshold_Desc")));
|
||||
|
||||
Parameters.Add("ThresholdValue", new ProcessorParameter(
|
||||
"ThresholdValue",
|
||||
LocalizationHelper.GetString("ContourProcessor_ThresholdValue"),
|
||||
typeof(int),
|
||||
120,
|
||||
0,
|
||||
255,
|
||||
LocalizationHelper.GetString("ContourProcessor_ThresholdValue_Desc")));
|
||||
|
||||
Parameters.Add("UseOtsu", new ProcessorParameter(
|
||||
"UseOtsu",
|
||||
LocalizationHelper.GetString("ContourProcessor_UseOtsu"),
|
||||
typeof(bool),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("ContourProcessor_UseOtsu_Desc")));
|
||||
|
||||
Parameters.Add("MinArea", new ProcessorParameter(
|
||||
"MinArea",
|
||||
LocalizationHelper.GetString("ContourProcessor_MinArea"),
|
||||
typeof(double),
|
||||
10.0,
|
||||
0.0,
|
||||
10000.0,
|
||||
LocalizationHelper.GetString("ContourProcessor_MinArea_Desc")));
|
||||
|
||||
Parameters.Add("MaxArea", new ProcessorParameter(
|
||||
"MaxArea",
|
||||
LocalizationHelper.GetString("ContourProcessor_MaxArea"),
|
||||
typeof(double),
|
||||
100000.0,
|
||||
0.0,
|
||||
1000000.0,
|
||||
LocalizationHelper.GetString("ContourProcessor_MaxArea_Desc")));
|
||||
|
||||
Parameters.Add("Thickness", new ProcessorParameter(
|
||||
"Thickness",
|
||||
LocalizationHelper.GetString("ContourProcessor_Thickness"),
|
||||
typeof(int),
|
||||
2,
|
||||
1,
|
||||
10,
|
||||
LocalizationHelper.GetString("ContourProcessor_Thickness_Desc")));
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
string targetColor = GetParameter<string>("TargetColor");
|
||||
bool useThreshold = GetParameter<bool>("UseThreshold");
|
||||
int thresholdValue = GetParameter<int>("ThresholdValue");
|
||||
bool useOtsu = GetParameter<bool>("UseOtsu");
|
||||
double minArea = GetParameter<double>("MinArea");
|
||||
double maxArea = GetParameter<double>("MaxArea");
|
||||
int thickness = GetParameter<int>("Thickness");
|
||||
|
||||
_logger.Debug("Process started: TargetColor = '{TargetColor}', UseThreshold = {UseThreshold}, ThresholdValue = {ThresholdValue}, UseOtsu = {UseOtsu}",
|
||||
targetColor, useThreshold, thresholdValue, useOtsu);
|
||||
|
||||
OutputData.Clear();
|
||||
|
||||
// �𥕦遣颲枏��曉�����祉鍂鈭𤾸��?
|
||||
Image<Gray, byte> processImage = inputImage.Clone();
|
||||
|
||||
// 甇仿炊1嚗𡁜��𨅯鍳�券��澆��莎����銵䔶��澆�
|
||||
if (useThreshold)
|
||||
{
|
||||
_logger.Debug("Applying threshold processing");
|
||||
Image<Gray, byte> thresholdImage = new Image<Gray, byte>(processImage.Size);
|
||||
|
||||
if (useOtsu)
|
||||
{
|
||||
// 雿輻鍂Otsu�芸𢆡���?
|
||||
CvInvoke.Threshold(processImage, thresholdImage, 0, 255, ThresholdType.Otsu);
|
||||
_logger.Debug("Applied Otsu threshold");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 雿輻鍂�箏����?
|
||||
CvInvoke.Threshold(processImage, thresholdImage, thresholdValue, 255, ThresholdType.Binary);
|
||||
_logger.Debug("Applied binary threshold with value {ThresholdValue}", thresholdValue);
|
||||
}
|
||||
|
||||
// 靽嘥����澆������㦛�讐鍂鈭舘�霂?
|
||||
try
|
||||
{
|
||||
string debugPath = Path.Combine("logs", $"contour_threshold_{DateTime.Now:yyyyMMdd_HHmmss}.png");
|
||||
Directory.CreateDirectory("logs");
|
||||
thresholdImage.Save(debugPath);
|
||||
_logger.Information("Saved threshold image to: {DebugPath}", debugPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Failed to save threshold image for debugging");
|
||||
}
|
||||
|
||||
processImage.Dispose();
|
||||
processImage = thresholdImage;
|
||||
}
|
||||
|
||||
// 甇仿炊2嚗𡁜��𦦵𤌍��糓暺𤏸𠧧�箏�嚗屸�閬��頧砍㦛�?
|
||||
bool isBlackTarget = targetColor != null &&
|
||||
(targetColor.Equals("Black", StringComparison.OrdinalIgnoreCase) ||
|
||||
targetColor.Equals("暺𤏸𠧧", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (isBlackTarget)
|
||||
{
|
||||
_logger.Debug("Inverting image for black region detection");
|
||||
CvInvoke.BitwiseNot(processImage, processImage);
|
||||
|
||||
// 靽嘥�蝧餉蓮�𡒊��曉��其�靚��
|
||||
try
|
||||
{
|
||||
string debugPath = Path.Combine("logs", $"contour_inverted_{DateTime.Now:yyyyMMdd_HHmmss}.png");
|
||||
Directory.CreateDirectory("logs");
|
||||
processImage.Save(debugPath);
|
||||
_logger.Information("Saved inverted image to: {DebugPath}", debugPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Failed to save inverted image for debugging");
|
||||
}
|
||||
}
|
||||
|
||||
// 甇仿炊3嚗𡁏䰻�曇蔭撱?
|
||||
using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint())
|
||||
{
|
||||
Mat hierarchy = new Mat();
|
||||
CvInvoke.FindContours(processImage, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
|
||||
|
||||
_logger.Debug("Found {TotalContours} total contours before filtering", contours.Size);
|
||||
|
||||
List<ContourInfo> contourInfos = new();
|
||||
|
||||
for (int i = 0; i < contours.Size; i++)
|
||||
{
|
||||
double area = CvInvoke.ContourArea(contours[i]);
|
||||
if (area >= minArea && area <= maxArea)
|
||||
{
|
||||
var moments = CvInvoke.Moments(contours[i]);
|
||||
var boundingRect = CvInvoke.BoundingRectangle(contours[i]);
|
||||
double perimeter = CvInvoke.ArcLength(contours[i], true);
|
||||
var circle = CvInvoke.MinEnclosingCircle(contours[i]);
|
||||
|
||||
contourInfos.Add(new ContourInfo
|
||||
{
|
||||
Index = i,
|
||||
Area = area,
|
||||
Perimeter = perimeter,
|
||||
CenterX = moments.M10 / moments.M00,
|
||||
CenterY = moments.M01 / moments.M00,
|
||||
BoundingBox = boundingRect,
|
||||
Points = contours[i].ToArray(),
|
||||
CircleCenter = circle.Center,
|
||||
CircleRadius = circle.Radius
|
||||
});
|
||||
|
||||
_logger.Debug("Contour {Index}: Area = {Area}, Center = ({CenterX:F2}, {CenterY:F2})",
|
||||
i, area, moments.M10 / moments.M00, moments.M01 / moments.M00);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Contour {Index} filtered out: Area = {Area} (not in range {MinArea} - {MaxArea})",
|
||||
i, area, minArea, maxArea);
|
||||
}
|
||||
}
|
||||
|
||||
OutputData["ContourCount"] = contourInfos.Count;
|
||||
OutputData["Contours"] = contourInfos;
|
||||
OutputData["Thickness"] = thickness;
|
||||
|
||||
hierarchy.Dispose();
|
||||
processImage.Dispose();
|
||||
|
||||
_logger.Information("Process completed: TargetColor = '{TargetColor}', Found {ContourCount} contours (filtered from {TotalContours})",
|
||||
targetColor, contourInfos.Count, contours.Size);
|
||||
return inputImage.Clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 頧桀�靽⊥�
|
||||
/// </summary>
|
||||
public class ContourInfo
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public double Area { get; set; }
|
||||
public double Perimeter { get; set; }
|
||||
public double CenterX { get; set; }
|
||||
public double CenterY { get; set; }
|
||||
public Rectangle BoundingBox { get; set; }
|
||||
public Point[] Points { get; set; } = Array.Empty<Point>();
|
||||
public PointF CircleCenter { get; set; }
|
||||
public float CircleRadius { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件� EllipseDetectionProcessor.cs
|
||||
// æ��è¿°: æ¤åœ†æ£€æµ‹ç®—å�,基于轮廓分æž�å’Œæ¤åœ†æ‹Ÿå�ˆæ£€æµ‹å›¾åƒ�ä¸çš„æ¤åœ?
|
||||
// 功能:
|
||||
// - 阈值分�+ 轮廓��
|
||||
// - æ¤åœ†æ‹Ÿå�ˆï¼ˆFitEllipseï¼?
|
||||
// - �积/轴长/离心�拟�误差多维过滤
|
||||
// - 支��阈值分割和 Otsu 自动阈�
|
||||
// 算法: 阈值分�+ OpenCV FitEllipse
|
||||
// 作� �伟 wei.lw.li@hexagon.com
|
||||
// ============================================================================
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// æ¤åœ†æ£€æµ‹ç»“æž?
|
||||
/// </summary>
|
||||
public class EllipseInfo
|
||||
{
|
||||
/// <summary>��</summary>
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>ä¸å¿ƒç‚¹X</summary>
|
||||
public float CenterX { get; set; }
|
||||
|
||||
/// <summary>ä¸å¿ƒç‚¹Y</summary>
|
||||
public float CenterY { get; set; }
|
||||
|
||||
/// <summary>长轴长度</summary>
|
||||
public float MajorAxis { get; set; }
|
||||
|
||||
/// <summary>çŸè½´é•¿åº¦</summary>
|
||||
public float MinorAxis { get; set; }
|
||||
|
||||
/// <summary>旋转角度(度�/summary>
|
||||
public float Angle { get; set; }
|
||||
|
||||
/// <summary>�积</summary>
|
||||
public double Area { get; set; }
|
||||
|
||||
/// <summary>周长</summary>
|
||||
public double Perimeter { get; set; }
|
||||
|
||||
/// <summary>离心çŽ?(0=åœ? 接近1=æ‰�æ¤åœ?</summary>
|
||||
public double Eccentricity { get; set; }
|
||||
|
||||
/// <summary>拟å�ˆè¯¯å·®ï¼ˆåƒ�ç´ ï¼‰</summary>
|
||||
public double FitError { get; set; }
|
||||
|
||||
/// <summary>轮廓点集</summary>
|
||||
public Point[] ContourPoints { get; set; } = Array.Empty<Point>();
|
||||
|
||||
/// <summary>外接矩形</summary>
|
||||
public Rectangle BoundingBox { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// æ¤åœ†æ£€æµ‹å™¨
|
||||
/// </summary>
|
||||
public class EllipseDetector
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<EllipseDetector>();
|
||||
|
||||
public int MinThreshold { get; set; } = 64;
|
||||
public int MaxThreshold { get; set; } = 192;
|
||||
public bool UseOtsu { get; set; } = false;
|
||||
public int MinContourPoints { get; set; } = 30;
|
||||
public double MinArea { get; set; } = 100;
|
||||
public double MaxArea { get; set; } = 1000000;
|
||||
public float MinMajorAxis { get; set; } = 10;
|
||||
public double MaxEccentricity { get; set; } = 0.95;
|
||||
public double MaxFitError { get; set; } = 5.0;
|
||||
public int Thickness { get; set; } = 2;
|
||||
|
||||
/// <summary>执行æ¤åœ†æ£€æµ?/summary>
|
||||
public List<EllipseInfo> Detect(Image<Gray, byte> inputImage, Image<Gray, byte>? roiMask = null)
|
||||
{
|
||||
_logger.Debug("Ellipse detection started: UseOtsu={UseOtsu}, MinThreshold={Min}, MaxThreshold={Max}",
|
||||
UseOtsu, MinThreshold, MaxThreshold);
|
||||
var results = new List<EllipseInfo>();
|
||||
|
||||
using var binary = new Image<Gray, byte>(inputImage.Size);
|
||||
|
||||
if (UseOtsu)
|
||||
{
|
||||
CvInvoke.Threshold(inputImage, binary, MinThreshold, 255, ThresholdType.Otsu);
|
||||
_logger.Debug("Using Otsu auto threshold");
|
||||
}
|
||||
else
|
||||
{
|
||||
// �阈值分割:介于MinThreshold和MaxThreshold之间的为�景(255),其他为背景(0)
|
||||
byte[,,] inputData = inputImage.Data;
|
||||
byte[,,] outputData = binary.Data;
|
||||
int height = inputImage.Height;
|
||||
int width = inputImage.Width;
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
byte pixelValue = inputData[y, x, 0];
|
||||
outputData[y, x, 0] = (pixelValue >= MinThreshold && pixelValue <= MaxThreshold)
|
||||
? (byte)255
|
||||
: (byte)0;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Dual threshold segmentation: MinThreshold={Min}, MaxThreshold={Max}", MinThreshold, MaxThreshold);
|
||||
}
|
||||
|
||||
// 应用ROI掩ç �
|
||||
if (roiMask != null)
|
||||
{
|
||||
CvInvoke.BitwiseAnd(binary, roiMask, binary);
|
||||
}
|
||||
|
||||
using var contours = new VectorOfVectorOfPoint();
|
||||
using var hierarchy = new Mat();
|
||||
CvInvoke.FindContours(binary, contours, hierarchy, RetrType.List, ChainApproxMethod.ChainApproxNone);
|
||||
_logger.Debug("Found {Count} contours", contours.Size);
|
||||
|
||||
int index = 0;
|
||||
for (int i = 0; i < contours.Size; i++)
|
||||
{
|
||||
var contour = contours[i];
|
||||
if (contour.Size < Math.Max(5, MinContourPoints)) continue;
|
||||
|
||||
double area = CvInvoke.ContourArea(contour);
|
||||
if (area < MinArea || area > MaxArea) continue;
|
||||
|
||||
RotatedRect ellipseRect = CvInvoke.FitEllipse(contour);
|
||||
float majorAxis = Math.Max(ellipseRect.Size.Width, ellipseRect.Size.Height);
|
||||
float minorAxis = Math.Min(ellipseRect.Size.Width, ellipseRect.Size.Height);
|
||||
if (majorAxis < MinMajorAxis) continue;
|
||||
|
||||
double eccentricity = 0;
|
||||
if (majorAxis > 0)
|
||||
{
|
||||
double ratio = minorAxis / majorAxis;
|
||||
eccentricity = Math.Sqrt(1.0 - ratio * ratio);
|
||||
}
|
||||
if (eccentricity > MaxEccentricity) continue;
|
||||
|
||||
double fitError = ComputeFitError(contour.ToArray(), ellipseRect);
|
||||
if (fitError > MaxFitError) continue;
|
||||
|
||||
results.Add(new EllipseInfo
|
||||
{
|
||||
Index = index++,
|
||||
CenterX = ellipseRect.Center.X,
|
||||
CenterY = ellipseRect.Center.Y,
|
||||
MajorAxis = majorAxis,
|
||||
MinorAxis = minorAxis,
|
||||
Angle = ellipseRect.Angle,
|
||||
Area = area,
|
||||
Perimeter = CvInvoke.ArcLength(contour, true),
|
||||
Eccentricity = eccentricity,
|
||||
FitError = fitError,
|
||||
ContourPoints = contour.ToArray(),
|
||||
BoundingBox = CvInvoke.BoundingRectangle(contour)
|
||||
});
|
||||
}
|
||||
|
||||
_logger.Information("Ellipse detection completed: detected {Count} ellipses", results.Count);
|
||||
return results;
|
||||
}
|
||||
|
||||
private static double ComputeFitError(Point[] contourPoints, RotatedRect ellipse)
|
||||
{
|
||||
double cx = ellipse.Center.X, cy = ellipse.Center.Y;
|
||||
double a = Math.Max(ellipse.Size.Width, ellipse.Size.Height) / 2.0;
|
||||
double b = Math.Min(ellipse.Size.Width, ellipse.Size.Height) / 2.0;
|
||||
double angleRad = ellipse.Angle * Math.PI / 180.0;
|
||||
double cosA = Math.Cos(angleRad), sinA = Math.Sin(angleRad);
|
||||
if (a < 1e-6) return double.MaxValue;
|
||||
|
||||
double totalError = 0;
|
||||
foreach (var pt in contourPoints)
|
||||
{
|
||||
double dx = pt.X - cx, dy = pt.Y - cy;
|
||||
double localX = dx * cosA + dy * sinA;
|
||||
double localY = -dx * sinA + dy * cosA;
|
||||
double ellipseVal = (localX * localX) / (a * a) + (localY * localY) / (b * b);
|
||||
totalError += Math.Abs(Math.Sqrt(ellipseVal) - 1.0) * Math.Sqrt(a * b);
|
||||
}
|
||||
return totalError / contourPoints.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// æ¤åœ†æ£€æµ‹ç®—å?
|
||||
/// </summary>
|
||||
public class EllipseDetectionProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<EllipseDetectionProcessor>();
|
||||
|
||||
public EllipseDetectionProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("EllipseDetectionProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("EllipseDetectionProcessor_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("EllipseDetectionProcessor_MinThreshold"),
|
||||
typeof(int), 64, 0, 255,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_MinThreshold_Desc")));
|
||||
|
||||
Parameters.Add("MaxThreshold", new ProcessorParameter(
|
||||
"MaxThreshold", LocalizationHelper.GetString("EllipseDetectionProcessor_MaxThreshold"),
|
||||
typeof(int), 192, 0, 255,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_MaxThreshold_Desc")));
|
||||
|
||||
Parameters.Add("UseOtsu", new ProcessorParameter(
|
||||
"UseOtsu", LocalizationHelper.GetString("EllipseDetectionProcessor_UseOtsu"),
|
||||
typeof(bool), false, null, null,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_UseOtsu_Desc")));
|
||||
|
||||
Parameters.Add("MinContourPoints", new ProcessorParameter(
|
||||
"MinContourPoints", LocalizationHelper.GetString("EllipseDetectionProcessor_MinContourPoints"),
|
||||
typeof(int), 30, 5, 1000,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_MinContourPoints_Desc")));
|
||||
|
||||
Parameters.Add("MinArea", new ProcessorParameter(
|
||||
"MinArea", LocalizationHelper.GetString("EllipseDetectionProcessor_MinArea"),
|
||||
typeof(double), 100.0, 0.0, 1000000.0,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_MinArea_Desc")));
|
||||
|
||||
Parameters.Add("MaxArea", new ProcessorParameter(
|
||||
"MaxArea", LocalizationHelper.GetString("EllipseDetectionProcessor_MaxArea"),
|
||||
typeof(double), 1000000.0, 0.0, 10000000.0,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_MaxArea_Desc")));
|
||||
|
||||
Parameters.Add("MaxEccentricity", new ProcessorParameter(
|
||||
"MaxEccentricity", LocalizationHelper.GetString("EllipseDetectionProcessor_MaxEccentricity"),
|
||||
typeof(double), 0.95, 0.0, 1.0,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_MaxEccentricity_Desc")));
|
||||
|
||||
Parameters.Add("MaxFitError", new ProcessorParameter(
|
||||
"MaxFitError", LocalizationHelper.GetString("EllipseDetectionProcessor_MaxFitError"),
|
||||
typeof(double), 5.0, 0.0, 50.0,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_MaxFitError_Desc")));
|
||||
|
||||
Parameters.Add("Thickness", new ProcessorParameter(
|
||||
"Thickness", LocalizationHelper.GetString("EllipseDetectionProcessor_Thickness"),
|
||||
typeof(int), 2, 1, 10,
|
||||
LocalizationHelper.GetString("EllipseDetectionProcessor_Thickness_Desc")));
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
int thickness = GetParameter<int>("Thickness");
|
||||
|
||||
_logger.Debug("Ellipse detection started");
|
||||
OutputData.Clear();
|
||||
|
||||
// 构建多边形ROI掩ç �
|
||||
int polyCount = GetParameter<int>("PolyCount");
|
||||
Image<Gray, byte>? roiMask = null;
|
||||
if (polyCount >= 3)
|
||||
{
|
||||
var pts = new Point[polyCount];
|
||||
for (int i = 0; i < polyCount; i++)
|
||||
pts[i] = new Point(GetParameter<int>($"PolyX{i}"), GetParameter<int>($"PolyY{i}"));
|
||||
roiMask = new Image<Gray, byte>(inputImage.Width, inputImage.Height);
|
||||
using var vop = new VectorOfPoint(pts);
|
||||
using var vvop = new VectorOfVectorOfPoint(vop);
|
||||
CvInvoke.DrawContours(roiMask, vvop, 0, new MCvScalar(255), -1);
|
||||
}
|
||||
|
||||
var detector = new EllipseDetector
|
||||
{
|
||||
MinThreshold = GetParameter<int>("MinThreshold"),
|
||||
MaxThreshold = GetParameter<int>("MaxThreshold"),
|
||||
UseOtsu = GetParameter<bool>("UseOtsu"),
|
||||
MinContourPoints = GetParameter<int>("MinContourPoints"),
|
||||
MinArea = GetParameter<double>("MinArea"),
|
||||
MaxArea = GetParameter<double>("MaxArea"),
|
||||
MaxEccentricity = GetParameter<double>("MaxEccentricity"),
|
||||
MaxFitError = GetParameter<double>("MaxFitError"),
|
||||
Thickness = thickness
|
||||
};
|
||||
|
||||
var ellipses = detector.Detect(inputImage, roiMask);
|
||||
|
||||
OutputData["Ellipses"] = ellipses;
|
||||
OutputData["EllipseCount"] = ellipses.Count;
|
||||
OutputData["Thickness"] = thickness;
|
||||
|
||||
roiMask?.Dispose();
|
||||
_logger.Information("Ellipse detection completed: detected {Count} ellipses", ellipses.Count);
|
||||
return inputImage.Clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件� FillRateProcessor.cs
|
||||
// æ��è¿°: 通å”填锡率测é‡�ç®—å�ï¼ˆå€¾æ–œæŠ•å½±å‡ ä½•æ³•ï¼‰ï¼ŒåŸºäºŽå››æ¤åœ†ROI
|
||||
// 功能:
|
||||
// - æ ·å“�倾斜çº?5°放置,利用投影ä½�移关系计算填锡率
|
||||
// - 四个æ¤åœ†å®šä¹‰ï¼?
|
||||
// E1 = 通å”底部轮廓
|
||||
// E2 = 通å”顶部轮廓
|
||||
// E3 = 填锡起点(与E1��,代�%填锡�
|
||||
// E4 = 填锡终点(锡实际填充到的高度�
|
||||
// - 填锡çŽ?= |E4ä¸å¿ƒ - E3ä¸å¿ƒ| / |E2ä¸å¿ƒ - E1ä¸å¿ƒ| × 100%
|
||||
// - çº¯å‡ ä½•æ–¹æ³•ï¼Œä¸�ä¾�èµ–ç�°åº¦åˆ†æž?
|
||||
// - IPC-610 THT 分级判定(Class 1/2/3�
|
||||
// 算法: 倾斜投影�移比例
|
||||
// 作� �伟 wei.lw.li@hexagon.com
|
||||
// ============================================================================
|
||||
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.Structure;
|
||||
using Serilog;
|
||||
using System.Drawing;
|
||||
using XP.ImageProcessing.Core;
|
||||
|
||||
namespace XP.ImageProcessing.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// 通å”填锡率测é‡�ç®—å�ï¼ˆå€¾æ–œæŠ•å½±å‡ ä½•æ³•ï¼‰
|
||||
/// </summary>
|
||||
public class FillRateProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<FillRateProcessor>();
|
||||
|
||||
public FillRateProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("FillRateProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("FillRateProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
// 四个æ¤åœ†ï¼ˆç”±äº¤äº’控件注入,UIä¸�å�¯è§�)
|
||||
AddEllipseParams("E1", 200, 250, 60, 50, 0); // 底部
|
||||
AddEllipseParams("E2", 220, 180, 60, 50, 0); // 顶部
|
||||
AddEllipseParams("E3", 200, 250, 60, 50, 0); // 填锡起点�E1�
|
||||
AddEllipseParams("E4", 210, 220, 55, 45, 0); // 填锡终点
|
||||
|
||||
Parameters.Add("THTLimit", new ProcessorParameter(
|
||||
"THTLimit",
|
||||
LocalizationHelper.GetString("FillRateProcessor_THTLimit"),
|
||||
typeof(double), 75.0, 0.0, 100.0,
|
||||
LocalizationHelper.GetString("FillRateProcessor_THTLimit_Desc")));
|
||||
|
||||
Parameters.Add("Thickness", new ProcessorParameter(
|
||||
"Thickness",
|
||||
LocalizationHelper.GetString("FillRateProcessor_Thickness"),
|
||||
typeof(int), 2, 1, 10,
|
||||
LocalizationHelper.GetString("FillRateProcessor_Thickness_Desc")));
|
||||
}
|
||||
|
||||
private void AddEllipseParams(string prefix, int cx, int cy, double a, double b, double angle)
|
||||
{
|
||||
Parameters.Add($"{prefix}_CX", new ProcessorParameter($"{prefix}_CX", $"{prefix}_CX", typeof(int), cx, null, null, "") { IsVisible = false });
|
||||
Parameters.Add($"{prefix}_CY", new ProcessorParameter($"{prefix}_CY", $"{prefix}_CY", typeof(int), cy, null, null, "") { IsVisible = false });
|
||||
Parameters.Add($"{prefix}_A", new ProcessorParameter($"{prefix}_A", $"{prefix}_A", typeof(double), a, null, null, "") { IsVisible = false });
|
||||
Parameters.Add($"{prefix}_B", new ProcessorParameter($"{prefix}_B", $"{prefix}_B", typeof(double), b, null, null, "") { IsVisible = false });
|
||||
Parameters.Add($"{prefix}_Angle", new ProcessorParameter($"{prefix}_Angle", $"{prefix}_Angle", typeof(double), angle, null, null, "") { IsVisible = false });
|
||||
}
|
||||
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
double thtLimit = GetParameter<double>("THTLimit");
|
||||
int thickness = GetParameter<int>("Thickness");
|
||||
|
||||
// 获å�–四个æ¤åœ†ä¸å¿ƒ
|
||||
int e1cx = GetParameter<int>("E1_CX"), e1cy = GetParameter<int>("E1_CY");
|
||||
int e2cx = GetParameter<int>("E2_CX"), e2cy = GetParameter<int>("E2_CY");
|
||||
int e3cx = GetParameter<int>("E3_CX"), e3cy = GetParameter<int>("E3_CY");
|
||||
int e4cx = GetParameter<int>("E4_CX"), e4cy = GetParameter<int>("E4_CY");
|
||||
|
||||
// 获å�–æ¤åœ†è½´å�‚数(用于绘制ï¼?
|
||||
double e1a = GetParameter<double>("E1_A"), e1b = GetParameter<double>("E1_B"), e1ang = GetParameter<double>("E1_Angle");
|
||||
double e2a = GetParameter<double>("E2_A"), e2b = GetParameter<double>("E2_B"), e2ang = GetParameter<double>("E2_Angle");
|
||||
double e3a = GetParameter<double>("E3_A"), e3b = GetParameter<double>("E3_B"), e3ang = GetParameter<double>("E3_Angle");
|
||||
double e4a = GetParameter<double>("E4_A"), e4b = GetParameter<double>("E4_B"), e4ang = GetParameter<double>("E4_Angle");
|
||||
|
||||
_logger.Debug("FillRate: E1=({E1X},{E1Y}), E2=({E2X},{E2Y}), E3=({E3X},{E3Y}), E4=({E4X},{E4Y})",
|
||||
e1cx, e1cy, e2cx, e2cy, e3cx, e3cy, e4cx, e4cy);
|
||||
|
||||
OutputData.Clear();
|
||||
|
||||
// 计算通å”全高度的投影ä½�移(E1底部 â†?E2顶部ï¼?
|
||||
double fullDx = e2cx - e1cx;
|
||||
double fullDy = e2cy - e1cy;
|
||||
double fullDistance = Math.Sqrt(fullDx * fullDx + fullDy * fullDy);
|
||||
|
||||
// 计算填锡高度的投影�移(E3起点 �E4终点�
|
||||
double fillDx = e4cx - e3cx;
|
||||
double fillDy = e4cy - e3cy;
|
||||
double fillDistance = Math.Sqrt(fillDx * fillDx + fillDy * fillDy);
|
||||
|
||||
// 填锡�= 填锡�移 / 全高度��
|
||||
double fillRate = fullDistance > 0 ? (fillDistance / fullDistance) * 100.0 : 0;
|
||||
fillRate = Math.Clamp(fillRate, 0, 100);
|
||||
|
||||
// 判定
|
||||
string classification = fillRate >= thtLimit ? "PASS" : "FAIL";
|
||||
|
||||
// å˜å‚¨ç»“æžœ
|
||||
OutputData["FillRateResult"] = true;
|
||||
OutputData["FillRate"] = fillRate;
|
||||
OutputData["VoidRate"] = 100.0 - fillRate;
|
||||
OutputData["FullDistance"] = fullDistance;
|
||||
OutputData["FillDistance"] = fillDistance;
|
||||
OutputData["THTLimit"] = thtLimit;
|
||||
OutputData["Classification"] = classification;
|
||||
OutputData["Thickness"] = thickness;
|
||||
|
||||
// æ¤åœ†å‡ 何(用于绘制)
|
||||
OutputData["E1"] = (new Point(e1cx, e1cy), new Size((int)e1a, (int)e1b), e1ang);
|
||||
OutputData["E2"] = (new Point(e2cx, e2cy), new Size((int)e2a, (int)e2b), e2ang);
|
||||
OutputData["E3"] = (new Point(e3cx, e3cy), new Size((int)e3a, (int)e3b), e3ang);
|
||||
OutputData["E4"] = (new Point(e4cx, e4cy), new Size((int)e4a, (int)e4b), e4ang);
|
||||
|
||||
string resultText = $"{fillRate:F1}% | {classification}";
|
||||
OutputData["ResultText"] = resultText;
|
||||
|
||||
_logger.Information("FillRate (geometric): {Rate}%, {Class}, FullDist={FD:F1}, FillDist={FiD:F1}",
|
||||
fillRate, classification, fullDistance, fillDistance);
|
||||
|
||||
return inputImage.Clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件� LineMeasurementProcessor.cs
|
||||
// æ��è¿°: 直线测é‡�ç®—å�,用于测é‡�图åƒ�ä¸ä¸¤ç‚¹ä¹‹é—´çš„è·�ç¦?
|
||||
// 功能:
|
||||
// - 用户指定两个点å��æ ‡ï¼ˆåƒ�ç´ å��æ ‡ï¼?
|
||||
// - 计算两点之间的欧æ°�è·�离(åƒ�ç´ å�•ä½�ï¼?
|
||||
// - 支æŒ�åƒ�ç´ å°ºå¯¸æ ‡å®šï¼Œè¾“å‡ºå®žé™…ç‰©ç�†è·�ç¦?
|
||||
// - 在图åƒ�上绘制测é‡�çº¿å’Œæ ‡æ³¨
|
||||
// - 输出测é‡�结果供å�Žç»å¤„ç�†ä½¿ç”?
|
||||
// 算法: 欧��离计算
|
||||
// 作� �伟 wei.lw.li@hexagon.com
|
||||
// ============================================================================
|
||||
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.Structure;
|
||||
using Serilog;
|
||||
using System.Drawing;
|
||||
using XP.ImageProcessing.Core;
|
||||
|
||||
namespace XP.ImageProcessing.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// 直线测é‡�ç®—å� - 测é‡�两点之间的è·�ç¦?
|
||||
/// </summary>
|
||||
public class LineMeasurementProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<LineMeasurementProcessor>();
|
||||
|
||||
public LineMeasurementProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("LineMeasurementProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("LineMeasurementProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
Parameters.Add("X1", new ProcessorParameter(
|
||||
"X1",
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_X1"),
|
||||
typeof(int), 100, null, null,
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_X1_Desc"))
|
||||
{ IsVisible = false });
|
||||
|
||||
Parameters.Add("Y1", new ProcessorParameter(
|
||||
"Y1",
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_Y1"),
|
||||
typeof(int), 100, null, null,
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_Y1_Desc"))
|
||||
{ IsVisible = false });
|
||||
|
||||
Parameters.Add("X2", new ProcessorParameter(
|
||||
"X2",
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_X2"),
|
||||
typeof(int), 400, null, null,
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_X2_Desc"))
|
||||
{ IsVisible = false });
|
||||
|
||||
Parameters.Add("Y2", new ProcessorParameter(
|
||||
"Y2",
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_Y2"),
|
||||
typeof(int), 400, null, null,
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_Y2_Desc"))
|
||||
{ IsVisible = false });
|
||||
|
||||
Parameters.Add("PixelSize", new ProcessorParameter(
|
||||
"PixelSize",
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_PixelSize"),
|
||||
typeof(double), 1.0, null, null,
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_PixelSize_Desc")));
|
||||
|
||||
Parameters.Add("Unit", new ProcessorParameter(
|
||||
"Unit",
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_Unit"),
|
||||
typeof(string), "px", null, null,
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_Unit_Desc"),
|
||||
new string[] { "px", "mm", "μm", "cm" }));
|
||||
|
||||
Parameters.Add("Thickness", new ProcessorParameter(
|
||||
"Thickness",
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_Thickness"),
|
||||
typeof(int), 2, 1, 10,
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_Thickness_Desc")));
|
||||
|
||||
Parameters.Add("ShowLabel", new ProcessorParameter(
|
||||
"ShowLabel",
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_ShowLabel"),
|
||||
typeof(bool), true, null, null,
|
||||
LocalizationHelper.GetString("LineMeasurementProcessor_ShowLabel_Desc")));
|
||||
}
|
||||
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
int x1 = GetParameter<int>("X1");
|
||||
int y1 = GetParameter<int>("Y1");
|
||||
int x2 = GetParameter<int>("X2");
|
||||
int y2 = GetParameter<int>("Y2");
|
||||
double pixelSize = GetParameter<double>("PixelSize");
|
||||
string unit = GetParameter<string>("Unit");
|
||||
int thickness = GetParameter<int>("Thickness");
|
||||
bool showLabel = GetParameter<bool>("ShowLabel");
|
||||
|
||||
_logger.Debug("LineMeasurement: ({X1},{Y1}) -> ({X2},{Y2}), PixelSize={PixelSize}, Unit={Unit}",
|
||||
x1, y1, x2, y2, pixelSize, unit);
|
||||
|
||||
OutputData.Clear();
|
||||
|
||||
// é™�制å��æ ‡åœ¨å›¾åƒ�范围内
|
||||
x1 = Math.Clamp(x1, 0, inputImage.Width - 1);
|
||||
y1 = Math.Clamp(y1, 0, inputImage.Height - 1);
|
||||
x2 = Math.Clamp(x2, 0, inputImage.Width - 1);
|
||||
y2 = Math.Clamp(y2, 0, inputImage.Height - 1);
|
||||
|
||||
// 计算åƒ�ç´ è·�离
|
||||
double dx = x2 - x1;
|
||||
double dy = y2 - y1;
|
||||
double pixelDistance = Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 计算实际�离
|
||||
double actualDistance = pixelDistance * pixelSize;
|
||||
|
||||
// 计算角度(相对于水平方��
|
||||
double angleRad = Math.Atan2(dy, dx);
|
||||
double angleDeg = angleRad * 180.0 / Math.PI;
|
||||
|
||||
// å˜å‚¨æµ‹é‡�结果
|
||||
OutputData["MeasurementType"] = "Line";
|
||||
OutputData["Point1"] = new Point(x1, y1);
|
||||
OutputData["Point2"] = new Point(x2, y2);
|
||||
OutputData["PixelDistance"] = pixelDistance;
|
||||
OutputData["ActualDistance"] = actualDistance;
|
||||
OutputData["Unit"] = unit;
|
||||
OutputData["Angle"] = angleDeg;
|
||||
OutputData["Thickness"] = thickness;
|
||||
OutputData["ShowLabel"] = showLabel;
|
||||
|
||||
// 构建测�信�文本
|
||||
string distanceText = unit == "px"
|
||||
? $"{pixelDistance:F2} px"
|
||||
: $"{actualDistance:F4} {unit} ({pixelDistance:F2} px)";
|
||||
|
||||
OutputData["MeasurementText"] = distanceText;
|
||||
|
||||
_logger.Information("LineMeasurement completed: Distance={Distance}, Angle={Angle:F2}°",
|
||||
distanceText, angleDeg);
|
||||
|
||||
return inputImage.Clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件� PointToLineProcessor.cs
|
||||
// æ��è¿°: 点到直线è·�离测é‡�ç®—å�
|
||||
// 功能:
|
||||
// - 用户定义一�直线(两个端点)和一个测�点
|
||||
// - 计算测�点到直线的垂直��
|
||||
// - 支æŒ�åƒ�ç´ å°ºå¯¸æ ‡å®šè¾“å‡ºç‰©ç�†è·�离
|
||||
// - 在图åƒ�上绘制直线ã€�测é‡�点ã€�垂足和è·�ç¦»æ ‡æ³¨
|
||||
// 算法: 点到直线�离公�
|
||||
// 作� �伟 wei.lw.li@hexagon.com
|
||||
// ============================================================================
|
||||
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.Structure;
|
||||
using Serilog;
|
||||
using System.Drawing;
|
||||
using XP.ImageProcessing.Core;
|
||||
|
||||
namespace XP.ImageProcessing.Processors;
|
||||
|
||||
public class PointToLineProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<PointToLineProcessor>();
|
||||
|
||||
public PointToLineProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("PointToLineProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("PointToLineProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
// 直线两端�+ 测�点(由交互控件注入)
|
||||
Parameters.Add("L1X", new ProcessorParameter("L1X", "L1X", typeof(int), 100, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("L1Y", new ProcessorParameter("L1Y", "L1Y", typeof(int), 200, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("L2X", new ProcessorParameter("L2X", "L2X", typeof(int), 400, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("L2Y", new ProcessorParameter("L2Y", "L2Y", typeof(int), 200, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("PX", new ProcessorParameter("PX", "PX", typeof(int), 250, null, null, "") { IsVisible = false });
|
||||
Parameters.Add("PY", new ProcessorParameter("PY", "PY", typeof(int), 100, null, null, "") { IsVisible = false });
|
||||
|
||||
Parameters.Add("PixelSize", new ProcessorParameter(
|
||||
"PixelSize",
|
||||
LocalizationHelper.GetString("PointToLineProcessor_PixelSize"),
|
||||
typeof(double), 1.0, null, null,
|
||||
LocalizationHelper.GetString("PointToLineProcessor_PixelSize_Desc")));
|
||||
|
||||
Parameters.Add("Unit", new ProcessorParameter(
|
||||
"Unit",
|
||||
LocalizationHelper.GetString("PointToLineProcessor_Unit"),
|
||||
typeof(string), "px", null, null,
|
||||
LocalizationHelper.GetString("PointToLineProcessor_Unit_Desc"),
|
||||
new string[] { "px", "mm", "μm", "cm" }));
|
||||
|
||||
Parameters.Add("Thickness", new ProcessorParameter(
|
||||
"Thickness",
|
||||
LocalizationHelper.GetString("PointToLineProcessor_Thickness"),
|
||||
typeof(int), 2, 1, 10,
|
||||
LocalizationHelper.GetString("PointToLineProcessor_Thickness_Desc")));
|
||||
}
|
||||
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
int l1x = GetParameter<int>("L1X"), l1y = GetParameter<int>("L1Y");
|
||||
int l2x = GetParameter<int>("L2X"), l2y = GetParameter<int>("L2Y");
|
||||
int px = GetParameter<int>("PX"), py = GetParameter<int>("PY");
|
||||
double pixelSize = GetParameter<double>("PixelSize");
|
||||
string unit = GetParameter<string>("Unit");
|
||||
int thickness = GetParameter<int>("Thickness");
|
||||
|
||||
OutputData.Clear();
|
||||
|
||||
// 点到直线�离公�: |AB × AP| / |AB|
|
||||
double abx = l2x - l1x, aby = l2y - l1y;
|
||||
double abLen = Math.Sqrt(abx * abx + aby * aby);
|
||||
|
||||
double pixelDistance = 0;
|
||||
int footX = px, footY = py;
|
||||
|
||||
if (abLen > 0.001)
|
||||
{
|
||||
// �积求��
|
||||
double cross = Math.Abs(abx * (l1y - py) - aby * (l1x - px));
|
||||
pixelDistance = cross / abLen;
|
||||
|
||||
// 垂足: 投影�数 t = AP·AB / |AB|²
|
||||
double apx = px - l1x, apy = py - l1y;
|
||||
double t = (apx * abx + apy * aby) / (abLen * abLen);
|
||||
footX = (int)(l1x + t * abx);
|
||||
footY = (int)(l1y + t * aby);
|
||||
OutputData["ProjectionT"] = t;
|
||||
}
|
||||
|
||||
double actualDistance = pixelDistance * pixelSize;
|
||||
|
||||
string distanceText = unit == "px"
|
||||
? $"{pixelDistance:F2} px"
|
||||
: $"{actualDistance:F4} {unit} ({pixelDistance:F2} px)";
|
||||
|
||||
OutputData["PointToLineResult"] = true;
|
||||
OutputData["Line1"] = new Point(l1x, l1y);
|
||||
OutputData["Line2"] = new Point(l2x, l2y);
|
||||
OutputData["MeasurePoint"] = new Point(px, py);
|
||||
OutputData["FootPoint"] = new Point(footX, footY);
|
||||
OutputData["PixelDistance"] = pixelDistance;
|
||||
OutputData["ActualDistance"] = actualDistance;
|
||||
OutputData["Unit"] = unit;
|
||||
OutputData["Thickness"] = thickness;
|
||||
OutputData["DistanceText"] = distanceText;
|
||||
|
||||
_logger.Information("PointToLine: Distance={Dist}, Foot=({FX},{FY})", distanceText, footX, footY);
|
||||
|
||||
return inputImage.Clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// ============================================================================
|
||||
// 文件� 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>();
|
||||
}
|
||||
Reference in New Issue
Block a user