// ============================================================================ // 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; /// /// 一体化 QFN 检测:先模板匹配获取位姿,再将中心 ROI 对齐后做中心空洞检测,最后做引脚空洞检测并汇总判定。 /// public sealed class QfnAutoDetectionProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); 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 Process(Image 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("RefCenterX"), GetParameter("RefCenterY"), GetParameter("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 inputImage, out Dictionary templateOutput) { var proc = new RotatedTemplateMatchingProcessor(); proc.SetParameter("TemplatePath", GetParameter("TemplatePath")); proc.SetParameter("ModelPath", GetParameter("ModelPath")); proc.SetParameter("MatchThreshold", GetParameter("MatchThreshold")); proc.SetParameter("MaxMatchCount", 100); proc.SetParameter("ToleranceAngle", GetParameter("ToleranceAngle")); proc.SetParameter("MaxOverlap", GetParameter("MaxOverlap")); proc.SetParameter("MinReduceArea", GetParameter("MinReduceArea")); proc.SetParameter("UseSIMD", GetParameter("UseSIMD")); proc.SetParameter("UseSubPixel", GetParameter("UseSubPixel")); proc.SetParameter("DrawResults", false); proc.SetParameter("DrawThickness", 1); var processed = proc.Process(inputImage); processed.Dispose(); templateOutput = new Dictionary(proc.OutputData); int matchIndex = GetParameter("MatchIndex"); if (!TemplateMatchOutputReader.TryReadMeasuredPose(templateOutput, out var measuredPose, out var error, matchIndex)) return (false, default, error); return (true, measuredPose, null); } private (bool Success, Dictionary Output, string? ErrorMessage) RunCenterVoid( Image inputImage, RoiAlignmentResult centerAlign) { var proc = new VoidMeasurementProcessor(); proc.SetParameter("MinThreshold", GetParameter("CenterMinThreshold")); proc.SetParameter("MaxThreshold", GetParameter("CenterMaxThreshold")); proc.SetParameter("MinVoidArea", GetParameter("CenterMinVoidArea")); proc.SetParameter("MergeRadius", GetParameter("CenterMergeRadius")); proc.SetParameter("BlurSize", GetParameter("CenterBlurSize")); proc.SetParameter("VoidLimit", GetParameter("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(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 Output, string? ErrorMessage) RunLeadVoid( Image inputImage, Pose2D measuredPose) { var proc = new QfnLeadPadVoidProcessor(); proc.SetParameter("PadBlurSize", GetParameter("LeadPadBlurSize")); proc.SetParameter("PadThresholdLow", GetParameter("LeadPadThresholdLow")); proc.SetParameter("PadThresholdHigh", GetParameter("LeadPadThresholdHigh")); proc.SetParameter("PadMorphKernel", GetParameter("LeadPadMorphKernel")); proc.SetParameter("MinPadArea", GetParameter("LeadMinPadArea")); proc.SetParameter("MaxPadArea", GetParameter("LeadMaxPadArea")); proc.SetParameter("PadAspectRatioMin", GetParameter("LeadPadAspectRatioMin")); proc.SetParameter("VoidThresholdLow", GetParameter("LeadVoidThresholdLow")); proc.SetParameter("VoidThresholdHigh", GetParameter("LeadVoidThresholdHigh")); proc.SetParameter("MinVoidArea", GetParameter("LeadMinVoidArea")); proc.SetParameter("VoidMergeRadius", GetParameter("LeadVoidMergeRadius")); proc.SetParameter("VoidRateLimit", GetParameter("LeadVoidRateLimit")); proc.SetParameter("MinQualifiedPadArea", GetParameter("LeadMinQualifiedPadArea")); proc.SetParameter("Thickness", GetParameter("LeadThickness")); string leadRoiMode = GetParameter("LeadRoiMode"); bool alignLeadRoi = GetParameter("AlignLeadRoiWithTemplate"); if (leadRoiMode == "Polygon") { IReadOnlyList<(int X, int Y)> points; if (alignLeadRoi) { var referencePose = new Pose2D( GetParameter("RefCenterX"), GetParameter("RefCenterY"), GetParameter("RefAngle")); var leadTeach = ReadPrefixedPolygon("Lead"); var leadAlign = RoiAlignmentApplier.Apply(referencePose, leadTeach, measuredPose); if (!leadAlign.Success) return (false, new Dictionary(), 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(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 ReadPrefixedPolygon(string prefix) { int count = GetParameter($"{prefix}PolyCount"); if (count < 3) return Array.Empty(); count = Math.Min(count, RoiPolygonParameterNames.MaxPoints); var points = new List(count); for (int i = 0; i < count; i++) { points.Add(new Point2D( GetParameter($"{prefix}PolyX{i}"), GetParameter($"{prefix}PolyY{i}"))); } return points; } private static string ReadString(IReadOnlyDictionary data, string key, string defaultValue) => data.TryGetValue(key, out var v) && v is string s ? s : defaultValue; private static int ReadInt(IReadOnlyDictionary data, string key) => data.TryGetValue(key, out var v) ? Convert.ToInt32(v) : 0; private static double ReadDouble(IReadOnlyDictionary data, string key) => data.TryGetValue(key, out var v) ? Convert.ToDouble(v) : 0.0; }