176 lines
6.2 KiB
C#
176 lines
6.2 KiB
C#
using OpenCvSharp;
|
|
using Serilog;
|
|
using Region = Poe2Trade.Core.Region;
|
|
using Point = OpenCvSharp.Point;
|
|
using Size = OpenCvSharp.Size;
|
|
|
|
namespace Poe2Trade.Navigation;
|
|
|
|
public class MinimapCapture : IDisposable
|
|
{
|
|
private readonly MinimapConfig _config;
|
|
private readonly IScreenCapture _backend;
|
|
private Mat? _circularMask;
|
|
|
|
public MinimapCapture(MinimapConfig config)
|
|
{
|
|
_config = config;
|
|
_backend = CreateBackend();
|
|
BuildCircularMask();
|
|
}
|
|
|
|
private static IScreenCapture CreateBackend()
|
|
{
|
|
// WGC primary → DXGI fallback → GDI last resort
|
|
try
|
|
{
|
|
var wgc = new WgcCapture();
|
|
Log.Information("Screen capture: WGC (Windows Graphics Capture)");
|
|
return wgc;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "WGC unavailable, trying DXGI Desktop Duplication");
|
|
}
|
|
|
|
try
|
|
{
|
|
var dxgi = new DesktopDuplication();
|
|
Log.Information("Screen capture: DXGI Desktop Duplication");
|
|
return dxgi;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "DXGI unavailable, falling back to GDI");
|
|
}
|
|
|
|
Log.Information("Screen capture: GDI (CopyFromScreen)");
|
|
return new GdiCapture();
|
|
}
|
|
|
|
private void BuildCircularMask()
|
|
{
|
|
var size = _config.CaptureSize;
|
|
_circularMask = new Mat(size, size, MatType.CV_8UC1, Scalar.Black);
|
|
var center = new Point(size / 2, size / 2);
|
|
Cv2.Circle(_circularMask, center, _config.FogRadius, Scalar.White, -1);
|
|
}
|
|
|
|
public MinimapFrame? CaptureFrame()
|
|
{
|
|
var region = _config.CaptureRegion;
|
|
using var bgr = _backend.CaptureRegion(region);
|
|
|
|
if (bgr == null || bgr.Empty())
|
|
return null;
|
|
|
|
// Apply circular mask to ignore area outside fog-of-war circle
|
|
using var masked = new Mat();
|
|
Cv2.BitwiseAnd(bgr, bgr, masked, _circularMask!);
|
|
|
|
// Convert to HSV
|
|
using var hsv = new Mat();
|
|
Cv2.CvtColor(masked, hsv, ColorConversionCodes.BGR2HSV);
|
|
|
|
// Classify explored areas
|
|
using var exploredMask = new Mat();
|
|
Cv2.InRange(hsv, _config.ExploredLoHSV, _config.ExploredHiHSV, exploredMask);
|
|
|
|
// Classify walls: dark pixels within the circular mask
|
|
using var valueChan = new Mat();
|
|
Cv2.ExtractChannel(hsv, valueChan, 2); // V channel
|
|
using var darkMask = new Mat();
|
|
Cv2.Threshold(valueChan, darkMask, _config.WallMaxValue, 255, ThresholdTypes.BinaryInv);
|
|
// Apply morphological close to connect wall fragments
|
|
using var wallKernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
|
|
using var wallMask = new Mat();
|
|
Cv2.MorphologyEx(darkMask, wallMask, MorphTypes.Close, wallKernel);
|
|
// Only within circular mask
|
|
Cv2.BitwiseAnd(wallMask, _circularMask!, wallMask);
|
|
// Don't count explored pixels as wall
|
|
using var notExplored = new Mat();
|
|
Cv2.BitwiseNot(exploredMask, notExplored);
|
|
Cv2.BitwiseAnd(wallMask, notExplored, wallMask);
|
|
|
|
// Build classified mat: Unknown=0, Explored=1, Wall=2
|
|
var classified = new Mat(_config.CaptureSize, _config.CaptureSize, MatType.CV_8UC1, Scalar.Black);
|
|
classified.SetTo(new Scalar((byte)MapCell.Explored), exploredMask);
|
|
classified.SetTo(new Scalar((byte)MapCell.Wall), wallMask);
|
|
// Ensure only within circular mask
|
|
Cv2.BitwiseAnd(classified, _circularMask!, classified);
|
|
|
|
// Detect player marker (orange X)
|
|
using var playerMask = new Mat();
|
|
Cv2.InRange(hsv, _config.PlayerLoHSV, _config.PlayerHiHSV, playerMask);
|
|
var playerOffset = FindCentroid(playerMask);
|
|
|
|
// Convert to grayscale for phase correlation
|
|
var gray = new Mat();
|
|
Cv2.CvtColor(masked, gray, ColorConversionCodes.BGR2GRAY);
|
|
// Apply circular mask to gray too
|
|
Cv2.BitwiseAnd(gray, _circularMask!, gray);
|
|
|
|
return new MinimapFrame(
|
|
GrayMat: gray,
|
|
ClassifiedMat: classified,
|
|
PlayerOffset: playerOffset,
|
|
Timestamp: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save debug images: raw capture, HSV mask, classified result.
|
|
/// Call once to diagnose color ranges.
|
|
/// </summary>
|
|
public void SaveDebugCapture(string dir = "debug-minimap")
|
|
{
|
|
Directory.CreateDirectory(dir);
|
|
var region = _config.CaptureRegion;
|
|
using var bgr = _backend.CaptureRegion(region);
|
|
if (bgr == null || bgr.Empty()) return;
|
|
|
|
using var masked = new Mat();
|
|
Cv2.BitwiseAnd(bgr, bgr, masked, _circularMask!);
|
|
|
|
using var hsv = new Mat();
|
|
Cv2.CvtColor(masked, hsv, ColorConversionCodes.BGR2HSV);
|
|
|
|
using var exploredMask = new Mat();
|
|
Cv2.InRange(hsv, _config.ExploredLoHSV, _config.ExploredHiHSV, exploredMask);
|
|
|
|
using var playerMask = new Mat();
|
|
Cv2.InRange(hsv, _config.PlayerLoHSV, _config.PlayerHiHSV, playerMask);
|
|
|
|
// Save raw, masked, explored filter, player filter
|
|
Cv2.ImWrite(Path.Combine(dir, "1-raw.png"), bgr);
|
|
Cv2.ImWrite(Path.Combine(dir, "2-masked.png"), masked);
|
|
Cv2.ImWrite(Path.Combine(dir, "3-explored.png"), exploredMask);
|
|
Cv2.ImWrite(Path.Combine(dir, "4-player.png"), playerMask);
|
|
|
|
// Save HSV channels separately for tuning
|
|
var channels = Cv2.Split(hsv);
|
|
Cv2.ImWrite(Path.Combine(dir, "5-hue.png"), channels[0]);
|
|
Cv2.ImWrite(Path.Combine(dir, "6-sat.png"), channels[1]);
|
|
Cv2.ImWrite(Path.Combine(dir, "7-val.png"), channels[2]);
|
|
foreach (var c in channels) c.Dispose();
|
|
|
|
Log.Information("Debug minimap images saved to {Dir}", Path.GetFullPath(dir));
|
|
}
|
|
|
|
private Point2d FindCentroid(Mat mask)
|
|
{
|
|
var moments = Cv2.Moments(mask, true);
|
|
if (moments.M00 < 10) // not enough pixels
|
|
return new Point2d(0, 0);
|
|
|
|
var cx = moments.M10 / moments.M00 - _config.CaptureSize / 2.0;
|
|
var cy = moments.M01 / moments.M00 - _config.CaptureSize / 2.0;
|
|
return new Point2d(cx, cy);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_circularMask?.Dispose();
|
|
_backend.Dispose();
|
|
}
|
|
}
|