Files
XplorePlane/XP.ImageProcessing.Processors/定位识别/TemplateMatchingProcessor.cs
T
2026-05-13 09:05:05 +08:00

436 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
// 文件名: TemplateMatchingProcessor.cs
// 描述: 灰度模板匹配算子,在整幅图像中搜索与模板最相似的位置
// 功能:
// - 从磁盘加载模板(灰度或彩色图自动转灰度)
// - OpenCV MatchTemplate + MinMaxLoc 求最佳匹配
// - 可选在输出图上绘制匹配矩形
// - 将匹配结果写入 OutputData(坐标、得分、是否通过阈值)
// 作者: 李伟 wei.lw.li@hexagon.com
// ============================================================================
using System.Drawing;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using XP.ImageProcessing.Core;
using Serilog;
namespace XP.ImageProcessing.Processors;
/// <summary>
/// 模板匹配算子(定位识别)
/// </summary>
/// <remarks>
/// 算法原理:
/// 模板匹配是一种基于图像块的匹配方法,通过在待搜索图像上滑动模板,
/// 计算每个位置与模板的相似度,找到最佳匹配位置。
///
/// 匹配方法说明:
/// 1. CcoeffNormed (归一化相关系数) - 推荐使用,对光照变化有一定的鲁棒性
/// 公式: corr = Σ(I(x,y) * T(x,y)) / sqrt(ΣI² * ΣT²)
/// 分数范围: -1 ~ 1,值越大越相似
///
/// 2. SqdiffNormed (归一化平方差) - 值越小越相似
/// 公式: diff = Σ(I(x,y) - T(x,y))² / (ΣI² + ΣT²)
/// 分数范围: 0 ~ 1,值越小越相似
///
/// 3. CcorrNormed (归一化相关) - 对模板和图像的亮度变化敏感
/// 4. Ccoeff (相关系数) - 未归一化版本
/// 5. Ccorr (相关) - 未归一化版本
/// 6. Sqdiff (平方差) - 未归一化版本
///
/// 性能说明:
/// - 时间复杂度: O(W * H * w * h),其中W/H为图像尺寸,w/h为模板尺寸
/// - 可通过设置SearchRegion限制搜索范围来提升性能
/// </remarks>
public class TemplateMatchingProcessor : ImageProcessorBase
{
private static readonly ILogger _logger = Log.ForContext<TemplateMatchingProcessor>();
/// <summary>
/// 匹配方法选项列表,供UI下拉选择
/// </summary>
private static readonly string[] MatchMethodOptions =
{
"CcoeffNormed", // 归一化相关系数(推荐)
"SqdiffNormed", // 归一化平方差
"CcorrNormed", // 归一化相关
"Ccoeff", // 相关系数
"Ccorr", // 相关
"Sqdiff" // 平方差
};
public TemplateMatchingProcessor()
{
Name = LocalizationHelper.GetString("TemplateMatchingProcessor_Name");
Description = LocalizationHelper.GetString("TemplateMatchingProcessor_Description");
}
/// <summary>
/// 初始化参数定义
/// </summary>
protected override void InitializeParameters()
{
// ===== 模板相关参数 =====
/// <summary>
/// 模板图片路径,支持灰度或彩色图片(自动转灰度)
/// </summary>
Parameters.Add("TemplatePath", new ProcessorParameter(
"TemplatePath",
LocalizationHelper.GetString("TemplateMatchingProcessor_TemplatePath"),
typeof(string),
string.Empty,
null,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_TemplatePath_Desc")));
/// <summary>
/// 匹配算法选择,不同算法对光照和旋转的敏感度不同
/// </summary>
Parameters.Add("MatchMethod", new ProcessorParameter(
"MatchMethod",
LocalizationHelper.GetString("TemplateMatchingProcessor_MatchMethod"),
typeof(string),
"CcoeffNormed", // 默认使用归一化相关系数
null,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_MatchMethod_Desc"),
MatchMethodOptions));
/// <summary>
/// 匹配阈值,判断匹配是否成功的分数门限
/// - CcoeffNormed/CcorrNormed: 建议 0.75-0.95
/// - SqdiffNormed: 建议 0.1-0.3
/// </summary>
Parameters.Add("MatchThreshold", new ProcessorParameter(
"MatchThreshold",
LocalizationHelper.GetString("TemplateMatchingProcessor_MatchThreshold"),
typeof(double),
0.75, // 默认阈值
0.0,
1.0,
LocalizationHelper.GetString("TemplateMatchingProcessor_MatchThreshold_Desc")));
/// <summary>
/// 是否在输出图像上绘制匹配矩形框
/// </summary>
Parameters.Add("DrawRectangle", new ProcessorParameter(
"DrawRectangle",
LocalizationHelper.GetString("TemplateMatchingProcessor_DrawMatch"),
typeof(bool),
true,
null,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_DrawMatch_Desc")));
/// <summary>
/// 匹配矩形框的线条粗细
/// </summary>
Parameters.Add("RectangleThickness", new ProcessorParameter(
"RectangleThickness",
LocalizationHelper.GetString("TemplateMatchingProcessor_RectThickness"),
typeof(int),
2,
1,
8,
LocalizationHelper.GetString("TemplateMatchingProcessor_RectThickness_Desc")));
// ===== 搜索区域参数(可选,用于限制搜索范围提升性能)=====
/// <summary>
/// 搜索区域左上角X坐标
/// </summary>
Parameters.Add("SearchRegionX", new ProcessorParameter(
"SearchRegionX",
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionX"),
typeof(int),
0,
0,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc")));
/// <summary>
/// 搜索区域左上角Y坐标
/// </summary>
Parameters.Add("SearchRegionY", new ProcessorParameter(
"SearchRegionY",
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionY"),
typeof(int),
0,
0,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc")));
/// <summary>
/// 搜索区域宽度,0表示使用整幅图像
/// </summary>
Parameters.Add("SearchRegionWidth", new ProcessorParameter(
"SearchRegionWidth",
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionWidth"),
typeof(int),
0,
0,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc")));
/// <summary>
/// 搜索区域高度,0表示使用整幅图像
/// </summary>
Parameters.Add("SearchRegionHeight", new ProcessorParameter(
"SearchRegionHeight",
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionHeight"),
typeof(int),
0,
0,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc")));
_logger.Debug("InitializeParameters");
}
/// <summary>
/// 执行模板匹配处理
/// </summary>
/// <param name="inputImage">输入的灰度图像</param>
/// <returns>处理后的图像(可选带匹配矩形框)</returns>
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
{
// ===== 1. 获取参数 =====
var path = (GetParameter<string>("TemplatePath") ?? string.Empty).Trim();
var methodName = GetParameter<string>("MatchMethod") ?? "CcoeffNormed";
var threshold = GetParameter<double>("MatchThreshold");
var draw = GetParameter<bool>("DrawRectangle");
var thickness = GetParameter<int>("RectangleThickness");
var searchRx = GetParameter<int>("SearchRegionX");
var searchRy = GetParameter<int>("SearchRegionY");
var searchRw = GetParameter<int>("SearchRegionWidth");
var searchRh = GetParameter<int>("SearchRegionHeight");
// 清除上一次的输出数据
OutputData.Clear();
// 克隆输入图像用于输出(避免修改原图)
var output = inputImage.Clone();
// ===== 2. 参数校验:模板文件 =====
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
_logger.Warning("TemplateMatching: invalid or missing template file: {Path}", path);
OutputData["Matched"] = false;
OutputData["Message"] = "Template file not found";
return output;
}
// ===== 3. 加载模板图像 =====
using var template = LoadTemplate(path);
if (template == null)
{
OutputData["Matched"] = false;
OutputData["Message"] = "Template load failed";
return output;
}
// ===== 4. 确定搜索区域(ROI=====
// 如果未指定搜索区域,则使用整幅图像
var searchRoi = ResolveSearchRoi(inputImage.Width, inputImage.Height, searchRx, searchRy, searchRw, searchRh);
var offsetX = searchRoi.X; // 记录ROI偏移,用于还原到全局坐标
var offsetY = searchRoi.Y;
// ===== 5. 在ROI区域内执行模板匹配 =====
Image<Gray, byte>? roiImage = null;
try
{
// 设置输入图像的ROI(感兴趣区域)
inputImage.ROI = searchRoi;
// 复制ROI区域到新图像
roiImage = inputImage.Copy();
// 清除ROI设置
inputImage.ROI = Rectangle.Empty;
// ===== 5.1 校验模板尺寸 =====
if (template.Width > roiImage.Width || template.Height > roiImage.Height)
{
_logger.Warning("TemplateMatching: template larger than search region ({Tw}x{Th} vs {Iw}x{Ih})",
template.Width, template.Height, roiImage.Width, roiImage.Height);
OutputData["Matched"] = false;
OutputData["Message"] = "Template larger than search region";
return output;
}
// ===== 5.2 执行模板匹配 =====
// OpenCV MatchTemplate 在ROI上滑动模板,计算每个位置的相似度
// 结果是一个 (W-w+1) x (H-h+1) 的分数矩阵
var method = ParseMethod(methodName);
using var resultMat = new Mat();
CvInvoke.MatchTemplate(roiImage, template, resultMat, method);
// ===== 5.3 找到最佳匹配位置 =====
// MinMaxLoc 在分数矩阵中找到最小值和最大值的位置
double minVal = 0, maxVal = 0;
Point minLoc = default, maxLoc = default;
CvInvoke.MinMaxLoc(resultMat, ref minVal, ref maxVal, ref minLoc, ref maxLoc);
// 根据匹配方法选择使用最小值还是最大值
// 平方差类方法:值越小越好(使用minLoc)
// 相关类方法:值越大越好(使用maxLoc)
var useMin = method == TemplateMatchingType.Sqdiff || method == TemplateMatchingType.SqdiffNormed;
var loc = useMin ? minLoc : maxLoc;
var score = useMin ? minVal : maxVal;
// ===== 5.4 判定匹配是否成功 =====
var matched = IsMatchAcceptable(method, minVal, maxVal, threshold);
// ===== 5.5 转换到全局坐标 =====
// 由于是在ROI内匹配的,需要加上ROI的偏移量
var globalLoc = new Point(loc.X + offsetX, loc.Y + offsetY);
// ===== 5.6 输出结果数据 =====
OutputData["Matched"] = matched;
OutputData["MatchScore"] = score;
OutputData["MatchX"] = globalLoc.X;
OutputData["MatchY"] = globalLoc.Y;
OutputData["TemplateWidth"] = template.Width;
OutputData["TemplateHeight"] = template.Height;
OutputData["MatchMethod"] = methodName;
// ===== 5.7 可选:绘制匹配矩形 =====
if (matched && draw)
{
var rect = new Rectangle(globalLoc.X, globalLoc.Y, template.Width, template.Height);
CvInvoke.Rectangle(output, rect, new MCvScalar(255), thickness);
}
_logger.Debug("TemplateMatching: Matched={Matched}, Score={Score}, Origin=({X},{Y}), SearchRoi=({Rx},{Ry},{Rw},{Rh})",
matched, score, globalLoc.X, globalLoc.Y, searchRoi.X, searchRoi.Y, searchRoi.Width, searchRoi.Height);
return output;
}
finally
{
// 释放ROI图像资源
roiImage?.Dispose();
}
}
/// <summary>
/// 计算实际的搜索ROI区域
/// </summary>
/// <param name="imgW">图像宽度</param>
/// <param name="imgH">图像高度</param>
/// <param name="rx">用户指定的ROI X坐标</param>
/// <param name="ry">用户指定的ROI Y坐标</param>
/// <param name="rw">用户指定的ROI宽度,0表示整幅图像</param>
/// <param name="rh">用户指定的ROI高度,0表示整幅图像</param>
/// <returns>计算后的有效ROI区域</returns>
private static Rectangle ResolveSearchRoi(int imgW, int imgH, int rx, int ry, int rw, int rh)
{
// 宽度或高度为0时,使用整幅图像作为搜索区域
if (rw <= 0 || rh <= 0)
return new Rectangle(0, 0, imgW, imgH);
// 限制坐标在图像范围内
rx = Math.Clamp(rx, 0, Math.Max(0, imgW - 1));
ry = Math.Clamp(ry, 0, Math.Max(0, imgH - 1));
// 限制宽度和高度不超出图像边界
rw = Math.Clamp(rw, 1, Math.Max(1, imgW - rx));
rh = Math.Clamp(rh, 1, Math.Max(1, imgH - ry));
return new Rectangle(rx, ry, rw, rh);
}
/// <summary>
/// 判断匹配结果是否满足阈值条件
/// </summary>
/// <param name="method">匹配方法类型</param>
/// <param name="minVal">最小相似度分数</param>
/// <param name="maxVal">最大相似度分数</param>
/// <param name="threshold">用户设定的阈值</param>
/// <returns>是否满足匹配条件</returns>
/// <remarks>
/// 不同匹配方法的分数含义不同:
/// - 平方差类(Sqdiff/SqdiffNormed): 分数越小越相似,需要 minVal <= threshold
/// - 相关类(Ccoeff/Ccorr/CcoeffNormed/CcorrNormed): 分数越大越相似,需要 maxVal >= threshold
/// </remarks>
private static bool IsMatchAcceptable(TemplateMatchingType method, double minVal, double maxVal, double threshold)
{
return method switch
{
// 平方差类:值越小越好
TemplateMatchingType.SqdiffNormed => minVal <= threshold,
TemplateMatchingType.Sqdiff => minVal <= threshold,
// 相关类:值越大越好
TemplateMatchingType.CcorrNormed or TemplateMatchingType.CcoeffNormed => maxVal >= threshold,
TemplateMatchingType.Ccorr or TemplateMatchingType.Ccoeff => maxVal >= threshold,
// 默认按相关类处理
_ => maxVal >= threshold
};
}
/// <summary>
/// 将字符串方法名转换为OpenCV枚举类型
/// </summary>
/// <param name="name">方法名称字符串</param>
/// <returns>对应的TemplateMatchingType枚举值</returns>
private static TemplateMatchingType ParseMethod(string? name)
{
return name?.Trim() switch
{
"Sqdiff" => TemplateMatchingType.Sqdiff,
"SqdiffNormed" => TemplateMatchingType.SqdiffNormed,
"Ccorr" => TemplateMatchingType.Ccorr,
"CcorrNormed" => TemplateMatchingType.CcorrNormed,
"Ccoeff" => TemplateMatchingType.Ccoeff,
// 默认返回归一化相关系数(推荐)
_ => TemplateMatchingType.CcoeffNormed
};
}
/// <summary>
/// 从磁盘加载模板图像并转换为灰度图
/// </summary>
/// <param name="path">模板图片文件路径</param>
/// <returns>灰度模板图像,加载失败返回null</returns>
/// <remarks>
/// 支持的输入格式:
/// - 灰度图像:直接使用
/// - 彩色图像:自动转换为灰度
/// - 支持任意位深度的图像
/// </remarks>
private static Image<Gray, byte>? LoadTemplate(string path)
{
try
{
// 使用任意格式读取(支持灰度、彩色、16位等)
using var raw = CvInvoke.Imread(path, ImreadModes.AnyDepth | ImreadModes.AnyColor);
if (raw.IsEmpty)
return null;
// 创建灰度图像
var templ = new Image<Gray, byte>(raw.Width, raw.Height);
// 根据通道数决定是否需要灰度转换
if (raw.NumberOfChannels == 1)
{
// 已经是灰度图,直接复制
raw.CopyTo(templ.Mat);
}
else
{
// 彩色图转灰度 (BGR -> Gray)
CvInvoke.CvtColor(raw, templ, ColorConversion.Bgr2Gray);
}
return templ;
}
catch (Exception ex)
{
_logger.Error(ex, "TemplateMatching: failed to load template {Path}", path);
return null;
}
}
}