// ============================================================================ // 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; } } }