新增 ROI 对齐基础能力并打通到算子与 UI。

统一补齐对齐核心工具类、RoiAlignment 算子、模板匹配对齐扩展和多语言资源,便于在检测前稳定完成示教 ROI 到运行图的变换。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
李伟
2026-06-01 17:04:32 +08:00
parent b0397365b2
commit 1874c4a5bb
15 changed files with 790 additions and 1 deletions
+1
View File
@@ -29,6 +29,7 @@ bld/
[Ll]ogs/
lib/
XP.ImageProcessing/
XP.ImageProcessing.SmokeTest/
ImageProcessing.sln
# 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构)
+50
View File
@@ -1551,6 +1551,50 @@
<value>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.</value>
</data>
<!-- RoiAlignmentProcessor -->
<data name="RoiAlignmentProcessor_Name" xml:space="preserve">
<value>ROI Alignment</value>
</data>
<data name="RoiAlignmentProcessor_Description" xml:space="preserve">
<value>Transform teach polygon ROI to the run image using reference and template-match poses (outputs Poly params for downstream inspectors).</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterX" xml:space="preserve">
<value>Reference center X</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterX_Desc" xml:space="preserve">
<value>Template/part center X on the teach image (pixels).</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterY" xml:space="preserve">
<value>Reference center Y</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterY_Desc" xml:space="preserve">
<value>Template/part center Y on the teach image (pixels).</value>
</data>
<data name="RoiAlignmentProcessor_RefAngle" xml:space="preserve">
<value>Reference angle (°)</value>
</data>
<data name="RoiAlignmentProcessor_RefAngle_Desc" xml:space="preserve">
<value>Reference angle on the teach image; usually 0.</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterX" xml:space="preserve">
<value>Measured center X</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterX_Desc" xml:space="preserve">
<value>Match center X on the current image; inject from previous step OutputData.</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterY" xml:space="preserve">
<value>Measured center Y</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterY_Desc" xml:space="preserve">
<value>Match center Y on the current image.</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredAngle" xml:space="preserve">
<value>Measured angle (°)</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredAngle_Desc" xml:space="preserve">
<value>Match angle on the current image.</value>
</data>
<data name="AngleMeasurementProcessor_Name" xml:space="preserve">
<value>Angle Measurement</value>
</data>
@@ -2106,4 +2150,10 @@ Reprojection error: {1:F4} pixels</value>
<data name="QfnLeadPadVoidProcessor_Thickness_Desc" xml:space="preserve">
<value>Contour drawing line thickness</value>
</data>
<data name="QfnAutoDetectionProcessor_Name" xml:space="preserve">
<value>QFN Integrated Detection</value>
</data>
<data name="QfnAutoDetectionProcessor_Description" xml:space="preserve">
<value>After template-match alignment, automatically run center pad void and lead pad void inspections and aggregate the final classification.</value>
</data>
</root>
+50
View File
@@ -1573,6 +1573,50 @@
<value>已训练模型文件路径(.tmmodel)。若存在则直接加载跳过学习;若不存在则从模板学习后自动保存。</value>
</data>
<!-- RoiAlignmentProcessor -->
<data name="RoiAlignmentProcessor_Name" xml:space="preserve">
<value>ROI对齐</value>
</data>
<data name="RoiAlignmentProcessor_Description" xml:space="preserve">
<value>按示教位姿与模板匹配位姿,将示教多边形 ROI 变换到运行图(输出 Poly 参数供下游检测算子使用)。</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterX" xml:space="preserve">
<value>基准中心X</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterX_Desc" xml:space="preserve">
<value>示教图上的模板/器件中心 X(像素)。</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterY" xml:space="preserve">
<value>基准中心Y</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterY_Desc" xml:space="preserve">
<value>示教图上的模板/器件中心 Y(像素)。</value>
</data>
<data name="RoiAlignmentProcessor_RefAngle" xml:space="preserve">
<value>基准角度(°)</value>
</data>
<data name="RoiAlignmentProcessor_RefAngle_Desc" xml:space="preserve">
<value>示教图上的基准角度,通常为 0。</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterX" xml:space="preserve">
<value>测量中心X</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterX_Desc" xml:space="preserve">
<value>当前图上模板匹配中心 X,由流水线从上一步 OutputData 注入。</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterY" xml:space="preserve">
<value>测量中心Y</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterY_Desc" xml:space="preserve">
<value>当前图上模板匹配中心 Y。</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredAngle" xml:space="preserve">
<value>测量角度(°)</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredAngle_Desc" xml:space="preserve">
<value>当前图上模板匹配角度。</value>
</data>
<!-- AngleMeasurementProcessor -->
<data name="AngleMeasurementProcessor_Name" xml:space="preserve">
<value>角度测量</value>
@@ -2139,4 +2183,10 @@
<data name="QfnLeadPadVoidProcessor_Thickness_Desc" xml:space="preserve">
<value>轮廓绘制线条粗细</value>
</data>
<data name="QfnAutoDetectionProcessor_Name" xml:space="preserve">
<value>QFN一体检测</value>
</data>
<data name="QfnAutoDetectionProcessor_Description" xml:space="preserve">
<value>模板匹配对齐后,自动完成中心焊盘空洞与引脚空洞检测并汇总判定</value>
</data>
</root>
+50
View File
@@ -1545,6 +1545,50 @@
<value>未找到 TemplateMatchLib.dll,请先编译 C++ DLL 工程。</value>
</data>
<!-- RoiAlignmentProcessor -->
<data name="RoiAlignmentProcessor_Name" xml:space="preserve">
<value>ROI对齐</value>
</data>
<data name="RoiAlignmentProcessor_Description" xml:space="preserve">
<value>按示教位姿与模板匹配位姿,将示教多边形 ROI 变换到运行图(输出 Poly 参数供下游检测算子使用)。</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterX" xml:space="preserve">
<value>基准中心X</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterX_Desc" xml:space="preserve">
<value>示教图上的模板/器件中心 X(像素)。</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterY" xml:space="preserve">
<value>基准中心Y</value>
</data>
<data name="RoiAlignmentProcessor_RefCenterY_Desc" xml:space="preserve">
<value>示教图上的模板/器件中心 Y(像素)。</value>
</data>
<data name="RoiAlignmentProcessor_RefAngle" xml:space="preserve">
<value>基准角度(°)</value>
</data>
<data name="RoiAlignmentProcessor_RefAngle_Desc" xml:space="preserve">
<value>示教图上的基准角度,通常为 0。</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterX" xml:space="preserve">
<value>测量中心X</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterX_Desc" xml:space="preserve">
<value>当前图上模板匹配中心 X,由流水线从上一步 OutputData 注入。</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterY" xml:space="preserve">
<value>测量中心Y</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredCenterY_Desc" xml:space="preserve">
<value>当前图上模板匹配中心 Y。</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredAngle" xml:space="preserve">
<value>测量角度(°)</value>
</data>
<data name="RoiAlignmentProcessor_MeasuredAngle_Desc" xml:space="preserve">
<value>当前图上模板匹配角度。</value>
</data>
<data name="AngleMeasurementProcessor_Name" xml:space="preserve">
<value>角度测量</value>
</data>
@@ -2100,4 +2144,10 @@
<data name="QfnLeadPadVoidProcessor_Thickness_Desc" xml:space="preserve">
<value>轮廓绘制线条粗细</value>
</data>
<data name="QfnAutoDetectionProcessor_Name" xml:space="preserve">
<value>QFN一体检测</value>
</data>
<data name="QfnAutoDetectionProcessor_Description" xml:space="preserve">
<value>模板匹配对齐后,自动完成中心焊盘空洞与引脚空洞检测并汇总判定</value>
</data>
</root>
@@ -18,4 +18,17 @@ public sealed class AlignmentRecipe
/// <summary>变换为整型顶点,供检测算子注入。</summary>
public (int X, int Y)[] TransformRoiToInt(Pose2D measuredPose)
=> RoiAlignment.TransformPolygonToInt(RoiPoints, ReferencePose, measuredPose);
/// <summary>从算子参数字典读取示教多边形并填充 <see cref="RoiPoints"/>。</summary>
public static AlignmentRecipe FromTeachParameters(
Pose2D referencePose,
IReadOnlyDictionary<string, object> parameters)
{
var points = RoiAlignmentApplier.ReadTeachPolygon(parameters);
return new AlignmentRecipe
{
ReferencePose = referencePose,
RoiPoints = points.ToList()
};
}
}
@@ -0,0 +1,146 @@
namespace XP.ImageProcessing.Core.Alignment;
/// <summary>
/// 示教 ROI + 基准/测量位姿 → 运行图多边形;供 <c>RoiAlignment</c> 算子与流水线胶水调用。
/// </summary>
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<Point2D> 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<string, object>? 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<Point2D> ReadTeachPolygon(IReadOnlyDictionary<string, object> parameters)
{
if (parameters == null)
return Array.Empty<Point2D>();
if (!parameters.TryGetValue(RoiPolygonParameterNames.PolyCount, out var countObj))
return Array.Empty<Point2D>();
int count = Convert.ToInt32(countObj);
if (count < 3)
return Array.Empty<Point2D>();
count = Math.Min(count, RoiPolygonParameterNames.MaxPoints);
var points = new List<Point2D>(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;
}
/// <summary>
/// 将变换结果写入参数字典(PolyCount、PolyX/PolyY、RoiMode),供下游 VoidMeasurement 等算子使用。
/// </summary>
public static void WriteToParameters(
IDictionary<string, object> 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";
}
/// <summary>
/// 将变换结果写入算子 OutputData(与 <see cref="WriteToParameters"/> 键名一致,便于流水线拷贝)。
/// </summary>
public static void WriteToOutputData(IDictionary<string, object> 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
};
}
@@ -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`,减少中心约定偏差。
@@ -0,0 +1,21 @@
namespace XP.ImageProcessing.Core.Alignment;
/// <summary>
/// RoiAlignment 算子(键 <see cref="ProcessorKey"/>)与 <see cref="RoiAlignmentApplier"/> 的输出/参数字典键名,供流水线拷贝到下游检测算子。
/// </summary>
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";
}
@@ -0,0 +1,114 @@
namespace XP.ImageProcessing.Core.Alignment;
/// <summary>
/// 流水线胶水入口:模板匹配 OutputData → ROI 变换 → 写入下游 VoidMeasurement 等算子参数字典。
/// 不依赖 XplorePlane;由 PipelineExecutionService 在步骤间调用。
/// </summary>
public static class RoiAlignmentPipelineBridge
{
/// <summary>
/// 将测量位姿写入 RoiAlignment 算子参数(MeasuredCenterX/Y/Angle)。
/// </summary>
public static void InjectMeasuredPose(IDictionary<string, object> roiAlignmentParameters, Pose2D measuredPose)
{
if (roiAlignmentParameters == null)
throw new ArgumentNullException(nameof(roiAlignmentParameters));
roiAlignmentParameters["MeasuredCenterX"] = measuredPose.X;
roiAlignmentParameters["MeasuredCenterY"] = measuredPose.Y;
roiAlignmentParameters["MeasuredAngle"] = measuredPose.AngleDegrees;
}
/// <summary>
/// 将示教基准位姿写入 RoiAlignment 算子参数(RefCenterX/Y/Angle)。
/// </summary>
public static void InjectReferencePose(IDictionary<string, object> roiAlignmentParameters, Pose2D referencePose)
{
if (roiAlignmentParameters == null)
throw new ArgumentNullException(nameof(roiAlignmentParameters));
roiAlignmentParameters["RefCenterX"] = referencePose.X;
roiAlignmentParameters["RefCenterY"] = referencePose.Y;
roiAlignmentParameters["RefAngle"] = referencePose.AngleDegrees;
}
/// <summary>
/// 从模板匹配一步 OutputData 读取位姿并对齐示教 ROI。
/// </summary>
public static bool TryAlignFromTemplateMatch(
AlignmentRecipe recipe,
IReadOnlyDictionary<string, object>? templateMatchOutput,
out RoiAlignmentResult result,
int matchIndex = 0)
{
result = RoiAlignmentApplier.ApplyFromTemplateMatchOutput(recipe, templateMatchOutput, matchIndex);
return result.Success;
}
/// <summary>
/// 将 RoiAlignment 算子 OutputData 中的 Poly* 拷贝到检测算子参数字典(如 VoidMeasurement)。
/// </summary>
public static bool TryCopyAlignedRoiToDetectionParameters(
IReadOnlyDictionary<string, object>? roiAlignmentOutput,
IDictionary<string, object> 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<string, object> 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;
}
}
@@ -0,0 +1,19 @@
namespace XP.ImageProcessing.Core.Alignment;
/// <summary>
/// ROI 对齐变换结果,供流水线写入下游检测算子参数。
/// </summary>
public sealed class RoiAlignmentResult
{
public bool Success { get; init; }
public string? ErrorMessage { get; init; }
public Pose2D ReferencePose { get; init; }
public Pose2D MeasuredPose { get; init; }
/// <summary>运行图坐标下的多边形顶点(至少 3 点)。</summary>
public IReadOnlyList<Point2D> TransformedPoints { get; init; } = Array.Empty<Point2D>();
/// <summary>四舍五入后的整型顶点,可直接写入 PolyX/PolyY。</summary>
public IReadOnlyList<(int X, int Y)> TransformedPointsInt { get; init; } = Array.Empty<(int X, int Y)>();
}
@@ -0,0 +1,15 @@
namespace XP.ImageProcessing.Core.Alignment;
/// <summary>
/// 与 VoidMeasurement / BgaVoidRate / QfnLeadPadVoid 等算子一致的多边形 ROI 参数名。
/// </summary>
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}";
}
@@ -0,0 +1,82 @@
namespace XP.ImageProcessing.Core.Alignment;
/// <summary>
/// 从旋转模板匹配算子(键 <see cref="ProcessorKey"/>)的 OutputData 读取测量位姿。
/// </summary>
public static class TemplateMatchOutputReader
{
public const string ProcessorKey = "RotatedTemplateMatching";
public static bool TryReadMeasuredPose(
IReadOnlyDictionary<string, object>? 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<string, object> 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;
}
}
}
@@ -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;
/// <summary>
/// 将示教图上的多边形 ROI 变换到当前图(不修改图像,仅输出对齐后的 ROI 参数)。
/// </summary>
public class RoiAlignmentProcessor : ImageProcessorBase
{
private static readonly ILogger _logger = Log.ForContext<RoiAlignmentProcessor>();
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<Gray, byte> Process(Image<Gray, byte> inputImage)
{
OutputData.Clear();
var reference = new Pose2D(
GetParameter<double>("RefCenterX"),
GetParameter<double>("RefCenterY"),
GetParameter<double>("RefAngle"));
var measured = new Pose2D(
GetParameter<double>("MeasuredCenterX"),
GetParameter<double>("MeasuredCenterY"),
GetParameter<double>("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();
}
}
@@ -24,4 +24,19 @@ public static class TemplateMatchAlignmentExtensions
result.LbX,
result.LbY,
tolerancePixels);
/// <summary>从 RotatedTemplateMatching 的 OutputData 读取测量位姿。</summary>
public static bool TryReadMeasuredPose(
IReadOnlyDictionary<string, object>? outputData,
out Pose2D measuredPose,
out string? errorMessage,
int matchIndex = 0)
=> TemplateMatchOutputReader.TryReadMeasuredPose(outputData, out measuredPose, out errorMessage, matchIndex);
/// <summary>用匹配结果 + 示教配方一步得到对齐后的 ROI。</summary>
public static RoiAlignmentResult AlignRecipe(
AlignmentRecipe recipe,
IReadOnlyDictionary<string, object>? templateMatchOutput,
int matchIndex = 0)
=> RoiAlignmentApplier.ApplyFromTemplateMatchOutput(recipe, templateMatchOutput, matchIndex);
}
@@ -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"))