feat: 改进BGA焊点和气泡检测算法,提升复杂背景下的检测精度

- 焊点检测:增加开运算去噪、闭运算改为1次减少粘连、提高默认圆度阈值0.6

- 新增焊球最小中心距过滤和面积一致性过滤,去除粘连和背景误检

- 气泡检测:新增自适应阈值,基于焊球内部灰度分布自动计算阈值

- BGA检测面板增加新参数控件:自适应阈值开关、阈值百分位、中心距、面积一致性
This commit is contained in:
wei.lw.li
2026-07-21 13:31:51 +08:00
parent 26a0f5bf00
commit 38471bf510
3 changed files with 142 additions and 15 deletions
@@ -82,10 +82,30 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
Parameters.Add("BgaCircularity", new ProcessorParameter(
"BgaCircularity",
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaCircularity"),
typeof(double), 0.5, 0.0, 1.0,
typeof(double), 0.6, 0.0, 1.0,
LocalizationHelper.GetString("BgaVoidRateProcessor_BgaCircularity_Desc")));
Parameters.Add("BgaMinCenterDistance", new ProcessorParameter(
"BgaMinCenterDistance",
"焊球最小中心距", typeof(int), 0, 0, 5000,
"焊球中心之间的最小距离(像素),0=不启用。用于去除粘连和背景误检"));
Parameters.Add("BgaAreaConsistency", new ProcessorParameter(
"BgaAreaConsistency",
"焊球面积一致性", typeof(double), 0.5, 0.0, 1.0,
"焊球面积一致性系数(0-1),去除与均值偏差过大的离群焊球。0=不启用"));
// ── 第二步:气泡检测参数(阈值 0-65535)──
Parameters.Add("UseAdaptiveThreshold", new ProcessorParameter(
"UseAdaptiveThreshold",
"自适应阈值", typeof(bool), false, null, null,
"启用自适应阈值后,每个焊球的气泡阈值根据其内部灰度分布自动计算"));
Parameters.Add("VoidThresholdPercentile", new ProcessorParameter(
"VoidThresholdPercentile",
"气泡阈值百分位", typeof(double), 80.0, 0.0, 100.0,
"自适应阈值百分位(0-100),例如80表示取焊球内部灰度第80百分位作为气泡阈值"));
Parameters.Add("MinThreshold", new ProcessorParameter(
"MinThreshold",
LocalizationHelper.GetString("BgaVoidRateProcessor_MinThreshold"),
@@ -131,6 +151,10 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
double bgaCircularity = GetParameter<double>("BgaCircularity");
int bgaThreshLow = GetParameter<int>("BgaThresholdLow");
int bgaThreshHigh = GetParameter<int>("BgaThresholdHigh");
int bgaMinCenterDist = GetParameter<int>("BgaMinCenterDistance");
double bgaAreaConsistency = GetParameter<double>("BgaAreaConsistency");
bool useAdaptiveThreshold = GetParameter<bool>("UseAdaptiveThreshold");
double voidThresholdPercentile = GetParameter<double>("VoidThresholdPercentile");
int minThresh = GetParameter<int>("MinThreshold");
int maxThresh = GetParameter<int>("MaxThreshold");
int minVoidArea = GetParameter<int>("MinVoidArea");
@@ -166,7 +190,7 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
// 第一步:BGA焊球定位(在 16 位图上做双阈值分割)
// ================================================================
var bgaResults = DetectBgaBalls(inputImage, bgaBlurSize, bgaMinArea, bgaMaxArea,
bgaCircularity, bgaThreshLow, bgaThreshHigh, roiMask);
bgaCircularity, bgaThreshLow, bgaThreshHigh, bgaMinCenterDist, bgaAreaConsistency, roiMask);
_logger.Information("第一步完成: 检测到 {Count} 个BGA焊球", bgaResults.Count);
@@ -196,7 +220,8 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
foreach (var bga in bgaResults)
{
DetectVoidsInBga(inputImage, bga, minThresh, maxThresh, minVoidArea);
DetectVoidsInBga(inputImage, bga, minThresh, maxThresh, minVoidArea,
useAdaptiveThreshold, voidThresholdPercentile);
totalBgaArea += bga.BgaArea;
totalVoidArea += bga.VoidPixels;
totalVoidCount += bga.Voids.Count;
@@ -229,12 +254,14 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
}
/// <summary>
/// 第一步:BGA焊球定位(16 位双阈值分割 → 8 位二值图 → FindContours
/// 第一步:BGA焊球定位(16 位双阈值分割 → 8 位二值图 → 形态学 → FindContours → 过滤
/// 改进:增加开运算去噪、闭运算1次防粘连、距离过滤、面积一致性过滤
/// </summary>
private List<BgaBallInfo> DetectBgaBalls(
Image<Gray, ushort> input, int blurSize,
int minArea, int maxArea, double minCircularity,
int threshLow, int threshHigh, Image<Gray, byte>? roiMask)
int threshLow, int threshHigh, int minCenterDist, double areaConsistency,
Image<Gray, byte>? roiMask)
{
var results = new List<BgaBallInfo>();
int w = input.Width, h = input.Height;
@@ -258,9 +285,13 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
if (roiMask != null)
CvInvoke.BitwiseAnd(binary, roiMask, binary);
// 形态学闭运算
using 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));
// 开运算去除小噪点(先腐蚀后膨胀)
CvInvoke.MorphologyEx(binary, binary, MorphOp.Open, kernel, new Point(-1, -1), 1, BorderType.Default, new MCvScalar(0));
// 闭运算填补焊球内部小孔(迭代1次,减少粘连)
CvInvoke.MorphologyEx(binary, binary, MorphOp.Close, kernel, new Point(-1, -1), 1, BorderType.Default, new MCvScalar(0));
// 轮廓检测
using var contours = new VectorOfVectorOfPoint();
@@ -297,6 +328,39 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
});
}
// ── 距离过滤:去除中心距过近的粘连轮廓 ──
if (minCenterDist > 0 && results.Count > 1)
{
var filtered = new List<BgaBallInfo>();
foreach (var bga in results)
{
bool tooClose = false;
foreach (var other in filtered)
{
double dx = bga.CenterX - other.CenterX;
double dy = bga.CenterY - other.CenterY;
double dist = Math.Sqrt(dx * dx + dy * dy);
if (dist < minCenterDist)
{
tooClose = true;
break;
}
}
if (!tooClose)
filtered.Add(bga);
}
results = filtered;
}
// ── 面积一致性过滤:去除与均值偏差过大的离群焊球 ──
if (areaConsistency > 0 && results.Count > 2)
{
double avgArea = results.Average(r => r.BgaArea);
double minAllowed = avgArea * (1.0 - areaConsistency);
double maxAllowed = avgArea * (1.0 + areaConsistency);
results = results.Where(r => r.BgaArea >= minAllowed && r.BgaArea <= maxAllowed).ToList();
}
results.Sort((a, b) => b.BgaArea.CompareTo(a.BgaArea));
for (int i = 0; i < results.Count; i++) results[i].Index = i + 1;
@@ -308,8 +372,9 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
/// <summary>
/// 第二步:在焊球区域内检测气泡(16 位双阈值分割 → 8 位二值图 → FindContours
/// 改进:支持自适应阈值,基于焊球内部灰度分布自动计算气泡阈值
/// </summary>
private void DetectVoidsInBga(Image<Gray, ushort> input, BgaBallInfo bga, int minThresh, int maxThresh, int minVoidArea)
private void DetectVoidsInBga(Image<Gray, ushort> input, BgaBallInfo bga, int minThresh, int maxThresh, int minVoidArea, bool useAdaptiveThreshold, double thresholdPercentile)
{
int w = input.Width, h = input.Height;
@@ -324,19 +389,45 @@ public class BgaVoidRateProcessor : ImageProcessorBase<ushort>
int bgaPixels = CvInvoke.CountNonZero(mask);
bga.BgaArea = bgaPixels;
// 确定气泡阈值
int effectiveMinThresh = minThresh;
int effectiveMaxThresh = maxThresh;
if (useAdaptiveThreshold && bgaPixels > 0)
{
// 收集焊球内部所有像素灰度值
var grayValues = new List<ushort>();
var srcData = input.Data;
var maskData = mask.Data;
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
if (maskData[y, x, 0] > 0)
grayValues.Add(srcData[y, x, 0]);
if (grayValues.Count > 0)
{
grayValues.Sort();
int percentileIndex = (int)(grayValues.Count * thresholdPercentile / 100.0);
percentileIndex = Math.Min(percentileIndex, grayValues.Count - 1);
effectiveMinThresh = grayValues[percentileIndex];
effectiveMaxThresh = 65535;
}
}
// 在 16 位图上做双阈值分割,输出 8 位二值图
var voidImg = new Image<Gray, byte>(w, h);
var srcData = input.Data;
var srcData2 = input.Data;
var dstData = voidImg.Data;
var maskData = mask.Data;
var maskData2 = mask.Data;
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
{
if (maskData[y, x, 0] > 0)
if (maskData2[y, x, 0] > 0)
{
ushort val = srcData[y, x, 0];
dstData[y, x, 0] = (val >= minThresh && val <= maxThresh) ? (byte)255 : (byte)0;
ushort val = srcData2[y, x, 0];
dstData[y, x, 0] = (val >= effectiveMinThresh && val <= effectiveMaxThresh) ? (byte)255 : (byte)0;
}
}
@@ -79,9 +79,15 @@ namespace XplorePlane.ViewModels.ImageProcessing
private int _blurSize = 5;
public int BlurSize { get => _blurSize; set => SetProperty(ref _blurSize, value); }
private double _circularity = 0.5;
private double _circularity = 0.6;
public double Circularity { get => _circularity; set => SetProperty(ref _circularity, value); }
private int _bgaMinCenterDistance = 0;
public int BgaMinCenterDistance { get => _bgaMinCenterDistance; set => SetProperty(ref _bgaMinCenterDistance, value); }
private double _bgaAreaConsistency = 0.5;
public double BgaAreaConsistency { get => _bgaAreaConsistency; set => SetProperty(ref _bgaAreaConsistency, value); }
// BGA定位阈值参数
private int _bgaThresholdLow = 0;
public int BgaThresholdLow { get => _bgaThresholdLow; set => SetProperty(ref _bgaThresholdLow, value); }
@@ -96,6 +102,12 @@ namespace XplorePlane.ViewModels.ImageProcessing
private int _maxThreshold = 65535;
public int MaxThreshold { get => _maxThreshold; set => SetProperty(ref _maxThreshold, value); }
private bool _useAdaptiveThreshold = false;
public bool UseAdaptiveThreshold { get => _useAdaptiveThreshold; set => SetProperty(ref _useAdaptiveThreshold, value); }
private double _voidThresholdPercentile = 80.0;
public double VoidThresholdPercentile { get => _voidThresholdPercentile; set => SetProperty(ref _voidThresholdPercentile, value); }
private int _minVoidArea = 10;
public int MinVoidArea { get => _minVoidArea; set => SetProperty(ref _minVoidArea, value); }
@@ -315,6 +327,10 @@ namespace XplorePlane.ViewModels.ImageProcessing
processor.SetParameter("BgaCircularity", Circularity);
processor.SetParameter("BgaThresholdLow", BgaThresholdLow);
processor.SetParameter("BgaThresholdHigh", BgaThresholdHigh);
processor.SetParameter("BgaMinCenterDistance", BgaMinCenterDistance);
processor.SetParameter("BgaAreaConsistency", BgaAreaConsistency);
processor.SetParameter("UseAdaptiveThreshold", UseAdaptiveThreshold);
processor.SetParameter("VoidThresholdPercentile", VoidThresholdPercentile);
processor.SetParameter("MinThreshold", MinThreshold);
processor.SetParameter("MaxThreshold", MaxThreshold);
processor.SetParameter("MinVoidArea", MinVoidArea);
@@ -471,6 +487,10 @@ namespace XplorePlane.ViewModels.ImageProcessing
parameters["BgaCircularity"] = Circularity;
parameters["BgaThresholdLow"] = BgaThresholdLow;
parameters["BgaThresholdHigh"] = BgaThresholdHigh;
parameters["BgaMinCenterDistance"] = BgaMinCenterDistance;
parameters["BgaAreaConsistency"] = BgaAreaConsistency;
parameters["UseAdaptiveThreshold"] = UseAdaptiveThreshold;
parameters["VoidThresholdPercentile"] = VoidThresholdPercentile;
parameters["MinThreshold"] = MinThreshold;
parameters["MaxThreshold"] = MaxThreshold;
parameters["MinVoidArea"] = MinVoidArea;
@@ -114,10 +114,20 @@
<Slider Minimum="1" Maximum="31" Value="{Binding BlurSize}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="圆度阈值" Style="{StaticResource ParamLabel}" />
<DockPanel>
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding Circularity, StringFormat=F2, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="1" Value="{Binding Circularity}" SmallChange="0.01" LargeChange="0.1" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="焊球最小中心距" Style="{StaticResource ParamLabel}" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding BgaMinCenterDistance, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="500" Value="{Binding BgaMinCenterDistance}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="焊球面积一致性" Style="{StaticResource ParamLabel}" />
<DockPanel>
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding BgaAreaConsistency, StringFormat=F2, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="1" Value="{Binding BgaAreaConsistency}" SmallChange="0.01" LargeChange="0.1" VerticalAlignment="Center" />
</DockPanel>
</StackPanel>
</Border>
@@ -125,6 +135,12 @@
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="气泡检测参数" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,8" />
<CheckBox IsChecked="{Binding UseAdaptiveThreshold}" Content="使用自适应阈值" FontSize="11" Margin="0,0,0,6" Foreground="#555" />
<TextBlock Text="气泡阈值百分位" Style="{StaticResource ParamLabel}" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding VoidThresholdPercentile, StringFormat=F1, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="100" Value="{Binding VoidThresholdPercentile}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="最小灰度阈值" Style="{StaticResource ParamLabel}" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding MinThreshold, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />