using OpenCvSharp; using Serilog; namespace Poe2Trade.Navigation; /// /// Detects minimap icons (doors, checkpoints) via template matching. /// Loads RGBA templates once, converts to grayscale for matching. /// internal class IconDetector : IDisposable { private readonly Mat _doorTemplate; private readonly Mat _checkpointOffTemplate; private readonly Mat _checkpointOnTemplate; private const double DoorThreshold = 0.65; private const double CheckpointThreshold = 0.75; public IconDetector(string assetsDir) { _doorTemplate = LoadGray(Path.Combine(assetsDir, "door.png")); _checkpointOffTemplate = LoadGray(Path.Combine(assetsDir, "checkpoint-off.png")); _checkpointOnTemplate = LoadGray(Path.Combine(assetsDir, "checkpoint-on.png")); Log.Information("IconDetector loaded: door={DW}x{DH} cpOff={OW}x{OH} cpOn={NW}x{NH}", _doorTemplate.Width, _doorTemplate.Height, _checkpointOffTemplate.Width, _checkpointOffTemplate.Height, _checkpointOnTemplate.Width, _checkpointOnTemplate.Height); } private static Mat LoadGray(string path) { var bgra = Cv2.ImRead(path, ImreadModes.Unchanged); if (bgra.Empty()) throw new FileNotFoundException($"Icon template not found: {path}"); var gray = new Mat(); if (bgra.Channels() == 4) { using var bgr = new Mat(); Cv2.CvtColor(bgra, bgr, ColorConversionCodes.BGRA2BGR); Cv2.CvtColor(bgr, gray, ColorConversionCodes.BGR2GRAY); } else if (bgra.Channels() == 3) { Cv2.CvtColor(bgra, gray, ColorConversionCodes.BGR2GRAY); } else { gray = bgra.Clone(); } bgra.Dispose(); return gray; } /// /// Detect all door icons in the frame. Returns center points in frame coords. /// public List DetectDoors(Mat grayFrame) => Detect(grayFrame, _doorTemplate, DoorThreshold); /// /// Detect all inactive checkpoint icons. Returns center points in frame coords. /// public List DetectCheckpointsOff(Mat grayFrame) => Detect(grayFrame, _checkpointOffTemplate, CheckpointThreshold); /// /// Detect all active checkpoint icons. Returns center points in frame coords. /// public List DetectCheckpointsOn(Mat grayFrame) => Detect(grayFrame, _checkpointOnTemplate, CheckpointThreshold); /// /// Greedy multi-match: find best match, record center, zero out neighborhood, repeat. /// private static List Detect(Mat grayFrame, Mat template, double threshold) { var results = new List(); if (grayFrame.Width < template.Width || grayFrame.Height < template.Height) return results; using var result = new Mat(); Cv2.MatchTemplate(grayFrame, template, result, TemplateMatchModes.CCoeffNormed); var tw = template.Width; var th = template.Height; while (true) { Cv2.MinMaxLoc(result, out _, out var maxVal, out _, out var maxLoc); if (maxVal < threshold) break; // Record center point of the matched region var centerX = maxLoc.X + tw / 2; var centerY = maxLoc.Y + th / 2; results.Add(new Point(centerX, centerY)); // Zero out neighborhood to prevent re-detection var zeroX = Math.Max(0, maxLoc.X - tw / 2); var zeroY = Math.Max(0, maxLoc.Y - th / 2); var zeroW = Math.Min(tw * 2, result.Width - zeroX); var zeroH = Math.Min(th * 2, result.Height - zeroY); if (zeroW > 0 && zeroH > 0) { using var roi = new Mat(result, new Rect(zeroX, zeroY, zeroW, zeroH)); roi.SetTo(Scalar.Black); } } return results; } /// /// Get the template size for doors (used to stamp wall regions). /// public Size DoorSize => new(_doorTemplate.Width, _doorTemplate.Height); public void Dispose() { _doorTemplate.Dispose(); _checkpointOffTemplate.Dispose(); _checkpointOnTemplate.Dispose(); } }