// ============================================================================ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件名: RotatedTemplateMatchingProcessor.cs // 描述: 旋转多目标模板匹配(金字塔、SIMD、亚像素) // 功能: // - 调用 TemplateMatchLib.dll 实现高性能旋转模板匹配 // - 支持多目标检测和重叠过滤 // - 支持图像金字塔加速 // - 支持 SIMD 加速和亚像素精度 // - 输出匹配结果(中心坐标、角度、分数、四角坐标) // ============================================================================ using System; using Emgu.CV; using Emgu.CV.Structure; using XP.ImageProcessing.Core; using Serilog; namespace XP.ImageProcessing.Processors; /// /// 旋转多目标模板匹配(定位识别),基于 TemplateMatchLib 原生库。 /// public class RotatedTemplateMatchingProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); public RotatedTemplateMatchingProcessor() { Name = LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_Name"); Description = LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_Description"); } protected override void InitializeParameters() { Parameters.Add("TemplatePath", new ProcessorParameter( "TemplatePath", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_TemplatePath"), typeof(string), string.Empty, null, null, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_TemplatePath_Desc"))); Parameters.Add("MatchThreshold", new ProcessorParameter( "MatchThreshold", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_MatchThreshold"), typeof(double), 0.75, 0.0, 1.0, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_MatchThreshold_Desc"))); Parameters.Add("MaxMatchCount", new ProcessorParameter( "MaxMatchCount", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_MaxMatchCount"), typeof(int), 1, 1, 100, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_MaxMatchCount_Desc"))); Parameters.Add("ToleranceAngle", new ProcessorParameter( "ToleranceAngle", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_ToleranceAngle"), typeof(double), 0.0, 0.0, 180.0, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_ToleranceAngle_Desc"))); Parameters.Add("MaxOverlap", new ProcessorParameter( "MaxOverlap", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_MaxOverlap"), typeof(double), 0.3, 0.0, 1.0, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_MaxOverlap_Desc"))); Parameters.Add("MinReduceArea", new ProcessorParameter( "MinReduceArea", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_MinReduceArea"), typeof(int), 256, 64, 4096, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_MinReduceArea_Desc"))); Parameters.Add("UseSIMD", new ProcessorParameter( "UseSIMD", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_UseSIMD"), typeof(bool), true, null, null, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_UseSIMD_Desc"))); Parameters.Add("UseSubPixel", new ProcessorParameter( "UseSubPixel", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_UseSubPixel"), typeof(bool), false, null, null, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_UseSubPixel_Desc"))); Parameters.Add("DrawResults", new ProcessorParameter( "DrawResults", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_DrawResults"), typeof(bool), true, null, null, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_DrawResults_Desc"))); Parameters.Add("DrawThickness", new ProcessorParameter( "DrawThickness", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_DrawThickness"), typeof(int), 1, 1, 8, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_DrawThickness_Desc"))); Parameters.Add("ModelPath", new ProcessorParameter( "ModelPath", LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_ModelPath"), typeof(string), string.Empty, null, null, LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_ModelPath_Desc"))); } public override Image Process(Image inputImage) { var path = (GetParameter("TemplatePath") ?? string.Empty).Trim(); var modelPath = (GetParameter("ModelPath") ?? string.Empty).Trim(); var threshold = GetParameter("MatchThreshold"); var maxCount = GetParameter("MaxMatchCount"); var toleranceAngle = GetParameter("ToleranceAngle"); var maxOverlap = GetParameter("MaxOverlap"); var minReduceArea = GetParameter("MinReduceArea"); var useSIMD = GetParameter("UseSIMD"); var useSubPixel = GetParameter("UseSubPixel"); OutputData.Clear(); var output = inputImage.Clone(); // 模板路径和模型路径都为空时报错 bool hasModel = !string.IsNullOrEmpty(modelPath) && System.IO.File.Exists(modelPath); bool hasTemplate = !string.IsNullOrEmpty(path) && System.IO.File.Exists(path); if (!hasModel && !hasTemplate) { _logger.Warning("RotatedTemplateMatching: no template or model file found"); OutputData["Matched"] = false; OutputData["MatchCount"] = 0; OutputData["Message"] = LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_Msg_TemplateNotFound"); return output; } try { using var matcher = new TemplateMatcherHandle(); // 优先加载模型文件,否则从模板图片学习 bool modelLoaded = false; if (hasModel) { modelLoaded = matcher.LoadModel(modelPath); if (modelLoaded) _logger.Debug("RotatedTemplateMatching: loaded model from {Path}", modelPath); } if (!modelLoaded) { if (!hasTemplate) { OutputData["Matched"] = false; OutputData["MatchCount"] = 0; OutputData["Message"] = LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_Msg_TemplateNotFound"); return output; } if (!matcher.LearnPatternFromFile(path)) { OutputData["Matched"] = false; OutputData["MatchCount"] = 0; OutputData["Message"] = LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_Msg_TemplateLearnFailed"); return output; } // 学习成功后自动保存模型 if (!string.IsNullOrEmpty(modelPath)) { var dir = System.IO.Path.GetDirectoryName(modelPath); if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir); if (matcher.SaveModel(modelPath)) _logger.Information("RotatedTemplateMatching: model saved to {Path}", modelPath); else _logger.Warning("RotatedTemplateMatching: failed to save model to {Path}", modelPath); } } var param = new TM_Params { Score = threshold, ToleranceAngle = toleranceAngle, MaxOverlap = maxOverlap, MaxCount = maxCount, MinReduceArea = minReduceArea, UseSIMD = useSIMD ? 1 : 0, UseSubPixel = useSubPixel ? 1 : 0 }; IntPtr srcData = inputImage.Mat.DataPointer; int srcWidth = inputImage.Width; int srcHeight = inputImage.Height; int srcStep = (int)inputImage.Mat.Step; var results = matcher.Match(srcData, srcWidth, srcHeight, srcStep, param); OutputData["Matched"] = results.Length > 0; OutputData["MatchCount"] = results.Length; OutputData["MatchTime"] = matcher.LastMatchTime; for (int i = 0; i < results.Length; i++) { var r = results[i]; string prefix = results.Length == 1 ? "" : $"[{i}]"; OutputData[$"CenterX{prefix}"] = r.CenterX; OutputData[$"CenterY{prefix}"] = r.CenterY; OutputData[$"Angle{prefix}"] = r.Angle; OutputData[$"Score{prefix}"] = r.Score; OutputData[$"LtX{prefix}"] = r.LtX; OutputData[$"LtY{prefix}"] = r.LtY; OutputData[$"RtX{prefix}"] = r.RtX; OutputData[$"RtY{prefix}"] = r.RtY; OutputData[$"RbX{prefix}"] = r.RbX; OutputData[$"RbY{prefix}"] = r.RbY; OutputData[$"LbX{prefix}"] = r.LbX; OutputData[$"LbY{prefix}"] = r.LbY; } _logger.Debug("RotatedTemplateMatching: Found {Count} matches in {Time:F1}ms", results.Length, matcher.LastMatchTime); return output; } catch (DllNotFoundException ex) { _logger.Error(ex, "RotatedTemplateMatching: TemplateMatchLib.dll not found"); OutputData["Matched"] = false; OutputData["MatchCount"] = 0; OutputData["Message"] = LocalizationHelper.GetString("RotatedTemplateMatchingProcessor_Msg_DllNotFound"); return output; } catch (Exception ex) { _logger.Error(ex, "RotatedTemplateMatching: unexpected error"); OutputData["Matched"] = false; OutputData["MatchCount"] = 0; OutputData["Message"] = ex.Message; return output; } } }