合并图像处理库,删除图像lib库

This commit is contained in:
李伟
2026-04-13 13:40:37 +08:00
parent 2a762396d5
commit c7ce4ea6a1
105 changed files with 16341 additions and 133 deletions
@@ -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 ImageProcessing.Core;
using Serilog;
using System.Drawing;
namespace 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注入,不可见,最多支持32个点)
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;
// 需要至少5个点才能拟合椭圆
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>();
}