新增 ROI 对齐基础能力并打通到算子与 UI。
统一补齐对齐核心工具类、RoiAlignment 算子、模板匹配对齐扩展和多语言资源,便于在检测前稳定完成示教 ROI 到运行图的变换。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user