From 5b4ff89ef09fe1c6d0cf5c110214ff92d8290376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Mon, 1 Jun 2026 17:06:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20QFN=20=E4=B8=80=E4=BD=93?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E7=AE=97=E5=AD=90=EF=BC=8C=E4=B8=B2=E8=81=94?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E5=8C=B9=E9=85=8D=E4=B8=8E=E5=8F=8C=E8=B7=AF?= =?UTF-8?q?=E7=A9=BA=E6=B4=9E=E6=A3=80=E6=B5=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将模板定位、中心 ROI 对齐、中心焊盘空洞和引脚空洞检测整合到单算子中,并输出统一判定结果,便于快速验证完整流程。 Co-authored-by: Cursor --- .../检测分析/QfnAutoDetectionProcessor.cs | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 XP.ImageProcessing.Processors/检测分析/QfnAutoDetectionProcessor.cs diff --git a/XP.ImageProcessing.Processors/检测分析/QfnAutoDetectionProcessor.cs b/XP.ImageProcessing.Processors/检测分析/QfnAutoDetectionProcessor.cs new file mode 100644 index 0000000..84172b8 --- /dev/null +++ b/XP.ImageProcessing.Processors/检测分析/QfnAutoDetectionProcessor.cs @@ -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; + +/// +/// 一体化 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; +} +