新增 QFN 一体检测算子,串联模板匹配与双路空洞检测。
将模板定位、中心 ROI 对齐、中心焊盘空洞和引脚空洞检测整合到单算子中,并输出统一判定结果,便于快速验证完整流程。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件名: QfnAutoDetectionProcessor.cs
|
||||
// 描述: QFN 一体检测算子(模板匹配对齐 + 中心空洞 + 引脚空洞)
|
||||
// ============================================================================
|
||||
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.Structure;
|
||||
using Serilog;
|
||||
using XP.ImageProcessing.Core;
|
||||
using XP.ImageProcessing.Core.Alignment;
|
||||
|
||||
namespace XP.ImageProcessing.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// 一体化 QFN 检测:先模板匹配获取位姿,再将中心 ROI 对齐后做中心空洞检测,最后做引脚空洞检测并汇总判定。
|
||||
/// </summary>
|
||||
public sealed class QfnAutoDetectionProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<QfnAutoDetectionProcessor>();
|
||||
|
||||
public QfnAutoDetectionProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("QfnAutoDetectionProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("QfnAutoDetectionProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
// 模板匹配参数
|
||||
Parameters.Add("TemplatePath", new ProcessorParameter("TemplatePath", "TemplatePath", typeof(string), string.Empty, null, null, ""));
|
||||
Parameters.Add("ModelPath", new ProcessorParameter("ModelPath", "ModelPath", typeof(string), string.Empty, null, null, ""));
|
||||
Parameters.Add("MatchThreshold", new ProcessorParameter("MatchThreshold", "MatchThreshold", typeof(double), 0.75, 0.0, 1.0, ""));
|
||||
Parameters.Add("ToleranceAngle", new ProcessorParameter("ToleranceAngle", "ToleranceAngle", typeof(double), 0.0, 0.0, 180.0, ""));
|
||||
Parameters.Add("MaxOverlap", new ProcessorParameter("MaxOverlap", "MaxOverlap", typeof(double), 0.3, 0.0, 1.0, ""));
|
||||
Parameters.Add("MinReduceArea", new ProcessorParameter("MinReduceArea", "MinReduceArea", typeof(int), 256, 64, 4096, ""));
|
||||
Parameters.Add("UseSIMD", new ProcessorParameter("UseSIMD", "UseSIMD", typeof(bool), true, null, null, ""));
|
||||
Parameters.Add("UseSubPixel", new ProcessorParameter("UseSubPixel", "UseSubPixel", typeof(bool), false, null, null, ""));
|
||||
Parameters.Add("MatchIndex", new ProcessorParameter("MatchIndex", "MatchIndex", typeof(int), 0, 0, 99, ""));
|
||||
|
||||
// 对齐基准位姿(示教)
|
||||
Parameters.Add("RefCenterX", new ProcessorParameter("RefCenterX", "RefCenterX", typeof(double), 0.0, null, null, ""));
|
||||
Parameters.Add("RefCenterY", new ProcessorParameter("RefCenterY", "RefCenterY", typeof(double), 0.0, null, null, ""));
|
||||
Parameters.Add("RefAngle", new ProcessorParameter("RefAngle", "RefAngle", typeof(double), 0.0, -180.0, 180.0, ""));
|
||||
|
||||
// 中心焊盘示教多边形(会随位姿对齐)
|
||||
AddHiddenPolygonParams("Center");
|
||||
|
||||
// 中心空洞参数(映射到 VoidMeasurementProcessor)
|
||||
Parameters.Add("CenterMinThreshold", new ProcessorParameter("CenterMinThreshold", "CenterMinThreshold", typeof(int), 128, 0, 255, ""));
|
||||
Parameters.Add("CenterMaxThreshold", new ProcessorParameter("CenterMaxThreshold", "CenterMaxThreshold", typeof(int), 255, 0, 255, ""));
|
||||
Parameters.Add("CenterMinVoidArea", new ProcessorParameter("CenterMinVoidArea", "CenterMinVoidArea", typeof(int), 10, 1, 100000, ""));
|
||||
Parameters.Add("CenterMergeRadius", new ProcessorParameter("CenterMergeRadius", "CenterMergeRadius", typeof(int), 3, 0, 30, ""));
|
||||
Parameters.Add("CenterBlurSize", new ProcessorParameter("CenterBlurSize", "CenterBlurSize", typeof(int), 3, 1, 31, ""));
|
||||
Parameters.Add("CenterVoidLimit", new ProcessorParameter("CenterVoidLimit", "CenterVoidLimit", typeof(double), 25.0, 0.0, 100.0, ""));
|
||||
|
||||
// 引脚检测参数(映射到 QfnLeadPadVoidProcessor)
|
||||
Parameters.Add("LeadRoiMode", new ProcessorParameter("LeadRoiMode", "LeadRoiMode", typeof(string), "None", null, null, "", new[] { "None", "Polygon" }));
|
||||
Parameters.Add("AlignLeadRoiWithTemplate", new ProcessorParameter("AlignLeadRoiWithTemplate", "AlignLeadRoiWithTemplate", typeof(bool), false, null, null, ""));
|
||||
AddHiddenPolygonParams("Lead");
|
||||
|
||||
Parameters.Add("LeadPadBlurSize", new ProcessorParameter("LeadPadBlurSize", "LeadPadBlurSize", typeof(int), 5, 1, 31, ""));
|
||||
Parameters.Add("LeadPadThresholdLow", new ProcessorParameter("LeadPadThresholdLow", "LeadPadThresholdLow", typeof(int), 0, 0, 255, ""));
|
||||
Parameters.Add("LeadPadThresholdHigh", new ProcessorParameter("LeadPadThresholdHigh", "LeadPadThresholdHigh", typeof(int), 120, 0, 255, ""));
|
||||
Parameters.Add("LeadPadMorphKernel", new ProcessorParameter("LeadPadMorphKernel", "LeadPadMorphKernel", typeof(int), 5, 1, 31, ""));
|
||||
Parameters.Add("LeadMinPadArea", new ProcessorParameter("LeadMinPadArea", "LeadMinPadArea", typeof(int), 200, 10, 1000000, ""));
|
||||
Parameters.Add("LeadMaxPadArea", new ProcessorParameter("LeadMaxPadArea", "LeadMaxPadArea", typeof(int), 100000, 100, 10000000, ""));
|
||||
Parameters.Add("LeadPadAspectRatioMin", new ProcessorParameter("LeadPadAspectRatioMin", "LeadPadAspectRatioMin", typeof(double), 1.2, 0.1, 20.0, ""));
|
||||
Parameters.Add("LeadVoidThresholdLow", new ProcessorParameter("LeadVoidThresholdLow", "LeadVoidThresholdLow", typeof(int), 128, 0, 255, ""));
|
||||
Parameters.Add("LeadVoidThresholdHigh", new ProcessorParameter("LeadVoidThresholdHigh", "LeadVoidThresholdHigh", typeof(int), 255, 0, 255, ""));
|
||||
Parameters.Add("LeadMinVoidArea", new ProcessorParameter("LeadMinVoidArea", "LeadMinVoidArea", typeof(int), 5, 1, 10000, ""));
|
||||
Parameters.Add("LeadVoidMergeRadius", new ProcessorParameter("LeadVoidMergeRadius", "LeadVoidMergeRadius", typeof(int), 2, 0, 20, ""));
|
||||
Parameters.Add("LeadVoidRateLimit", new ProcessorParameter("LeadVoidRateLimit", "LeadVoidRateLimit", typeof(double), 50.0, 0.0, 100.0, ""));
|
||||
Parameters.Add("LeadMinQualifiedPadArea", new ProcessorParameter("LeadMinQualifiedPadArea", "LeadMinQualifiedPadArea", typeof(int), 1000, 0, 1000000, ""));
|
||||
Parameters.Add("LeadThickness", new ProcessorParameter("LeadThickness", "LeadThickness", typeof(int), 2, 1, 10, ""));
|
||||
}
|
||||
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
OutputData.Clear();
|
||||
var output = inputImage.Clone();
|
||||
|
||||
try
|
||||
{
|
||||
// 1) 模板匹配
|
||||
var templateResult = RunTemplateMatching(inputImage, out var templateOutput);
|
||||
OutputData["TemplateOutput"] = templateOutput;
|
||||
|
||||
if (!templateResult.Success)
|
||||
{
|
||||
FillFailResult("FAIL_MATCH", templateResult.ErrorMessage ?? "Template matching failed.");
|
||||
return output;
|
||||
}
|
||||
|
||||
// 2) 中心 ROI 对齐
|
||||
var referencePose = new Pose2D(
|
||||
GetParameter<double>("RefCenterX"),
|
||||
GetParameter<double>("RefCenterY"),
|
||||
GetParameter<double>("RefAngle"));
|
||||
|
||||
var centerTeach = ReadPrefixedPolygon("Center");
|
||||
var centerAlign = RoiAlignmentApplier.Apply(referencePose, centerTeach, templateResult.MeasuredPose);
|
||||
OutputData["CenterAlignment"] = centerAlign;
|
||||
if (!centerAlign.Success)
|
||||
{
|
||||
FillFailResult("FAIL_ALIGN_CENTER", centerAlign.ErrorMessage ?? "Center ROI alignment failed.");
|
||||
return output;
|
||||
}
|
||||
|
||||
// 3) 中心空洞
|
||||
var centerDetection = RunCenterVoid(inputImage, centerAlign);
|
||||
OutputData["CenterOutput"] = centerDetection.Output;
|
||||
if (!centerDetection.Success)
|
||||
{
|
||||
FillFailResult("FAIL_CENTER", centerDetection.ErrorMessage ?? "Center void detection failed.");
|
||||
return output;
|
||||
}
|
||||
|
||||
// 4) 引脚空洞
|
||||
var leadDetection = RunLeadVoid(inputImage, templateResult.MeasuredPose);
|
||||
OutputData["LeadOutput"] = leadDetection.Output;
|
||||
if (!leadDetection.Success)
|
||||
{
|
||||
FillFailResult("FAIL_LEAD", leadDetection.ErrorMessage ?? "Lead void detection failed.");
|
||||
return output;
|
||||
}
|
||||
|
||||
// 5) 汇总
|
||||
string centerClass = ReadString(centerDetection.Output, "Classification", "N/A");
|
||||
string leadClass = ReadString(leadDetection.Output, "Classification", "N/A");
|
||||
string overall = (centerClass == "PASS" && leadClass == "PASS") ? "PASS" : "FAIL";
|
||||
|
||||
OutputData["QfnAutoDetectionResult"] = true;
|
||||
OutputData["TemplateMatched"] = true;
|
||||
OutputData["MeasuredPose"] = templateResult.MeasuredPose;
|
||||
OutputData["CenterVoidRate"] = ReadDouble(centerDetection.Output, "VoidRate");
|
||||
OutputData["CenterClassification"] = centerClass;
|
||||
OutputData["LeadVoidRate"] = ReadDouble(leadDetection.Output, "VoidRate");
|
||||
OutputData["LeadClassification"] = leadClass;
|
||||
OutputData["LeadCount"] = ReadInt(leadDetection.Output, "LeadCount");
|
||||
OutputData["Classification"] = overall;
|
||||
OutputData["ResultText"] =
|
||||
$"QFN Auto: {overall} | Center={ReadDouble(centerDetection.Output, "VoidRate"):F1}%({centerClass}) | Lead={ReadDouble(leadDetection.Output, "VoidRate"):F1}%({leadClass})";
|
||||
|
||||
_logger.Information("QfnAutoDetection: {Class}, Center={CenterClass}, Lead={LeadClass}",
|
||||
overall, centerClass, leadClass);
|
||||
return output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "QfnAutoDetection failed");
|
||||
FillFailResult("FAIL_EXCEPTION", ex.Message);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
private (bool Success, Pose2D MeasuredPose, string? ErrorMessage) RunTemplateMatching(
|
||||
Image<Gray, byte> inputImage,
|
||||
out Dictionary<string, object> templateOutput)
|
||||
{
|
||||
var proc = new RotatedTemplateMatchingProcessor();
|
||||
proc.SetParameter("TemplatePath", GetParameter<string>("TemplatePath"));
|
||||
proc.SetParameter("ModelPath", GetParameter<string>("ModelPath"));
|
||||
proc.SetParameter("MatchThreshold", GetParameter<double>("MatchThreshold"));
|
||||
proc.SetParameter("MaxMatchCount", 100);
|
||||
proc.SetParameter("ToleranceAngle", GetParameter<double>("ToleranceAngle"));
|
||||
proc.SetParameter("MaxOverlap", GetParameter<double>("MaxOverlap"));
|
||||
proc.SetParameter("MinReduceArea", GetParameter<int>("MinReduceArea"));
|
||||
proc.SetParameter("UseSIMD", GetParameter<bool>("UseSIMD"));
|
||||
proc.SetParameter("UseSubPixel", GetParameter<bool>("UseSubPixel"));
|
||||
proc.SetParameter("DrawResults", false);
|
||||
proc.SetParameter("DrawThickness", 1);
|
||||
|
||||
var processed = proc.Process(inputImage);
|
||||
processed.Dispose();
|
||||
|
||||
templateOutput = new Dictionary<string, object>(proc.OutputData);
|
||||
|
||||
int matchIndex = GetParameter<int>("MatchIndex");
|
||||
if (!TemplateMatchOutputReader.TryReadMeasuredPose(templateOutput, out var measuredPose, out var error, matchIndex))
|
||||
return (false, default, error);
|
||||
|
||||
return (true, measuredPose, null);
|
||||
}
|
||||
|
||||
private (bool Success, Dictionary<string, object> Output, string? ErrorMessage) RunCenterVoid(
|
||||
Image<Gray, byte> inputImage,
|
||||
RoiAlignmentResult centerAlign)
|
||||
{
|
||||
var proc = new VoidMeasurementProcessor();
|
||||
proc.SetParameter("MinThreshold", GetParameter<int>("CenterMinThreshold"));
|
||||
proc.SetParameter("MaxThreshold", GetParameter<int>("CenterMaxThreshold"));
|
||||
proc.SetParameter("MinVoidArea", GetParameter<int>("CenterMinVoidArea"));
|
||||
proc.SetParameter("MergeRadius", GetParameter<int>("CenterMergeRadius"));
|
||||
proc.SetParameter("BlurSize", GetParameter<int>("CenterBlurSize"));
|
||||
proc.SetParameter("VoidLimit", GetParameter<double>("CenterVoidLimit"));
|
||||
|
||||
proc.SetParameter("PolyCount", centerAlign.TransformedPointsInt.Count);
|
||||
for (int i = 0; i < RoiPolygonParameterNames.MaxPoints; i++)
|
||||
{
|
||||
int x = i < centerAlign.TransformedPointsInt.Count ? centerAlign.TransformedPointsInt[i].X : 0;
|
||||
int y = i < centerAlign.TransformedPointsInt.Count ? centerAlign.TransformedPointsInt[i].Y : 0;
|
||||
proc.SetParameter($"PolyX{i}", x);
|
||||
proc.SetParameter($"PolyY{i}", y);
|
||||
}
|
||||
|
||||
var processed = proc.Process(inputImage);
|
||||
processed.Dispose();
|
||||
|
||||
var output = new Dictionary<string, object>(proc.OutputData);
|
||||
bool ok = output.ContainsKey("VoidMeasurementResult") && output["VoidMeasurementResult"] is bool b && b;
|
||||
return (ok, output, ok ? null : "Center void processor did not return success.");
|
||||
}
|
||||
|
||||
private (bool Success, Dictionary<string, object> Output, string? ErrorMessage) RunLeadVoid(
|
||||
Image<Gray, byte> inputImage,
|
||||
Pose2D measuredPose)
|
||||
{
|
||||
var proc = new QfnLeadPadVoidProcessor();
|
||||
|
||||
proc.SetParameter("PadBlurSize", GetParameter<int>("LeadPadBlurSize"));
|
||||
proc.SetParameter("PadThresholdLow", GetParameter<int>("LeadPadThresholdLow"));
|
||||
proc.SetParameter("PadThresholdHigh", GetParameter<int>("LeadPadThresholdHigh"));
|
||||
proc.SetParameter("PadMorphKernel", GetParameter<int>("LeadPadMorphKernel"));
|
||||
proc.SetParameter("MinPadArea", GetParameter<int>("LeadMinPadArea"));
|
||||
proc.SetParameter("MaxPadArea", GetParameter<int>("LeadMaxPadArea"));
|
||||
proc.SetParameter("PadAspectRatioMin", GetParameter<double>("LeadPadAspectRatioMin"));
|
||||
proc.SetParameter("VoidThresholdLow", GetParameter<int>("LeadVoidThresholdLow"));
|
||||
proc.SetParameter("VoidThresholdHigh", GetParameter<int>("LeadVoidThresholdHigh"));
|
||||
proc.SetParameter("MinVoidArea", GetParameter<int>("LeadMinVoidArea"));
|
||||
proc.SetParameter("VoidMergeRadius", GetParameter<int>("LeadVoidMergeRadius"));
|
||||
proc.SetParameter("VoidRateLimit", GetParameter<double>("LeadVoidRateLimit"));
|
||||
proc.SetParameter("MinQualifiedPadArea", GetParameter<int>("LeadMinQualifiedPadArea"));
|
||||
proc.SetParameter("Thickness", GetParameter<int>("LeadThickness"));
|
||||
|
||||
string leadRoiMode = GetParameter<string>("LeadRoiMode");
|
||||
bool alignLeadRoi = GetParameter<bool>("AlignLeadRoiWithTemplate");
|
||||
if (leadRoiMode == "Polygon")
|
||||
{
|
||||
IReadOnlyList<(int X, int Y)> points;
|
||||
if (alignLeadRoi)
|
||||
{
|
||||
var referencePose = new Pose2D(
|
||||
GetParameter<double>("RefCenterX"),
|
||||
GetParameter<double>("RefCenterY"),
|
||||
GetParameter<double>("RefAngle"));
|
||||
var leadTeach = ReadPrefixedPolygon("Lead");
|
||||
var leadAlign = RoiAlignmentApplier.Apply(referencePose, leadTeach, measuredPose);
|
||||
if (!leadAlign.Success)
|
||||
return (false, new Dictionary<string, object>(), leadAlign.ErrorMessage);
|
||||
points = leadAlign.TransformedPointsInt;
|
||||
}
|
||||
else
|
||||
{
|
||||
var teach = ReadPrefixedPolygon("Lead");
|
||||
var list = new List<(int X, int Y)>(teach.Count);
|
||||
foreach (var p in teach)
|
||||
list.Add(((int)Math.Round(p.X), (int)Math.Round(p.Y)));
|
||||
points = list;
|
||||
}
|
||||
|
||||
proc.SetParameter("RoiMode", "Polygon");
|
||||
proc.SetParameter("PolyCount", points.Count);
|
||||
for (int i = 0; i < RoiPolygonParameterNames.MaxPoints; i++)
|
||||
{
|
||||
int x = i < points.Count ? points[i].X : 0;
|
||||
int y = i < points.Count ? points[i].Y : 0;
|
||||
proc.SetParameter($"PolyX{i}", x);
|
||||
proc.SetParameter($"PolyY{i}", y);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
proc.SetParameter("RoiMode", "None");
|
||||
proc.SetParameter("PolyCount", 0);
|
||||
for (int i = 0; i < RoiPolygonParameterNames.MaxPoints; i++)
|
||||
{
|
||||
proc.SetParameter($"PolyX{i}", 0);
|
||||
proc.SetParameter($"PolyY{i}", 0);
|
||||
}
|
||||
}
|
||||
|
||||
var processed = proc.Process(inputImage);
|
||||
processed.Dispose();
|
||||
|
||||
var output = new Dictionary<string, object>(proc.OutputData);
|
||||
bool ok = output.ContainsKey("QfnLeadResult") && output["QfnLeadResult"] is bool b && b;
|
||||
return (ok, output, ok ? null : "Qfn lead processor did not return success.");
|
||||
}
|
||||
|
||||
private void FillFailResult(string failCode, string message)
|
||||
{
|
||||
OutputData["QfnAutoDetectionResult"] = false;
|
||||
OutputData["Classification"] = "FAIL";
|
||||
OutputData["FailCode"] = failCode;
|
||||
OutputData["ResultText"] = $"QFN Auto: {failCode} | {message}";
|
||||
}
|
||||
|
||||
private void AddHiddenPolygonParams(string prefix)
|
||||
{
|
||||
Parameters.Add($"{prefix}PolyCount", new ProcessorParameter($"{prefix}PolyCount", $"{prefix}PolyCount", typeof(int), 0, null, null, "")
|
||||
{
|
||||
IsVisible = false
|
||||
});
|
||||
|
||||
for (int i = 0; i < RoiPolygonParameterNames.MaxPoints; i++)
|
||||
{
|
||||
Parameters.Add($"{prefix}PolyX{i}", new ProcessorParameter($"{prefix}PolyX{i}", $"{prefix}PolyX{i}", typeof(int), 0, null, null, "")
|
||||
{
|
||||
IsVisible = false
|
||||
});
|
||||
Parameters.Add($"{prefix}PolyY{i}", new ProcessorParameter($"{prefix}PolyY{i}", $"{prefix}PolyY{i}", typeof(int), 0, null, null, "")
|
||||
{
|
||||
IsVisible = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<Point2D> ReadPrefixedPolygon(string prefix)
|
||||
{
|
||||
int count = GetParameter<int>($"{prefix}PolyCount");
|
||||
if (count < 3) return Array.Empty<Point2D>();
|
||||
count = Math.Min(count, RoiPolygonParameterNames.MaxPoints);
|
||||
|
||||
var points = new List<Point2D>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
points.Add(new Point2D(
|
||||
GetParameter<int>($"{prefix}PolyX{i}"),
|
||||
GetParameter<int>($"{prefix}PolyY{i}")));
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static string ReadString(IReadOnlyDictionary<string, object> data, string key, string defaultValue)
|
||||
=> data.TryGetValue(key, out var v) && v is string s ? s : defaultValue;
|
||||
|
||||
private static int ReadInt(IReadOnlyDictionary<string, object> data, string key)
|
||||
=> data.TryGetValue(key, out var v) ? Convert.ToInt32(v) : 0;
|
||||
|
||||
private static double ReadDouble(IReadOnlyDictionary<string, object> data, string key)
|
||||
=> data.TryGetValue(key, out var v) ? Convert.ToDouble(v) : 0.0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user