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"))