272 lines
11 KiB
C#
272 lines
11 KiB
C#
// ============================================================================
|
|
// 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;
|
|
}
|
|
}
|
|
}
|