e0eec42a2f
- 新增模板助手/批量测试窗口、事件与 ViewModel,主窗口入口与 App 注册 - 运行匹配或批量测试前发布清除事件;视口通过 VisualTreeHelper 从父 Panel 移除持久虚线 ROI,避免 FindChild 失败时框残留 - TemplateMatchNative 等相关调整 Co-authored-by: Cursor <cursoragent@cursor.com>
280 lines
9.1 KiB
C#
280 lines
9.1 KiB
C#
// ============================================================================
|
||
// 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();
|
||
}
|
||
}
|