新增 ROI 对齐基础能力并打通到算子与 UI。
统一补齐对齐核心工具类、RoiAlignment 算子、模板匹配对齐扩展和多语言资源,便于在检测前稳定完成示教 ROI 到运行图的变换。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,6 +29,7 @@ bld/
|
||||
[Ll]ogs/
|
||||
lib/
|
||||
XP.ImageProcessing/
|
||||
XP.ImageProcessing.SmokeTest/
|
||||
ImageProcessing.sln
|
||||
|
||||
# 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user