新增 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
@@ -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;
}
}
}