using OpenCvSharp; using System.Drawing.Imaging; namespace anjianjingling.Vision { /// /// 找图:OpenCV 模板匹配(TM_CCOEFF_NORMED),支持相似度阈值。 /// public sealed class ImageFinder : IDisposable { private readonly Dictionary _templateCache = new(StringComparer.OrdinalIgnoreCase); private static Mat BitmapToMat(Bitmap bmp) { using var ms = new MemoryStream(); bmp.Save(ms, ImageFormat.Png); return Cv2.ImDecode(ms.ToArray(), ImreadModes.Color); } private Mat LoadTemplate(string path) { if (_templateCache.TryGetValue(path, out var cached)) return cached; if (!File.Exists(path)) throw new FileNotFoundException($"模板图片不存在:{path}"); var mat = Cv2.ImRead(path, ImreadModes.Color); if (mat.Empty()) throw new InvalidDataException($"模板图片无法读取:{path}"); _templateCache[path] = mat; return mat; } /// /// 在 haystack 位图中查找模板。 /// /// 被查找的画面(通常是区域截图) /// 模板图路径 /// 相似度阈值 0~1 /// 结果坐标要加上的屏幕偏移 X /// 结果坐标要加上的屏幕偏移 Y /// 匹配中心屏幕坐标与相似度;未达阈值返回 (-1,-1,score) public (int X, int Y, double Score) Find(Bitmap haystack, string templatePath, double threshold, int offsetX = 0, int offsetY = 0) { using var hay = BitmapToMat(haystack); using var tpl = LoadTemplate(templatePath); if (tpl.Width > hay.Width || tpl.Height > hay.Height) throw new ArgumentException($"模板图({tpl.Width}x{tpl.Height})大于查找区域({hay.Width}x{hay.Height})"); using var result = new Mat(); Cv2.MatchTemplate(hay, tpl, result, TemplateMatchModes.CCoeffNormed); Cv2.MinMaxLoc(result, out _, out double maxVal, out _, out OpenCvSharp.Point maxLoc); if (maxVal >= threshold) { int cx = offsetX + maxLoc.X + tpl.Width / 2; int cy = offsetY + maxLoc.Y + tpl.Height / 2; return (cx, cy, maxVal); } return (-1, -1, maxVal); } /// 清除模板缓存(模板文件更新后调用)。 public void ClearCache() { foreach (var m in _templateCache.Values) m.Dispose(); _templateCache.Clear(); } public void Dispose() { ClearCache(); } } }