// ============================================================================
// 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;
///
/// 模板匹配算子(定位识别)
///
public class TemplateMatchingProcessor : ImageProcessorBase
{
private static readonly ILogger _logger = Log.ForContext();
private static readonly string[] MatchMethodOptions =
{
"CcoeffNormed",
"SqdiffNormed",
"CcorrNormed",
"Ccoeff",
"Ccorr",
"Sqdiff"
};
public TemplateMatchingProcessor()
{
Name = LocalizationHelper.GetString("TemplateMatchingProcessor_Name");
Description = LocalizationHelper.GetString("TemplateMatchingProcessor_Description");
}
protected override void InitializeParameters()
{
Parameters.Add("TemplatePath", new ProcessorParameter(
"TemplatePath",
LocalizationHelper.GetString("TemplateMatchingProcessor_TemplatePath"),
typeof(string),
string.Empty,
null,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_TemplatePath_Desc")));
Parameters.Add("MatchMethod", new ProcessorParameter(
"MatchMethod",
LocalizationHelper.GetString("TemplateMatchingProcessor_MatchMethod"),
typeof(string),
"CcoeffNormed",
null,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_MatchMethod_Desc"),
MatchMethodOptions));
Parameters.Add("MatchThreshold", new ProcessorParameter(
"MatchThreshold",
LocalizationHelper.GetString("TemplateMatchingProcessor_MatchThreshold"),
typeof(double),
0.75,
0.0,
1.0,
LocalizationHelper.GetString("TemplateMatchingProcessor_MatchThreshold_Desc")));
Parameters.Add("DrawRectangle", new ProcessorParameter(
"DrawRectangle",
LocalizationHelper.GetString("TemplateMatchingProcessor_DrawMatch"),
typeof(bool),
true,
null,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_DrawMatch_Desc")));
Parameters.Add("RectangleThickness", new ProcessorParameter(
"RectangleThickness",
LocalizationHelper.GetString("TemplateMatchingProcessor_RectThickness"),
typeof(int),
2,
1,
8,
LocalizationHelper.GetString("TemplateMatchingProcessor_RectThickness_Desc")));
Parameters.Add("SearchRegionX", new ProcessorParameter(
"SearchRegionX",
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionX"),
typeof(int),
0,
0,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc")));
Parameters.Add("SearchRegionY", new ProcessorParameter(
"SearchRegionY",
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionY"),
typeof(int),
0,
0,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc")));
Parameters.Add("SearchRegionWidth", new ProcessorParameter(
"SearchRegionWidth",
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionWidth"),
typeof(int),
0,
0,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc")));
Parameters.Add("SearchRegionHeight", new ProcessorParameter(
"SearchRegionHeight",
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionHeight"),
typeof(int),
0,
0,
null,
LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc")));
_logger.Debug("InitializeParameters");
}
public override Image Process(Image inputImage)
{
var path = (GetParameter("TemplatePath") ?? string.Empty).Trim();
var methodName = GetParameter("MatchMethod") ?? "CcoeffNormed";
var threshold = GetParameter("MatchThreshold");
var draw = GetParameter("DrawRectangle");
var thickness = GetParameter("RectangleThickness");
var searchRx = GetParameter("SearchRegionX");
var searchRy = GetParameter("SearchRegionY");
var searchRw = GetParameter("SearchRegionWidth");
var searchRh = GetParameter("SearchRegionHeight");
OutputData.Clear();
var output = inputImage.Clone();
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;
}
using var template = LoadTemplate(path);
if (template == null)
{
OutputData["Matched"] = false;
OutputData["Message"] = "Template load failed";
return output;
}
var searchRoi = ResolveSearchRoi(inputImage.Width, inputImage.Height, searchRx, searchRy, searchRw, searchRh);
var offsetX = searchRoi.X;
var offsetY = searchRoi.Y;
Image? roiImage = null;
try
{
inputImage.ROI = searchRoi;
roiImage = inputImage.Copy();
inputImage.ROI = Rectangle.Empty;
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;
}
var method = ParseMethod(methodName);
using var resultMat = new Mat();
CvInvoke.MatchTemplate(roiImage, template, resultMat, method);
double minVal = 0, maxVal = 0;
Point minLoc = default, maxLoc = default;
CvInvoke.MinMaxLoc(resultMat, ref minVal, ref maxVal, ref minLoc, ref maxLoc);
var useMin = method == TemplateMatchingType.Sqdiff || method == TemplateMatchingType.SqdiffNormed;
var loc = useMin ? minLoc : maxLoc;
var score = useMin ? minVal : maxVal;
var matched = IsMatchAcceptable(method, minVal, maxVal, threshold);
var globalLoc = new Point(loc.X + offsetX, loc.Y + offsetY);
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;
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
{
roiImage?.Dispose();
}
}
private static Rectangle ResolveSearchRoi(int imgW, int imgH, int rx, int ry, int rw, int rh)
{
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);
}
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
};
}
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
};
}
private static Image? LoadTemplate(string path)
{
try
{
using var raw = CvInvoke.Imread(path, ImreadModes.AnyDepth | ImreadModes.AnyColor);
if (raw.IsEmpty)
return null;
var templ = new Image(raw.Width, raw.Height);
if (raw.NumberOfChannels == 1)
raw.CopyTo(templ.Mat);
else
CvInvoke.CvtColor(raw, templ, ColorConversion.Bgr2Gray);
return templ;
}
catch (Exception ex)
{
_logger.Error(ex, "TemplateMatching: failed to load template {Path}", path);
return null;
}
}
}