From 1874c4a5bb20aef817e57a4bb24450ae31d5b6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Mon, 1 Jun 2026 17:04:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20ROI=20=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=83=BD=E5=8A=9B=E5=B9=B6=E6=89=93=E9=80=9A?= =?UTF-8?q?=E5=88=B0=E7=AE=97=E5=AD=90=E4=B8=8E=20UI=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一补齐对齐核心工具类、RoiAlignment 算子、模板匹配对齐扩展和多语言资源,便于在检测前稳定完成示教 ROI 到运行图的变换。 Co-authored-by: Cursor --- .gitignore | 1 + XP.Common/Resources/Resources.en-US.resx | 50 ++++++ XP.Common/Resources/Resources.resx | 50 ++++++ XP.Common/Resources/Resources.zh-CN.resx | 50 ++++++ .../Alignment/AlignmentRecipe.cs | 13 ++ .../Alignment/RoiAlignmentApplier.cs | 146 ++++++++++++++++++ .../Alignment/RoiAlignmentIntegrationGuide.md | 81 ++++++++++ .../Alignment/RoiAlignmentOutputKeys.cs | 21 +++ .../Alignment/RoiAlignmentPipelineBridge.cs | 114 ++++++++++++++ .../Alignment/RoiAlignmentResult.cs | 19 +++ .../Alignment/RoiPolygonParameterNames.cs | 15 ++ .../Alignment/TemplateMatchOutputReader.cs | 82 ++++++++++ .../定位识别/RoiAlignmentProcessor.cs | 130 ++++++++++++++++ .../TemplateMatchAlignmentExtensions.cs | 15 ++ .../ImageProcessing/ProcessorUiMetadata.cs | 4 +- 15 files changed, 790 insertions(+), 1 deletion(-) create mode 100644 XP.ImageProcessing.Core/Alignment/RoiAlignmentApplier.cs create mode 100644 XP.ImageProcessing.Core/Alignment/RoiAlignmentIntegrationGuide.md create mode 100644 XP.ImageProcessing.Core/Alignment/RoiAlignmentOutputKeys.cs create mode 100644 XP.ImageProcessing.Core/Alignment/RoiAlignmentPipelineBridge.cs create mode 100644 XP.ImageProcessing.Core/Alignment/RoiAlignmentResult.cs create mode 100644 XP.ImageProcessing.Core/Alignment/RoiPolygonParameterNames.cs create mode 100644 XP.ImageProcessing.Core/Alignment/TemplateMatchOutputReader.cs create mode 100644 XP.ImageProcessing.Processors/定位识别/RoiAlignmentProcessor.cs diff --git a/.gitignore b/.gitignore index 4e446be..c0743c1 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ bld/ [Ll]ogs/ lib/ XP.ImageProcessing/ +XP.ImageProcessing.SmokeTest/ ImageProcessing.sln # 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构) diff --git a/XP.Common/Resources/Resources.en-US.resx b/XP.Common/Resources/Resources.en-US.resx index dd6efdd..c67e38a 100644 --- a/XP.Common/Resources/Resources.en-US.resx +++ b/XP.Common/Resources/Resources.en-US.resx @@ -1551,6 +1551,50 @@ Path to a pre-trained model file (.tmmodel). If it exists the model is loaded directly; otherwise the template is learned and the model is saved automatically. + + + ROI Alignment + + + Transform teach polygon ROI to the run image using reference and template-match poses (outputs Poly params for downstream inspectors). + + + Reference center X + + + Template/part center X on the teach image (pixels). + + + Reference center Y + + + Template/part center Y on the teach image (pixels). + + + Reference angle (°) + + + Reference angle on the teach image; usually 0. + + + Measured center X + + + Match center X on the current image; inject from previous step OutputData. + + + Measured center Y + + + Match center Y on the current image. + + + Measured angle (°) + + + Match angle on the current image. + + Angle Measurement @@ -2106,4 +2150,10 @@ Reprojection error: {1:F4} pixels Contour drawing line thickness + + QFN Integrated Detection + + + After template-match alignment, automatically run center pad void and lead pad void inspections and aggregate the final classification. + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.resx b/XP.Common/Resources/Resources.resx index c5dd687..926d126 100644 --- a/XP.Common/Resources/Resources.resx +++ b/XP.Common/Resources/Resources.resx @@ -1573,6 +1573,50 @@ 已训练模型文件路径(.tmmodel)。若存在则直接加载跳过学习;若不存在则从模板学习后自动保存。 + + + ROI对齐 + + + 按示教位姿与模板匹配位姿,将示教多边形 ROI 变换到运行图(输出 Poly 参数供下游检测算子使用)。 + + + 基准中心X + + + 示教图上的模板/器件中心 X(像素)。 + + + 基准中心Y + + + 示教图上的模板/器件中心 Y(像素)。 + + + 基准角度(°) + + + 示教图上的基准角度,通常为 0。 + + + 测量中心X + + + 当前图上模板匹配中心 X,由流水线从上一步 OutputData 注入。 + + + 测量中心Y + + + 当前图上模板匹配中心 Y。 + + + 测量角度(°) + + + 当前图上模板匹配角度。 + + 角度测量 @@ -2139,4 +2183,10 @@ 轮廓绘制线条粗细 + + QFN一体检测 + + + 模板匹配对齐后,自动完成中心焊盘空洞与引脚空洞检测并汇总判定 + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.zh-CN.resx b/XP.Common/Resources/Resources.zh-CN.resx index bfbb2cb..59ba646 100644 --- a/XP.Common/Resources/Resources.zh-CN.resx +++ b/XP.Common/Resources/Resources.zh-CN.resx @@ -1545,6 +1545,50 @@ 未找到 TemplateMatchLib.dll,请先编译 C++ DLL 工程。 + + + ROI对齐 + + + 按示教位姿与模板匹配位姿,将示教多边形 ROI 变换到运行图(输出 Poly 参数供下游检测算子使用)。 + + + 基准中心X + + + 示教图上的模板/器件中心 X(像素)。 + + + 基准中心Y + + + 示教图上的模板/器件中心 Y(像素)。 + + + 基准角度(°) + + + 示教图上的基准角度,通常为 0。 + + + 测量中心X + + + 当前图上模板匹配中心 X,由流水线从上一步 OutputData 注入。 + + + 测量中心Y + + + 当前图上模板匹配中心 Y。 + + + 测量角度(°) + + + 当前图上模板匹配角度。 + + 角度测量 @@ -2100,4 +2144,10 @@ 轮廓绘制线条粗细 + + QFN一体检测 + + + 模板匹配对齐后,自动完成中心焊盘空洞与引脚空洞检测并汇总判定 + \ No newline at end of file diff --git a/XP.ImageProcessing.Core/Alignment/AlignmentRecipe.cs b/XP.ImageProcessing.Core/Alignment/AlignmentRecipe.cs index ff33afb..c1fece2 100644 --- a/XP.ImageProcessing.Core/Alignment/AlignmentRecipe.cs +++ b/XP.ImageProcessing.Core/Alignment/AlignmentRecipe.cs @@ -18,4 +18,17 @@ public sealed class AlignmentRecipe /// 变换为整型顶点,供检测算子注入。 public (int X, int Y)[] TransformRoiToInt(Pose2D measuredPose) => RoiAlignment.TransformPolygonToInt(RoiPoints, ReferencePose, measuredPose); + + /// 从算子参数字典读取示教多边形并填充 + public static AlignmentRecipe FromTeachParameters( + Pose2D referencePose, + IReadOnlyDictionary parameters) + { + var points = RoiAlignmentApplier.ReadTeachPolygon(parameters); + return new AlignmentRecipe + { + ReferencePose = referencePose, + RoiPoints = points.ToList() + }; + } } diff --git a/XP.ImageProcessing.Core/Alignment/RoiAlignmentApplier.cs b/XP.ImageProcessing.Core/Alignment/RoiAlignmentApplier.cs new file mode 100644 index 0000000..ba061b9 --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/RoiAlignmentApplier.cs @@ -0,0 +1,146 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// 示教 ROI + 基准/测量位姿 → 运行图多边形;供 RoiAlignment 算子与流水线胶水调用。 +/// +public static class RoiAlignmentApplier +{ + public static RoiAlignmentResult Apply(AlignmentRecipe recipe, Pose2D measuredPose) + { + if (recipe == null) + return Fail(default, measuredPose, "Alignment recipe is null."); + + return Apply(recipe.ReferencePose, recipe.RoiPoints, measuredPose); + } + + public static RoiAlignmentResult Apply( + Pose2D referencePose, + IReadOnlyList teachPoints, + Pose2D measuredPose) + { + if (teachPoints == null || teachPoints.Count < 3) + return Fail(referencePose, measuredPose, "Teach polygon must have at least 3 points."); + + var transformed = RoiAlignment.TransformPolygon(teachPoints, referencePose, measuredPose); + var transformedInt = RoiAlignment.TransformPolygonToInt(teachPoints, referencePose, measuredPose); + + return new RoiAlignmentResult + { + Success = true, + ReferencePose = referencePose, + MeasuredPose = measuredPose, + TransformedPoints = transformed, + TransformedPointsInt = transformedInt + }; + } + + public static RoiAlignmentResult ApplyFromTemplateMatchOutput( + AlignmentRecipe recipe, + IReadOnlyDictionary? templateMatchOutput, + int matchIndex = 0) + { + if (recipe == null) + return Fail(default, default, "Alignment recipe is null."); + + if (!TemplateMatchOutputReader.TryReadMeasuredPose(templateMatchOutput, out var measured, out var error, matchIndex)) + return Fail(recipe.ReferencePose, default, error); + + return Apply(recipe, measured); + } + + public static IReadOnlyList ReadTeachPolygon(IReadOnlyDictionary parameters) + { + if (parameters == null) + return Array.Empty(); + + if (!parameters.TryGetValue(RoiPolygonParameterNames.PolyCount, out var countObj)) + return Array.Empty(); + + int count = Convert.ToInt32(countObj); + if (count < 3) + return Array.Empty(); + + count = Math.Min(count, RoiPolygonParameterNames.MaxPoints); + var points = new List(count); + for (int i = 0; i < count; i++) + { + if (!parameters.TryGetValue(RoiPolygonParameterNames.PolyX(i), out var xObj) + || !parameters.TryGetValue(RoiPolygonParameterNames.PolyY(i), out var yObj)) + continue; + + points.Add(new Point2D(Convert.ToDouble(xObj), Convert.ToDouble(yObj))); + } + + return points; + } + + /// + /// 将变换结果写入参数字典(PolyCount、PolyX/PolyY、RoiMode),供下游 VoidMeasurement 等算子使用。 + /// + public static void WriteToParameters( + IDictionary parameters, + RoiAlignmentResult result, + bool setRoiModePolygon = true) + { + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); + + parameters[RoiAlignmentOutputKeys.Success] = result.Success; + if (!string.IsNullOrEmpty(result.ErrorMessage)) + parameters[RoiAlignmentOutputKeys.Message] = result.ErrorMessage!; + + if (!result.Success) + return; + + int count = result.TransformedPointsInt.Count; + parameters[RoiPolygonParameterNames.PolyCount] = count; + + for (int i = 0; i < RoiPolygonParameterNames.MaxPoints; i++) + { + if (i < count) + { + parameters[RoiPolygonParameterNames.PolyX(i)] = result.TransformedPointsInt[i].X; + parameters[RoiPolygonParameterNames.PolyY(i)] = result.TransformedPointsInt[i].Y; + } + else + { + parameters[RoiPolygonParameterNames.PolyX(i)] = 0; + parameters[RoiPolygonParameterNames.PolyY(i)] = 0; + } + } + + if (setRoiModePolygon) + parameters[RoiPolygonParameterNames.RoiMode] = "Polygon"; + } + + /// + /// 将变换结果写入算子 OutputData(与 键名一致,便于流水线拷贝)。 + /// + public static void WriteToOutputData(IDictionary outputData, RoiAlignmentResult result) + { + if (outputData == null) + throw new ArgumentNullException(nameof(outputData)); + + WriteToParameters(outputData, result); + + if (result.Success) + { + outputData["ReferenceCenterX"] = result.ReferencePose.X; + outputData["ReferenceCenterY"] = result.ReferencePose.Y; + outputData["ReferenceAngle"] = result.ReferencePose.AngleDegrees; + outputData["MeasuredCenterX"] = result.MeasuredPose.X; + outputData["MeasuredCenterY"] = result.MeasuredPose.Y; + outputData["MeasuredAngle"] = result.MeasuredPose.AngleDegrees; + outputData["TransformedPointCount"] = result.TransformedPointsInt.Count; + } + } + + private static RoiAlignmentResult Fail(Pose2D reference, Pose2D measured, string? message) + => new() + { + Success = false, + ErrorMessage = message, + ReferencePose = reference, + MeasuredPose = measured + }; +} diff --git a/XP.ImageProcessing.Core/Alignment/RoiAlignmentIntegrationGuide.md b/XP.ImageProcessing.Core/Alignment/RoiAlignmentIntegrationGuide.md new file mode 100644 index 0000000..244e500 --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/RoiAlignmentIntegrationGuide.md @@ -0,0 +1,81 @@ +# ROI 对齐接入说明(给流水线) + +本文用于把 `RotatedTemplateMatching` 的结果接到 ROI 对齐,再把对齐后的 `Poly*` 注入到下游检测算子(如 `VoidMeasurement`、`QfnLeadPadVoid`)。 + +## 1. 最小流程 + +1. 先执行模板匹配(`RotatedTemplateMatching`)。 +2. 读取匹配位姿(`CenterX/CenterY/Angle`)。 +3. 用 `AlignmentRecipe` 做 ROI 变换。 +4. 把变换后的 `PolyCount/PolyX*/PolyY*` 写入下游检测参数。 +5. 再执行下游检测算子。 + +## 2. 推荐调用方式(最少代码) + +```csharp +using XP.ImageProcessing.Core.Alignment; + +// 1) recipe 来自示教(ReferencePose + RoiPoints) +AlignmentRecipe recipe = LoadRecipe(); + +// 2) templateOutput 为 RotatedTemplateMatching 的 OutputData +if (!RoiAlignmentPipelineBridge.TryAlignFromTemplateMatch( + recipe, + templateOutput, + out var alignResult)) +{ + // 匹配失败或对齐失败:按业务判定 NG/中断 + throw new InvalidOperationException(alignResult.ErrorMessage ?? "ROI alignment failed."); +} + +// 3) detectionParams 为下一步检测算子的参数字典(VoidMeasurement / QfnLeadPadVoid 等) +RoiAlignmentApplier.WriteToParameters(detectionParams, alignResult, setRoiModePolygon: true); + +// 4) 执行下游检测算子 +// ProcessImageWithOutputAsync(..., detectionParams, ...) +``` + +## 3. 若你走 `RoiAlignment` 算子节点 + +如果流程里显式放了 `RoiAlignment` 节点: + +- 输入参数:`RefCenterX/RefCenterY/RefAngle`、`MeasuredCenterX/MeasuredCenterY/MeasuredAngle`、示教 `Poly*`。 +- 输出参数:`PolyCount/PolyX*/PolyY*`、`RoiAlignmentSuccess`、`RoiAlignmentMessage`。 + +将 `RoiAlignment` 的输出字典拷贝到下游检测参数,可用: + +```csharp +if (!RoiAlignmentPipelineBridge.TryCopyAlignedRoiToDetectionParameters( + roiAlignmentOutput, + detectionParams, + out var error)) +{ + throw new InvalidOperationException(error ?? "Copy aligned ROI failed."); +} +``` + +## 4. 关键键名(常量) + +- ROI 对齐输出键:`RoiAlignmentOutputKeys` + - `Success` = `RoiAlignmentSuccess` + - `Message` = `RoiAlignmentMessage` + - `ResultText` = `ResultText` + - `ReferenceCenterX/ReferenceCenterY/ReferenceAngle` + - `MeasuredCenterX/MeasuredCenterY/MeasuredAngle` + - `TransformedPointCount` +- 多边形键:`RoiPolygonParameterNames` + - `PolyCount` + - `PolyX(i)` / `PolyY(i)` + - `RoiMode`(值建议写 `Polygon`) + +## 5. 失败处理建议 + +- `TryAlignFromTemplateMatch == false`:模板匹配失败或无效。 +- `alignResult.Success == false`:示教点不足(<3)或配方异常。 +- `PolyCount < 3`:不要继续下游检测,直接标记本次检测失败。 + +## 6. 示教数据要求 + +- `ReferencePose` 必须与示教 ROI 的坐标系一致(同一张示教图)。 +- `RoiPoints` 至少 3 点,建议按轮廓顺序保存。 +- 建议示教图先自匹配一次后再落 `ReferencePose`,减少中心约定偏差。 diff --git a/XP.ImageProcessing.Core/Alignment/RoiAlignmentOutputKeys.cs b/XP.ImageProcessing.Core/Alignment/RoiAlignmentOutputKeys.cs new file mode 100644 index 0000000..d68fa5d --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/RoiAlignmentOutputKeys.cs @@ -0,0 +1,21 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// RoiAlignment 算子(键 )与 的输出/参数字典键名,供流水线拷贝到下游检测算子。 +/// +public static class RoiAlignmentOutputKeys +{ + public const string ProcessorKey = "RoiAlignment"; + + public const string Success = "RoiAlignmentSuccess"; + public const string Message = "RoiAlignmentMessage"; + public const string ResultText = "ResultText"; + + public const string ReferenceCenterX = "ReferenceCenterX"; + public const string ReferenceCenterY = "ReferenceCenterY"; + public const string ReferenceAngle = "ReferenceAngle"; + public const string MeasuredCenterX = "MeasuredCenterX"; + public const string MeasuredCenterY = "MeasuredCenterY"; + public const string MeasuredAngle = "MeasuredAngle"; + public const string TransformedPointCount = "TransformedPointCount"; +} diff --git a/XP.ImageProcessing.Core/Alignment/RoiAlignmentPipelineBridge.cs b/XP.ImageProcessing.Core/Alignment/RoiAlignmentPipelineBridge.cs new file mode 100644 index 0000000..2705d41 --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/RoiAlignmentPipelineBridge.cs @@ -0,0 +1,114 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// 流水线胶水入口:模板匹配 OutputData → ROI 变换 → 写入下游 VoidMeasurement 等算子参数字典。 +/// 不依赖 XplorePlane;由 PipelineExecutionService 在步骤间调用。 +/// +public static class RoiAlignmentPipelineBridge +{ + /// + /// 将测量位姿写入 RoiAlignment 算子参数(MeasuredCenterX/Y/Angle)。 + /// + public static void InjectMeasuredPose(IDictionary roiAlignmentParameters, Pose2D measuredPose) + { + if (roiAlignmentParameters == null) + throw new ArgumentNullException(nameof(roiAlignmentParameters)); + + roiAlignmentParameters["MeasuredCenterX"] = measuredPose.X; + roiAlignmentParameters["MeasuredCenterY"] = measuredPose.Y; + roiAlignmentParameters["MeasuredAngle"] = measuredPose.AngleDegrees; + } + + /// + /// 将示教基准位姿写入 RoiAlignment 算子参数(RefCenterX/Y/Angle)。 + /// + public static void InjectReferencePose(IDictionary roiAlignmentParameters, Pose2D referencePose) + { + if (roiAlignmentParameters == null) + throw new ArgumentNullException(nameof(roiAlignmentParameters)); + + roiAlignmentParameters["RefCenterX"] = referencePose.X; + roiAlignmentParameters["RefCenterY"] = referencePose.Y; + roiAlignmentParameters["RefAngle"] = referencePose.AngleDegrees; + } + + /// + /// 从模板匹配一步 OutputData 读取位姿并对齐示教 ROI。 + /// + public static bool TryAlignFromTemplateMatch( + AlignmentRecipe recipe, + IReadOnlyDictionary? templateMatchOutput, + out RoiAlignmentResult result, + int matchIndex = 0) + { + result = RoiAlignmentApplier.ApplyFromTemplateMatchOutput(recipe, templateMatchOutput, matchIndex); + return result.Success; + } + + /// + /// 将 RoiAlignment 算子 OutputData 中的 Poly* 拷贝到检测算子参数字典(如 VoidMeasurement)。 + /// + public static bool TryCopyAlignedRoiToDetectionParameters( + IReadOnlyDictionary? roiAlignmentOutput, + IDictionary detectionParameters, + out string? errorMessage) + { + errorMessage = null; + if (roiAlignmentOutput == null) + { + errorMessage = "ROI alignment output is null."; + return false; + } + + if (roiAlignmentOutput.TryGetValue(RoiAlignmentOutputKeys.Success, out var okObj) + && okObj is bool ok + && !ok) + { + if (roiAlignmentOutput.TryGetValue(RoiAlignmentOutputKeys.Message, out var msgObj) + && msgObj is string msg + && !string.IsNullOrWhiteSpace(msg)) + errorMessage = msg; + else + errorMessage = "ROI alignment failed."; + return false; + } + + if (!roiAlignmentOutput.TryGetValue(RoiPolygonParameterNames.PolyCount, out var countObj)) + { + errorMessage = "ROI alignment output has no PolyCount."; + return false; + } + + int count = Convert.ToInt32(countObj); + if (count < 3) + { + errorMessage = $"Invalid aligned PolyCount={count}."; + return false; + } + + RoiAlignmentApplier.WriteToParameters(detectionParameters, new RoiAlignmentResult + { + Success = true, + TransformedPointsInt = ReadIntPointsFromOutput(roiAlignmentOutput, count) + }); + + return true; + } + + private static IReadOnlyList<(int X, int Y)> ReadIntPointsFromOutput( + IReadOnlyDictionary output, + int count) + { + var list = new List<(int X, int Y)>(count); + for (int i = 0; i < count; i++) + { + int x = output.TryGetValue(RoiPolygonParameterNames.PolyX(i), out var xo) + ? Convert.ToInt32(xo) : 0; + int y = output.TryGetValue(RoiPolygonParameterNames.PolyY(i), out var yo) + ? Convert.ToInt32(yo) : 0; + list.Add((x, y)); + } + + return list; + } +} diff --git a/XP.ImageProcessing.Core/Alignment/RoiAlignmentResult.cs b/XP.ImageProcessing.Core/Alignment/RoiAlignmentResult.cs new file mode 100644 index 0000000..c71a62a --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/RoiAlignmentResult.cs @@ -0,0 +1,19 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// ROI 对齐变换结果,供流水线写入下游检测算子参数。 +/// +public sealed class RoiAlignmentResult +{ + public bool Success { get; init; } + public string? ErrorMessage { get; init; } + + public Pose2D ReferencePose { get; init; } + public Pose2D MeasuredPose { get; init; } + + /// 运行图坐标下的多边形顶点(至少 3 点)。 + public IReadOnlyList TransformedPoints { get; init; } = Array.Empty(); + + /// 四舍五入后的整型顶点,可直接写入 PolyX/PolyY。 + public IReadOnlyList<(int X, int Y)> TransformedPointsInt { get; init; } = Array.Empty<(int X, int Y)>(); +} diff --git a/XP.ImageProcessing.Core/Alignment/RoiPolygonParameterNames.cs b/XP.ImageProcessing.Core/Alignment/RoiPolygonParameterNames.cs new file mode 100644 index 0000000..8c1df00 --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/RoiPolygonParameterNames.cs @@ -0,0 +1,15 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// 与 VoidMeasurement / BgaVoidRate / QfnLeadPadVoid 等算子一致的多边形 ROI 参数名。 +/// +public static class RoiPolygonParameterNames +{ + public const int MaxPoints = 32; + + public const string RoiMode = "RoiMode"; + public const string PolyCount = "PolyCount"; + + public static string PolyX(int index) => $"PolyX{index}"; + public static string PolyY(int index) => $"PolyY{index}"; +} diff --git a/XP.ImageProcessing.Core/Alignment/TemplateMatchOutputReader.cs b/XP.ImageProcessing.Core/Alignment/TemplateMatchOutputReader.cs new file mode 100644 index 0000000..dc35fa7 --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/TemplateMatchOutputReader.cs @@ -0,0 +1,82 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// 从旋转模板匹配算子(键 )的 OutputData 读取测量位姿。 +/// +public static class TemplateMatchOutputReader +{ + public const string ProcessorKey = "RotatedTemplateMatching"; + + public static bool TryReadMeasuredPose( + IReadOnlyDictionary? outputData, + out Pose2D measuredPose, + out string? errorMessage, + int matchIndex = 0) + { + measuredPose = default; + errorMessage = null; + + if (outputData == null || outputData.Count == 0) + { + errorMessage = "Template match output is empty."; + return false; + } + + if (outputData.TryGetValue("Matched", out var matchedObj) + && matchedObj is bool matched + && !matched) + { + if (outputData.TryGetValue("Message", out var msgObj) && msgObj is string msg && !string.IsNullOrWhiteSpace(msg)) + errorMessage = msg; + else + errorMessage = "Template match failed (Matched=false)."; + return false; + } + + if (outputData.TryGetValue("MatchCount", out var countObj)) + { + int count = Convert.ToInt32(countObj); + if (count <= 0) + { + errorMessage = "Template match count is zero."; + return false; + } + + if (matchIndex < 0 || matchIndex >= count) + { + errorMessage = $"Match index {matchIndex} is out of range (MatchCount={count})."; + return false; + } + } + + string prefix = matchIndex == 0 ? string.Empty : $"[{matchIndex}]"; + + if (!TryReadDouble(outputData, "CenterX" + prefix, out double cx) + || !TryReadDouble(outputData, "CenterY" + prefix, out double cy) + || !TryReadDouble(outputData, "Angle" + prefix, out double angle)) + { + errorMessage = "Template match output is missing CenterX/CenterY/Angle."; + return false; + } + + measuredPose = new Pose2D(cx, cy, angle); + return true; + } + + private static bool TryReadDouble(IReadOnlyDictionary data, string key, out double value) + { + value = 0; + if (!data.TryGetValue(key, out var obj) || obj == null) + return false; + + try + { + value = Convert.ToDouble(obj); + return true; + } + catch + { + return false; + } + } +} diff --git a/XP.ImageProcessing.Processors/定位识别/RoiAlignmentProcessor.cs b/XP.ImageProcessing.Processors/定位识别/RoiAlignmentProcessor.cs new file mode 100644 index 0000000..78a4bd6 --- /dev/null +++ b/XP.ImageProcessing.Processors/定位识别/RoiAlignmentProcessor.cs @@ -0,0 +1,130 @@ +// ============================================================================ +// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. +// 文件名: RoiAlignmentProcessor.cs +// 描述: 示教多边形 ROI 刚体对齐(平移+旋转) +// +// 流水线约定: +// 输入: 示教 Poly* + ReferencePose(Ref*) + 上一步模板匹配的 MeasuredPose(Measured*) +// 输出: OutputData 中含变换后的 PolyCount/PolyX/PolyY/RoiMode,可拷贝到 VoidMeasurement 等算子 +// 或: 流水线直接调用 XP.ImageProcessing.Core.Alignment.RoiAlignmentApplier +// ============================================================================ + +using Emgu.CV; +using Emgu.CV.Structure; +using XP.ImageProcessing.Core; +using XP.ImageProcessing.Core.Alignment; +using Serilog; + +namespace XP.ImageProcessing.Processors; + +/// +/// 将示教图上的多边形 ROI 变换到当前图(不修改图像,仅输出对齐后的 ROI 参数)。 +/// +public class RoiAlignmentProcessor : ImageProcessorBase +{ + private static readonly ILogger _logger = Log.ForContext(); + + public RoiAlignmentProcessor() + { + Name = LocalizationHelper.GetString("RoiAlignmentProcessor_Name"); + Description = LocalizationHelper.GetString("RoiAlignmentProcessor_Description"); + } + + protected override void InitializeParameters() + { + Parameters.Add("RefCenterX", new ProcessorParameter( + "RefCenterX", + LocalizationHelper.GetString("RoiAlignmentProcessor_RefCenterX"), + typeof(double), 0.0, null, null, + LocalizationHelper.GetString("RoiAlignmentProcessor_RefCenterX_Desc"))); + + Parameters.Add("RefCenterY", new ProcessorParameter( + "RefCenterY", + LocalizationHelper.GetString("RoiAlignmentProcessor_RefCenterY"), + typeof(double), 0.0, null, null, + LocalizationHelper.GetString("RoiAlignmentProcessor_RefCenterY_Desc"))); + + Parameters.Add("RefAngle", new ProcessorParameter( + "RefAngle", + LocalizationHelper.GetString("RoiAlignmentProcessor_RefAngle"), + typeof(double), 0.0, -180.0, 180.0, + LocalizationHelper.GetString("RoiAlignmentProcessor_RefAngle_Desc"))); + + Parameters.Add("MeasuredCenterX", new ProcessorParameter( + "MeasuredCenterX", + LocalizationHelper.GetString("RoiAlignmentProcessor_MeasuredCenterX"), + typeof(double), 0.0, null, null, + LocalizationHelper.GetString("RoiAlignmentProcessor_MeasuredCenterX_Desc"))); + + Parameters.Add("MeasuredCenterY", new ProcessorParameter( + "MeasuredCenterY", + LocalizationHelper.GetString("RoiAlignmentProcessor_MeasuredCenterY"), + typeof(double), 0.0, null, null, + LocalizationHelper.GetString("RoiAlignmentProcessor_MeasuredCenterY_Desc"))); + + Parameters.Add("MeasuredAngle", new ProcessorParameter( + "MeasuredAngle", + LocalizationHelper.GetString("RoiAlignmentProcessor_MeasuredAngle"), + typeof(double), 0.0, -180.0, 180.0, + LocalizationHelper.GetString("RoiAlignmentProcessor_MeasuredAngle_Desc"))); + + Parameters.Add(RoiPolygonParameterNames.PolyCount, new ProcessorParameter( + RoiPolygonParameterNames.PolyCount, + RoiPolygonParameterNames.PolyCount, + typeof(int), 0, null, null, + "") { IsVisible = false }); + + for (int i = 0; i < RoiPolygonParameterNames.MaxPoints; i++) + { + Parameters.Add(RoiPolygonParameterNames.PolyX(i), new ProcessorParameter( + RoiPolygonParameterNames.PolyX(i), + RoiPolygonParameterNames.PolyX(i), + typeof(int), 0, null, null, + "") { IsVisible = false }); + + Parameters.Add(RoiPolygonParameterNames.PolyY(i), new ProcessorParameter( + RoiPolygonParameterNames.PolyY(i), + RoiPolygonParameterNames.PolyY(i), + typeof(int), 0, null, null, + "") { IsVisible = false }); + } + } + + public override Image Process(Image inputImage) + { + OutputData.Clear(); + + var reference = new Pose2D( + GetParameter("RefCenterX"), + GetParameter("RefCenterY"), + GetParameter("RefAngle")); + + var measured = new Pose2D( + GetParameter("MeasuredCenterX"), + GetParameter("MeasuredCenterY"), + GetParameter("MeasuredAngle")); + + var paramDict = Parameters.ToDictionary(p => p.Key, p => p.Value.Value); + var teachPoints = RoiAlignmentApplier.ReadTeachPolygon(paramDict); + + var result = RoiAlignmentApplier.Apply(reference, teachPoints, measured); + RoiAlignmentApplier.WriteToOutputData(OutputData, result); + OutputData[RoiAlignmentOutputKeys.Success] = result.Success; + if (!string.IsNullOrEmpty(result.ErrorMessage)) + OutputData[RoiAlignmentOutputKeys.Message] = result.ErrorMessage!; + + OutputData["ResultText"] = OutputData[RoiAlignmentOutputKeys.ResultText] = result.Success + ? $"ROI aligned: {result.TransformedPointsInt.Count} pts" + : $"ROI align failed: {result.ErrorMessage}"; + + if (result.Success) + _logger.Debug("RoiAlignment: {Count} points, ref=({Rx:F1},{Ry:F1},{Ra:F1}) meas=({Mx:F1},{My:F1},{Ma:F1})", + result.TransformedPointsInt.Count, + reference.X, reference.Y, reference.AngleDegrees, + measured.X, measured.Y, measured.AngleDegrees); + else + _logger.Warning("RoiAlignment failed: {Msg}", result.ErrorMessage); + + return inputImage.Clone(); + } +} diff --git a/XP.ImageProcessing.Processors/定位识别/TemplateMatchAlignmentExtensions.cs b/XP.ImageProcessing.Processors/定位识别/TemplateMatchAlignmentExtensions.cs index 873650f..be222ff 100644 --- a/XP.ImageProcessing.Processors/定位识别/TemplateMatchAlignmentExtensions.cs +++ b/XP.ImageProcessing.Processors/定位识别/TemplateMatchAlignmentExtensions.cs @@ -24,4 +24,19 @@ public static class TemplateMatchAlignmentExtensions result.LbX, result.LbY, tolerancePixels); + + /// 从 RotatedTemplateMatching 的 OutputData 读取测量位姿。 + public static bool TryReadMeasuredPose( + IReadOnlyDictionary? outputData, + out Pose2D measuredPose, + out string? errorMessage, + int matchIndex = 0) + => TemplateMatchOutputReader.TryReadMeasuredPose(outputData, out measuredPose, out errorMessage, matchIndex); + + /// 用匹配结果 + 示教配方一步得到对齐后的 ROI。 + public static RoiAlignmentResult AlignRecipe( + AlignmentRecipe recipe, + IReadOnlyDictionary? templateMatchOutput, + int matchIndex = 0) + => RoiAlignmentApplier.ApplyFromTemplateMatchOutput(recipe, templateMatchOutput, matchIndex); } diff --git a/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs b/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs index 16235db..8d433ea 100644 --- a/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs +++ b/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs @@ -36,7 +36,7 @@ namespace XplorePlane.Services return "图像增强"; // 必须在 "Rotate" 之前:RotatedTemplateMatching 含子串 Rotate - if (ContainsAny(operatorKey, "RotatedTemplateMatching")) + if (ContainsAny(operatorKey, "RotatedTemplateMatching", "RoiAlignment")) return "定位识别"; if (ContainsAny(operatorKey, "Mirror", "Rotate", "Grayscale", "Threshold")) @@ -115,6 +115,8 @@ namespace XplorePlane.Services // 必须在 "Rotate" 之前:RotatedTemplateMatching 含子串 Rotate if (ContainsAny(operatorKey, "RotatedTemplateMatching")) return "🎯"; + if (ContainsAny(operatorKey, "RoiAlignment")) + return "📐"; if (ContainsAny(operatorKey, "Mirror")) return "↔"; if (ContainsAny(operatorKey, "Rotate"))