poe2-bot/src/Automata.Navigation/IconDetector.cs
2026-02-28 15:13:22 -05:00

128 lines
4.3 KiB
C#

using OpenCvSharp;
using Serilog;
namespace Poe2Trade.Navigation;
/// <summary>
/// Detects minimap icons (doors, checkpoints) via template matching.
/// Loads RGBA templates once, converts to grayscale for matching.
/// </summary>
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;
}
/// <summary>
/// Detect all door icons in the frame. Returns center points in frame coords.
/// </summary>
public List<Point> DetectDoors(Mat grayFrame)
=> Detect(grayFrame, _doorTemplate, DoorThreshold);
/// <summary>
/// Detect all inactive checkpoint icons. Returns center points in frame coords.
/// </summary>
public List<Point> DetectCheckpointsOff(Mat grayFrame)
=> Detect(grayFrame, _checkpointOffTemplate, CheckpointThreshold);
/// <summary>
/// Detect all active checkpoint icons. Returns center points in frame coords.
/// </summary>
public List<Point> DetectCheckpointsOn(Mat grayFrame)
=> Detect(grayFrame, _checkpointOnTemplate, CheckpointThreshold);
/// <summary>
/// Greedy multi-match: find best match, record center, zero out neighborhood, repeat.
/// </summary>
private static List<Point> Detect(Mat grayFrame, Mat template, double threshold)
{
var results = new List<Point>();
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;
}
/// <summary>
/// Get the template size for doors (used to stamp wall regions).
/// </summary>
public Size DoorSize => new(_doorTemplate.Width, _doorTemplate.Height);
public void Dispose()
{
_doorTemplate.Dispose();
_checkpointOffTemplate.Dispose();
_checkpointOnTemplate.Dispose();
}
}