删除简单的opencv模板匹配算子,改为使用更高级一点的可旋转匹配算子(C++)
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
// ============================================================================
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// 旋转多目标模板匹配(定位识别),基于 TemplateMatchLib 原生库。
|
||||
/// </summary>
|
||||
public class RotatedTemplateMatchingProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<RotatedTemplateMatchingProcessor>();
|
||||
|
||||
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<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
var path = (GetParameter<string>("TemplatePath") ?? string.Empty).Trim();
|
||||
var modelPath = (GetParameter<string>("ModelPath") ?? string.Empty).Trim();
|
||||
var threshold = GetParameter<double>("MatchThreshold");
|
||||
var maxCount = GetParameter<int>("MaxMatchCount");
|
||||
var toleranceAngle = GetParameter<double>("ToleranceAngle");
|
||||
var maxOverlap = GetParameter<double>("MaxOverlap");
|
||||
var minReduceArea = GetParameter<int>("MinReduceArea");
|
||||
var useSIMD = GetParameter<bool>("UseSIMD");
|
||||
var useSubPixel = GetParameter<bool>("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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// ============================================================================
|
||||
// TemplateMatchNative.cs
|
||||
// C++ DLL P/Invoke 封装层
|
||||
// 提供对 TemplateMatchLib.dll 的托管调用接口
|
||||
// ============================================================================
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace XP.ImageProcessing.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// 匹配参数(与C++ TM_Params对应)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TM_Params
|
||||
{
|
||||
/// <summary>匹配阈值 (0~1)</summary>
|
||||
public double Score;
|
||||
/// <summary>角度容差 (度),0表示不旋转</summary>
|
||||
public double ToleranceAngle;
|
||||
/// <summary>最大重叠比例 (0~1)</summary>
|
||||
public double MaxOverlap;
|
||||
/// <summary>最大匹配数</summary>
|
||||
public int MaxCount;
|
||||
/// <summary>金字塔最小面积,默认256</summary>
|
||||
public int MinReduceArea;
|
||||
/// <summary>是否使用SIMD加速 (1=是, 0=否)</summary>
|
||||
public int UseSIMD;
|
||||
/// <summary>是否亚像素估计 (1=是, 0=否)</summary>
|
||||
public int UseSubPixel;
|
||||
|
||||
/// <summary>
|
||||
/// 创建默认参数
|
||||
/// </summary>
|
||||
public static TM_Params Default => new TM_Params
|
||||
{
|
||||
Score = 0.75,
|
||||
ToleranceAngle = 0,
|
||||
MaxOverlap = 0.3,
|
||||
MaxCount = 1,
|
||||
MinReduceArea = 256,
|
||||
UseSIMD = 1,
|
||||
UseSubPixel = 0
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个匹配结果(与C++ TM_Result对应)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TM_Result
|
||||
{
|
||||
/// <summary>匹配中心X</summary>
|
||||
public double CenterX;
|
||||
/// <summary>匹配中心Y</summary>
|
||||
public double CenterY;
|
||||
/// <summary>匹配角度 (度)</summary>
|
||||
public double Angle;
|
||||
/// <summary>匹配分数</summary>
|
||||
public double Score;
|
||||
/// <summary>左上角X</summary>
|
||||
public double LtX;
|
||||
/// <summary>左上角Y</summary>
|
||||
public double LtY;
|
||||
/// <summary>右上角X</summary>
|
||||
public double RtX;
|
||||
/// <summary>右上角Y</summary>
|
||||
public double RtY;
|
||||
/// <summary>右下角X</summary>
|
||||
public double RbX;
|
||||
/// <summary>右下角Y</summary>
|
||||
public double RbY;
|
||||
/// <summary>左下角X</summary>
|
||||
public double LbX;
|
||||
/// <summary>左下角Y</summary>
|
||||
public double LbY;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TemplateMatchLib.dll P/Invoke 接口
|
||||
/// </summary>
|
||||
public static class TemplateMatchNative
|
||||
{
|
||||
private const string DllName = "TemplateMatchLib.dll";
|
||||
|
||||
/// <summary>创建匹配器实例</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern IntPtr TM_Create();
|
||||
|
||||
/// <summary>销毁匹配器实例</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void TM_Destroy(IntPtr handle);
|
||||
|
||||
/// <summary>从内存数据学习模板</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern int TM_LearnPattern(IntPtr handle,
|
||||
IntPtr templateData, int width, int height, int step);
|
||||
|
||||
/// <summary>从文件学习模板</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern int TM_LearnPatternFromFile(IntPtr handle,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string filePath);
|
||||
|
||||
/// <summary>执行模板匹配</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern int TM_Match(IntPtr handle,
|
||||
IntPtr srcData, int srcWidth, int srcHeight, int srcStep,
|
||||
ref TM_Params param,
|
||||
[Out] TM_Result[] results, int maxResults);
|
||||
|
||||
/// <summary>获取上次匹配耗时(毫秒)</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern double TM_GetLastMatchTime(IntPtr handle);
|
||||
|
||||
/// <summary>获取模板信息</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern int TM_GetTemplateInfo(IntPtr handle,
|
||||
out int width, out int height, out int pyramidLayers);
|
||||
|
||||
/// <summary>保存训练好的模型到文件</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern int TM_SaveModel(IntPtr handle,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string filePath);
|
||||
|
||||
/// <summary>从文件加载已训练的模型</summary>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern int TM_LoadModel(IntPtr handle,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模板匹配器托管封装(自动管理非托管资源)
|
||||
/// </summary>
|
||||
public sealed class TemplateMatcherHandle : IDisposable
|
||||
{
|
||||
private IntPtr _handle;
|
||||
private bool _disposed;
|
||||
|
||||
public TemplateMatcherHandle()
|
||||
{
|
||||
_handle = TemplateMatchNative.TM_Create();
|
||||
if (_handle == IntPtr.Zero)
|
||||
throw new InvalidOperationException("Failed to create TemplateMatcher instance");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件学习模板
|
||||
/// </summary>
|
||||
public bool LearnPatternFromFile(string filePath)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return TemplateMatchNative.TM_LearnPatternFromFile(_handle, filePath) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从EmguCV Image学习模板
|
||||
/// </summary>
|
||||
public bool LearnPattern(IntPtr data, int width, int height, int step)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return TemplateMatchNative.TM_LearnPattern(_handle, data, width, height, step) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行匹配
|
||||
/// </summary>
|
||||
public TM_Result[] Match(IntPtr srcData, int srcWidth, int srcHeight, int srcStep, TM_Params param)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var results = new TM_Result[param.MaxCount];
|
||||
int count = TemplateMatchNative.TM_Match(_handle, srcData, srcWidth, srcHeight, srcStep,
|
||||
ref param, results, param.MaxCount);
|
||||
|
||||
if (count <= 0)
|
||||
return Array.Empty<TM_Result>();
|
||||
|
||||
if (count < results.Length)
|
||||
Array.Resize(ref results, count);
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取上次匹配耗时(毫秒)
|
||||
/// </summary>
|
||||
public double LastMatchTime
|
||||
{
|
||||
get
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return TemplateMatchNative.TM_GetLastMatchTime(_handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取模板信息
|
||||
/// </summary>
|
||||
public bool GetTemplateInfo(out int width, out int height, out int pyramidLayers)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return TemplateMatchNative.TM_GetTemplateInfo(_handle, out width, out height, out pyramidLayers) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存训练好的模型到文件
|
||||
/// </summary>
|
||||
/// <param name="filePath">模型文件路径(建议扩展名 .tmmodel)</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public bool SaveModel(string filePath)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return TemplateMatchNative.TM_SaveModel(_handle, filePath) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载已训练的模型(跳过LearnPattern)
|
||||
/// </summary>
|
||||
/// <param name="filePath">模型文件路径</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public bool LoadModel(string filePath)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return TemplateMatchNative.TM_LoadModel(_handle, filePath) == 0;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(TemplateMatcherHandle));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
TemplateMatchNative.TM_Destroy(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
~TemplateMatcherHandle()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
// ============================================================================
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user