283 lines
11 KiB
C#
283 lines
11 KiB
C#
// ============================================================================
|
|
// 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>
|
|
public class TemplateMatchingProcessor : ImageProcessorBase
|
|
{
|
|
private static readonly ILogger _logger = Log.ForContext<TemplateMatchingProcessor>();
|
|
|
|
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<Gray, byte> Process(Image<Gray, byte> inputImage)
|
|
{
|
|
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();
|
|
|
|
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<Gray, byte>? 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<Gray, byte>? LoadTemplate(string path)
|
|
{
|
|
try
|
|
{
|
|
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
|
|
CvInvoke.CvtColor(raw, templ, ColorConversion.Bgr2Gray);
|
|
|
|
return templ;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "TemplateMatching: failed to load template {Path}", path);
|
|
return null;
|
|
}
|
|
}
|
|
}
|