Files
XplorePlane/XP.ImageProcessing.Processors/检测分析/QfnAutoDetectionProcessor.cs
T
李伟 5b4ff89ef0 新增 QFN 一体检测算子,串联模板匹配与双路空洞检测。
将模板定位、中心 ROI 对齐、中心焊盘空洞和引脚空洞检测整合到单算子中,并输出统一判定结果,便于快速验证完整流程。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 17:06:48 +08:00

346 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// 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;
}