403 lines
16 KiB
C#
403 lines
16 KiB
C#
// ============================================================================
|
|
// 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>();
|
|
} |