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;
+}
+