Files
XplorePlane/XP.ImageProcessing.Processors/定位识别/TemplateMatchNative.cs
T
李伟 e0eec42a2f feat: 模板匹配助手窗口与主视口 ROI 清除逻辑
- 新增模板助手/批量测试窗口、事件与 ViewModel,主窗口入口与 App 注册

- 运行匹配或批量测试前发布清除事件;视口通过 VisualTreeHelper 从父 Panel 移除持久虚线 ROI,避免 FindChild 失败时框残留

- TemplateMatchNative 等相关调整

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 10:42:20 +08:00

280 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// 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>
/// 开启亚像素且角度容差绝对值超过该值时,托管封装会在调用原生库前关闭亚像素,
/// 以避免部分版本 TemplateMatchLib 在 Debug 下出现 vector 越界断言。
/// </summary>
public const double SubPixelAngleSafetyLimitDegrees = 90.0;
/// <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();
// 与库默认一致并对齐已知崩溃组合:Debug 下亚像素 + 大角度容差易触发 vector 越界断言;
// 金字塔最小面积过小也可能与内部层级假设不一致。
int tw = 0, th = 0, _pyramidLayers = 0;
_ = GetTemplateInfo(out tw, out th, out _pyramidLayers);
int templatePixels = Math.Max(0, tw) * Math.Max(0, th);
int maxCount = Math.Clamp(param.MaxCount, 1, 100);
int minReduce = (int)Math.Clamp(param.MinReduceArea, 64, 4096);
if (templatePixels >= 512)
minReduce = Math.Max(256, minReduce);
if (templatePixels > 0)
minReduce = Math.Min(minReduce, templatePixels);
minReduce = Math.Max(64, minReduce);
int useSubPixel = param.UseSubPixel;
if (useSubPixel != 0 && Math.Abs(param.ToleranceAngle) > TM_Params.SubPixelAngleSafetyLimitDegrees)
useSubPixel = 0;
var p = param;
p.MaxCount = maxCount;
p.MinReduceArea = minReduce;
p.UseSubPixel = useSubPixel;
var results = new TM_Result[p.MaxCount];
int count = TemplateMatchNative.TM_Match(_handle, srcData, srcWidth, srcHeight, srcStep,
ref p, results, p.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();
}
}