full capture
This commit is contained in:
parent
d71c1d97c5
commit
6ea373f2c3
10 changed files with 291 additions and 173 deletions
|
|
@ -43,7 +43,7 @@ public sealed class DesktopDuplication : IScreenCapture
|
|||
Log.Debug("DXGI Desktop Duplication created");
|
||||
}
|
||||
|
||||
public unsafe Mat? CaptureRegion(Region region)
|
||||
public unsafe ScreenFrame? CaptureFrame()
|
||||
{
|
||||
if (_duplication == null) return null;
|
||||
|
||||
|
|
@ -70,52 +70,36 @@ public sealed class DesktopDuplication : IScreenCapture
|
|||
{
|
||||
if (ex.ResultCode == Vortice.DXGI.ResultCode.AccessLost)
|
||||
_needsRecreate = true;
|
||||
// WaitTimeout is normal when screen hasn't changed
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var srcTexture = resource!.QueryInterface<ID3D11Texture2D>();
|
||||
EnsureStaging(region.Width, region.Height);
|
||||
var srcTexture = resource!.QueryInterface<ID3D11Texture2D>();
|
||||
var desc = srcTexture.Description;
|
||||
var w = (int)desc.Width;
|
||||
var h = (int)desc.Height;
|
||||
EnsureStaging(w, h);
|
||||
|
||||
// Copy only the region we need from the desktop texture
|
||||
_context.CopySubresourceRegion(
|
||||
_staging!, 0, 0, 0, 0,
|
||||
srcTexture, 0,
|
||||
new Vortice.Mathematics.Box(region.X, region.Y, 0,
|
||||
region.X + region.Width, region.Y + region.Height, 1));
|
||||
_context.CopySubresourceRegion(_staging!, 0, 0, 0, 0, srcTexture, 0);
|
||||
|
||||
var mapped = _context.Map(_staging!, 0, MapMode.Read);
|
||||
try
|
||||
{
|
||||
var mat = new Mat(region.Height, region.Width, MatType.CV_8UC4);
|
||||
var rowBytes = region.Width * 4;
|
||||
|
||||
for (var row = 0; row < region.Height; row++)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
(void*)(mapped.DataPointer + row * mapped.RowPitch),
|
||||
(void*)mat.Ptr(row),
|
||||
rowBytes, rowBytes);
|
||||
}
|
||||
var mat = Mat.FromPixelData(h, w, MatType.CV_8UC4, mapped.DataPointer, (int)mapped.RowPitch);
|
||||
|
||||
// BGRA → BGR
|
||||
var bgr = new Mat();
|
||||
Cv2.CvtColor(mat, bgr, ColorConversionCodes.BGRA2BGR);
|
||||
mat.Dispose();
|
||||
return bgr;
|
||||
}
|
||||
finally
|
||||
var duplication = _duplication!;
|
||||
return new ScreenFrame(mat, () =>
|
||||
{
|
||||
_context.Unmap(_staging!, 0);
|
||||
srcTexture.Dispose();
|
||||
resource.Dispose();
|
||||
duplication.ReleaseFrame();
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
public unsafe Mat? CaptureRegion(Region region)
|
||||
{
|
||||
resource?.Dispose();
|
||||
_duplication!.ReleaseFrame();
|
||||
}
|
||||
using var frame = CaptureFrame();
|
||||
if (frame == null) return null;
|
||||
return frame.CropBgr(region);
|
||||
}
|
||||
|
||||
private void EnsureStaging(int w, int h)
|
||||
|
|
|
|||
37
src/Poe2Trade.Navigation/FramePipeline.cs
Normal file
37
src/Poe2Trade.Navigation/FramePipeline.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public class FramePipeline : IDisposable
|
||||
{
|
||||
private readonly IScreenCapture _capture;
|
||||
private readonly List<IFrameConsumer> _consumers = [];
|
||||
|
||||
public FramePipeline(IScreenCapture capture)
|
||||
{
|
||||
_capture = capture;
|
||||
}
|
||||
|
||||
public IScreenCapture Capture => _capture;
|
||||
|
||||
public void AddConsumer(IFrameConsumer consumer) => _consumers.Add(consumer);
|
||||
|
||||
/// <summary>
|
||||
/// Capture one frame, dispatch to all consumers in parallel, then dispose frame.
|
||||
/// </summary>
|
||||
public async Task ProcessOneFrame()
|
||||
{
|
||||
using var frame = _capture.CaptureFrame();
|
||||
if (frame == null) return;
|
||||
|
||||
if (_consumers.Count == 1)
|
||||
{
|
||||
_consumers[0].Process(frame);
|
||||
}
|
||||
else
|
||||
{
|
||||
var tasks = _consumers.Select(c => Task.Run(() => c.Process(frame)));
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _capture.Dispose();
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Extensions;
|
||||
using Region = Poe2Trade.Core.Region;
|
||||
|
|
@ -8,14 +9,45 @@ namespace Poe2Trade.Navigation;
|
|||
|
||||
public sealed class GdiCapture : IScreenCapture
|
||||
{
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetSystemMetrics(int nIndex);
|
||||
|
||||
private const int SM_CXSCREEN = 0;
|
||||
private const int SM_CYSCREEN = 1;
|
||||
|
||||
public ScreenFrame? CaptureFrame()
|
||||
{
|
||||
var w = GetSystemMetrics(SM_CXSCREEN);
|
||||
var h = GetSystemMetrics(SM_CYSCREEN);
|
||||
|
||||
var bitmap = new Bitmap(w, h, PixelFormat.Format32bppArgb);
|
||||
using (var g = Graphics.FromImage(bitmap))
|
||||
{
|
||||
g.CopyFromScreen(0, 0, 0, 0, new System.Drawing.Size(w, h),
|
||||
CopyPixelOperation.SourceCopy);
|
||||
}
|
||||
|
||||
var mat = BitmapConverter.ToMat(bitmap);
|
||||
bitmap.Dispose();
|
||||
|
||||
// ToMat returns BGR or BGRA depending on pixel format; ensure BGRA
|
||||
if (mat.Channels() == 3)
|
||||
{
|
||||
var bgra = new Mat();
|
||||
Cv2.CvtColor(mat, bgra, ColorConversionCodes.BGR2BGRA);
|
||||
mat.Dispose();
|
||||
mat = bgra;
|
||||
}
|
||||
|
||||
var ownedMat = mat;
|
||||
return new ScreenFrame(ownedMat, () => ownedMat.Dispose());
|
||||
}
|
||||
|
||||
public Mat? CaptureRegion(Region region)
|
||||
{
|
||||
using var bitmap = new Bitmap(region.Width, region.Height, PixelFormat.Format32bppArgb);
|
||||
using var g = Graphics.FromImage(bitmap);
|
||||
g.CopyFromScreen(region.X, region.Y, 0, 0,
|
||||
new System.Drawing.Size(region.Width, region.Height),
|
||||
CopyPixelOperation.SourceCopy);
|
||||
return BitmapConverter.ToMat(bitmap);
|
||||
using var frame = CaptureFrame();
|
||||
if (frame == null) return null;
|
||||
return frame.CropBgr(region);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
|
|
|||
6
src/Poe2Trade.Navigation/IFrameConsumer.cs
Normal file
6
src/Poe2Trade.Navigation/IFrameConsumer.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public interface IFrameConsumer
|
||||
{
|
||||
void Process(ScreenFrame frame);
|
||||
}
|
||||
|
|
@ -6,4 +6,5 @@ namespace Poe2Trade.Navigation;
|
|||
public interface IScreenCapture : IDisposable
|
||||
{
|
||||
Mat? CaptureRegion(Region region);
|
||||
ScreenFrame? CaptureFrame();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,56 +5,36 @@ using Size = OpenCvSharp.Size;
|
|||
|
||||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public class MinimapCapture : IDisposable
|
||||
public class MinimapCapture : IFrameConsumer, IDisposable
|
||||
{
|
||||
private readonly MinimapConfig _config;
|
||||
private readonly IScreenCapture _backend;
|
||||
private readonly WallColorTracker _colorTracker;
|
||||
private readonly IScreenCapture _backend; // kept for debug capture paths
|
||||
private int _modeCheckCounter;
|
||||
private MinimapMode _detectedMode = MinimapMode.Overlay;
|
||||
private MinimapFrame? _lastFrame;
|
||||
|
||||
public MinimapMode DetectedMode => _detectedMode;
|
||||
public MinimapFrame? LastFrame => _lastFrame;
|
||||
public event Action<MinimapMode>? ModeChanged;
|
||||
|
||||
public MinimapCapture(MinimapConfig config)
|
||||
public MinimapCapture(MinimapConfig config, IScreenCapture backend)
|
||||
{
|
||||
_config = config;
|
||||
_colorTracker = new WallColorTracker(config.WallLoHSV, config.WallHiHSV);
|
||||
_backend = CreateBackend();
|
||||
_backend = backend;
|
||||
}
|
||||
|
||||
private static IScreenCapture CreateBackend()
|
||||
/// <summary>
|
||||
/// IFrameConsumer entry point — called by FramePipeline with the shared screen frame.
|
||||
/// </summary>
|
||||
public void Process(ScreenFrame screen)
|
||||
{
|
||||
// WGC primary → DXGI fallback → GDI last resort
|
||||
try
|
||||
// Auto-detect minimap mode every 10th frame via single pixel probe
|
||||
if (++_modeCheckCounter >= 10)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
public MinimapFrame? CaptureFrame()
|
||||
{
|
||||
// Auto-detect minimap mode every frame (just a 5x5 pixel check, negligible cost)
|
||||
var detected = DetectMinimapMode();
|
||||
_modeCheckCounter = 0;
|
||||
var detected = DetectMinimapMode(screen);
|
||||
if (detected != _detectedMode)
|
||||
{
|
||||
_detectedMode = detected;
|
||||
|
|
@ -62,11 +42,25 @@ public class MinimapCapture : IDisposable
|
|||
ResetAdaptation();
|
||||
ModeChanged?.Invoke(_detectedMode);
|
||||
}
|
||||
}
|
||||
|
||||
using var bgr = CaptureAndNormalize(_detectedMode);
|
||||
if (bgr == null || bgr.Empty())
|
||||
return null;
|
||||
var region = _detectedMode == MinimapMode.Overlay
|
||||
? _config.OverlayRegion
|
||||
: _config.CornerRegion;
|
||||
|
||||
using var bgr = screen.CropBgr(region);
|
||||
if (bgr.Empty())
|
||||
return;
|
||||
|
||||
var frame = ProcessBgr(bgr);
|
||||
if (frame == null) return;
|
||||
|
||||
var old = Interlocked.Exchange(ref _lastFrame, frame);
|
||||
old?.Dispose();
|
||||
}
|
||||
|
||||
private MinimapFrame? ProcessBgr(Mat bgr)
|
||||
{
|
||||
using var hsv = new Mat();
|
||||
Cv2.CvtColor(bgr, hsv, ColorConversionCodes.BGR2HSV);
|
||||
|
||||
|
|
@ -84,12 +78,10 @@ public class MinimapCapture : IDisposable
|
|||
|
||||
if (_detectedMode == MinimapMode.Corner)
|
||||
{
|
||||
// Corner minimap is clean — skip fog detection
|
||||
classified.SetTo(new Scalar((byte)MapCell.Wall), wallMask);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Overlay: fog detection needed (broad blue minus walls minus player)
|
||||
using var fogMask = BuildFogMask(hsv, wallMask, playerMask);
|
||||
classified.SetTo(new Scalar((byte)MapCell.Fog), fogMask);
|
||||
classified.SetTo(new Scalar((byte)MapCell.Wall), wallMask);
|
||||
|
|
@ -101,7 +93,6 @@ public class MinimapCapture : IDisposable
|
|||
grayForCorr.SetTo(Scalar.Black, playerMask);
|
||||
|
||||
// Corner mode: rescale classified + wall mask to match overlay scale
|
||||
// Uses nearest-neighbor so discrete cell values (0-3) stay crisp
|
||||
if (_detectedMode == MinimapMode.Corner && Math.Abs(_config.CornerScale - 1.0) > 0.01)
|
||||
{
|
||||
var scaledSize = (int)Math.Round(frameSize * _config.CornerScale);
|
||||
|
|
@ -133,36 +124,37 @@ public class MinimapCapture : IDisposable
|
|||
);
|
||||
}
|
||||
|
||||
private Mat? CaptureAndNormalize(MinimapMode mode)
|
||||
{
|
||||
var region = mode == MinimapMode.Overlay
|
||||
? _config.OverlayRegion
|
||||
: _config.CornerRegion;
|
||||
|
||||
return _backend.CaptureRegion(region);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect minimap mode by sampling a small patch at the corner minimap center.
|
||||
/// Detect minimap mode by sampling pixels at the corner minimap center.
|
||||
/// If the pixel is close to #DE581B (orange player dot), corner minimap is active.
|
||||
/// </summary>
|
||||
private MinimapMode DetectMinimapMode()
|
||||
private MinimapMode DetectMinimapMode(ScreenFrame screen)
|
||||
{
|
||||
// Capture a tiny 5x5 region at the corner center
|
||||
var cx = _config.CornerCenterX;
|
||||
var cy = _config.CornerCenterY;
|
||||
var probe = new Poe2Trade.Core.Region(cx - 2, cy - 2, 5, 5);
|
||||
using var patch = _backend.CaptureRegion(probe);
|
||||
if (patch == null || patch.Empty())
|
||||
return _detectedMode; // keep current on failure
|
||||
|
||||
// Average the BGR values of the patch
|
||||
var mean = Cv2.Mean(patch);
|
||||
var b = mean.Val0;
|
||||
var g = mean.Val1;
|
||||
var r = mean.Val2;
|
||||
// Bounds check
|
||||
if (cx < 2 || cy < 2 || cx + 2 >= screen.Width || cy + 2 >= screen.Height)
|
||||
return _detectedMode;
|
||||
|
||||
// #DE581B → R=222, G=88, B=27 — check if close (tolerance ~60 per channel)
|
||||
// Average a 5x5 patch worth of pixels
|
||||
double bSum = 0, gSum = 0, rSum = 0;
|
||||
var count = 0;
|
||||
for (var dy = -2; dy <= 2; dy++)
|
||||
for (var dx = -2; dx <= 2; dx++)
|
||||
{
|
||||
var px = screen.PixelAt(cx + dx, cy + dy);
|
||||
bSum += px.Item0;
|
||||
gSum += px.Item1;
|
||||
rSum += px.Item2;
|
||||
count++;
|
||||
}
|
||||
|
||||
var b = bSum / count;
|
||||
var g = gSum / count;
|
||||
var r = rSum / count;
|
||||
|
||||
// #DE581B → R=222, G=88, B=27
|
||||
const int tol = 60;
|
||||
if (Math.Abs(r - 222) < tol && Math.Abs(g - 88) < tol && Math.Abs(b - 27) < tol)
|
||||
return MinimapMode.Corner;
|
||||
|
|
@ -172,23 +164,19 @@ public class MinimapCapture : IDisposable
|
|||
|
||||
private Mat BuildWallMask(Mat hsv, Mat playerMask, bool sample = false)
|
||||
{
|
||||
// Use adapted range if available (narrows per-map), otherwise broad default
|
||||
var lo = _colorTracker.AdaptedLo ?? _config.WallLoHSV;
|
||||
var hi = _colorTracker.AdaptedHi ?? _config.WallHiHSV;
|
||||
|
||||
var wallMask = new Mat();
|
||||
Cv2.InRange(hsv, lo, hi, wallMask);
|
||||
|
||||
// Subtract player marker (orange overlaps blue range slightly on some maps)
|
||||
using var notPlayer = new Mat();
|
||||
Cv2.BitwiseNot(playerMask, notPlayer);
|
||||
Cv2.BitwiseAnd(wallMask, notPlayer, wallMask);
|
||||
|
||||
// Sample from pure color-selected pixels BEFORE morphological ops
|
||||
if (sample)
|
||||
_colorTracker.SampleFrame(hsv, wallMask);
|
||||
|
||||
// Dilate to connect thin wall line fragments
|
||||
using var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3));
|
||||
Cv2.Dilate(wallMask, wallMask, kernel);
|
||||
|
||||
|
|
@ -198,11 +186,9 @@ public class MinimapCapture : IDisposable
|
|||
|
||||
private Mat BuildFogMask(Mat hsv, Mat wallMask, Mat playerMask)
|
||||
{
|
||||
// Broad blue detection (captures walls + fog + any blue)
|
||||
using var allBlue = new Mat();
|
||||
Cv2.InRange(hsv, _config.FogLoHSV, _config.FogHiHSV, allBlue);
|
||||
|
||||
// Subtract player and walls → remaining blue is fog
|
||||
using var notPlayer = new Mat();
|
||||
Cv2.BitwiseNot(playerMask, notPlayer);
|
||||
Cv2.BitwiseAnd(allBlue, notPlayer, allBlue);
|
||||
|
|
@ -225,13 +211,12 @@ public class MinimapCapture : IDisposable
|
|||
using var centroids = new Mat();
|
||||
var numLabels = Cv2.ConnectedComponentsWithStats(mask, labels, stats, centroids);
|
||||
|
||||
if (numLabels <= 1) return; // only background
|
||||
if (numLabels <= 1) return;
|
||||
|
||||
// Clear mask, then re-add only components meeting min area
|
||||
mask.SetTo(Scalar.Black);
|
||||
for (var i = 1; i < numLabels; i++)
|
||||
{
|
||||
var area = stats.At<int>(i, 4); // CC_STAT_AREA
|
||||
var area = stats.At<int>(i, 4);
|
||||
if (area < minArea) continue;
|
||||
|
||||
using var comp = new Mat();
|
||||
|
|
@ -242,10 +227,15 @@ public class MinimapCapture : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Capture a single frame and return the requested pipeline stage as PNG bytes.
|
||||
/// Uses CaptureRegion directly (debug path, not the pipeline).
|
||||
/// </summary>
|
||||
public byte[]? CaptureStage(MinimapDebugStage stage)
|
||||
{
|
||||
using var bgr = CaptureAndNormalize(_detectedMode);
|
||||
var region = _detectedMode == MinimapMode.Overlay
|
||||
? _config.OverlayRegion
|
||||
: _config.CornerRegion;
|
||||
|
||||
using var bgr = _backend.CaptureRegion(region);
|
||||
if (bgr == null || bgr.Empty()) return null;
|
||||
|
||||
if (stage == MinimapDebugStage.Raw) return EncodePng(bgr);
|
||||
|
|
@ -272,11 +262,10 @@ public class MinimapCapture : IDisposable
|
|||
using var fogMask = BuildFogMask(hsv, wallMask, playerMask);
|
||||
if (stage == MinimapDebugStage.Fog) return EncodePng(fogMask);
|
||||
|
||||
// Classified (walls + fog + player — explored is tracked by WorldMap)
|
||||
using var classified = new Mat(bgr.Height, bgr.Width, MatType.CV_8UC3, Scalar.Black);
|
||||
classified.SetTo(new Scalar(180, 140, 70), fogMask); // light blue for fog
|
||||
classified.SetTo(new Scalar(26, 45, 61), wallMask); // brown for walls
|
||||
classified.SetTo(new Scalar(0, 165, 255), playerMask); // orange for player
|
||||
classified.SetTo(new Scalar(180, 140, 70), fogMask);
|
||||
classified.SetTo(new Scalar(26, 45, 61), wallMask);
|
||||
classified.SetTo(new Scalar(0, 165, 255), playerMask);
|
||||
return EncodePng(classified);
|
||||
}
|
||||
|
||||
|
|
@ -292,7 +281,12 @@ public class MinimapCapture : IDisposable
|
|||
public void SaveDebugCapture(string dir = "debug-minimap")
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
using var bgr = CaptureAndNormalize(_detectedMode);
|
||||
|
||||
var region = _detectedMode == MinimapMode.Overlay
|
||||
? _config.OverlayRegion
|
||||
: _config.CornerRegion;
|
||||
|
||||
using var bgr = _backend.CaptureRegion(region);
|
||||
if (bgr == null || bgr.Empty()) return;
|
||||
|
||||
using var hsv = new Mat();
|
||||
|
|
@ -303,17 +297,15 @@ public class MinimapCapture : IDisposable
|
|||
|
||||
using var wallMask = BuildWallMask(hsv, playerMask);
|
||||
|
||||
// Colorized classified (walls + player)
|
||||
using var classified = new Mat(bgr.Height, bgr.Width, MatType.CV_8UC3, Scalar.Black);
|
||||
classified.SetTo(new Scalar(26, 45, 61), wallMask); // brown
|
||||
classified.SetTo(new Scalar(0, 165, 255), playerMask); // orange
|
||||
classified.SetTo(new Scalar(26, 45, 61), wallMask);
|
||||
classified.SetTo(new Scalar(0, 165, 255), playerMask);
|
||||
|
||||
Cv2.ImWrite(Path.Combine(dir, "01-raw.png"), bgr);
|
||||
Cv2.ImWrite(Path.Combine(dir, "02-walls.png"), wallMask);
|
||||
Cv2.ImWrite(Path.Combine(dir, "03-player.png"), playerMask);
|
||||
Cv2.ImWrite(Path.Combine(dir, "04-classified.png"), classified);
|
||||
|
||||
// HSV channels
|
||||
var channels = Cv2.Split(hsv);
|
||||
Cv2.ImWrite(Path.Combine(dir, "05-hue.png"), channels[0]);
|
||||
Cv2.ImWrite(Path.Combine(dir, "06-sat.png"), channels[1]);
|
||||
|
|
@ -326,7 +318,7 @@ public class MinimapCapture : IDisposable
|
|||
private Point2d FindCentroid(Mat mask)
|
||||
{
|
||||
var moments = Cv2.Moments(mask, true);
|
||||
if (moments.M00 < 10) // not enough pixels
|
||||
if (moments.M00 < 10)
|
||||
return new Point2d(0, 0);
|
||||
|
||||
var cx = moments.M10 / moments.M00 - mask.Width / 2.0;
|
||||
|
|
@ -334,14 +326,11 @@ public class MinimapCapture : IDisposable
|
|||
return new Point2d(cx, cy);
|
||||
}
|
||||
|
||||
/// <summary>Commit pending wall color samples (call after successful template match).</summary>
|
||||
public void CommitWallColors() => _colorTracker.Commit();
|
||||
|
||||
/// <summary>Reset adaptive color tracking (call on area change).</summary>
|
||||
public void ResetAdaptation() => _colorTracker.Reset();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_backend.Dispose();
|
||||
_lastFrame?.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ public class NavigationExecutor : IDisposable
|
|||
{
|
||||
private readonly IGameController _game;
|
||||
private readonly MinimapConfig _config;
|
||||
private readonly FramePipeline _pipeline;
|
||||
private readonly MinimapCapture _capture;
|
||||
private readonly WorldMap _worldMap;
|
||||
private NavigationState _state = NavigationState.Idle;
|
||||
|
|
@ -24,7 +25,12 @@ public class NavigationExecutor : IDisposable
|
|||
{
|
||||
_game = game;
|
||||
_config = config ?? new MinimapConfig();
|
||||
_capture = new MinimapCapture(_config);
|
||||
|
||||
var backend = CreateBackend();
|
||||
_pipeline = new FramePipeline(backend);
|
||||
_capture = new MinimapCapture(_config, backend);
|
||||
_pipeline.AddConsumer(_capture);
|
||||
|
||||
_worldMap = new WorldMap(_config);
|
||||
_capture.ModeChanged += _ =>
|
||||
{
|
||||
|
|
@ -34,6 +40,35 @@ public class NavigationExecutor : IDisposable
|
|||
};
|
||||
}
|
||||
|
||||
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 SetState(NavigationState s)
|
||||
{
|
||||
_state = s;
|
||||
|
|
@ -76,9 +111,10 @@ public class NavigationExecutor : IDisposable
|
|||
|
||||
try
|
||||
{
|
||||
// 1. Capture + track every frame
|
||||
// 1. Capture + process via pipeline (single full-screen capture)
|
||||
SetState(NavigationState.Capturing);
|
||||
using var frame = _capture.CaptureFrame();
|
||||
await _pipeline.ProcessOneFrame();
|
||||
var frame = _capture.LastFrame;
|
||||
if (frame == null)
|
||||
{
|
||||
Log.Warning("Failed to capture minimap frame");
|
||||
|
|
@ -238,15 +274,16 @@ public class NavigationExecutor : IDisposable
|
|||
public byte[] GetViewportSnapshot(int viewSize = 400) => _worldMap.GetViewportSnapshot(_worldMap.Position, viewSize);
|
||||
|
||||
/// <summary>
|
||||
/// Capture one frame, track position, stitch into world map.
|
||||
/// Capture one frame via pipeline, track position, stitch into world map.
|
||||
/// Returns PNG bytes for the requested debug stage (or world map viewport by default).
|
||||
/// </summary>
|
||||
public byte[]? ProcessFrame(MinimapDebugStage stage = MinimapDebugStage.WorldMap)
|
||||
public async Task<byte[]?> ProcessFrame(MinimapDebugStage stage = MinimapDebugStage.WorldMap)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var captureStart = sw.Elapsed.TotalMilliseconds;
|
||||
using var frame = _capture.CaptureFrame();
|
||||
await _pipeline.ProcessOneFrame();
|
||||
var frame = _capture.LastFrame;
|
||||
var captureMs = sw.Elapsed.TotalMilliseconds - captureStart;
|
||||
|
||||
if (frame == null) return null;
|
||||
|
|
@ -276,6 +313,7 @@ public class NavigationExecutor : IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
_capture.Dispose();
|
||||
_pipeline.Dispose();
|
||||
_worldMap.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
src/Poe2Trade.Navigation/ScreenFrame.cs
Normal file
37
src/Poe2Trade.Navigation/ScreenFrame.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using OpenCvSharp;
|
||||
using Region = Poe2Trade.Core.Region;
|
||||
|
||||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public class ScreenFrame : IDisposable
|
||||
{
|
||||
private readonly Mat _bgraMat;
|
||||
private readonly Action _disposeAction;
|
||||
|
||||
public ScreenFrame(Mat bgraMat, Action disposeAction)
|
||||
{
|
||||
_bgraMat = bgraMat;
|
||||
_disposeAction = disposeAction;
|
||||
}
|
||||
|
||||
public int Width => _bgraMat.Width;
|
||||
public int Height => _bgraMat.Height;
|
||||
|
||||
/// <summary>
|
||||
/// Crop region and convert to BGR. Returns a new Mat owned by the caller.
|
||||
/// </summary>
|
||||
public Mat CropBgr(Region region)
|
||||
{
|
||||
using var roi = new Mat(_bgraMat, new Rect(region.X, region.Y, region.Width, region.Height));
|
||||
var bgr = new Mat();
|
||||
Cv2.CvtColor(roi, bgr, ColorConversionCodes.BGRA2BGR);
|
||||
return bgr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a single pixel (B, G, R, A) — useful for mode detection probes.
|
||||
/// </summary>
|
||||
public Vec4b PixelAt(int x, int y) => _bgraMat.At<Vec4b>(y, x);
|
||||
|
||||
public void Dispose() => _disposeAction();
|
||||
}
|
||||
|
|
@ -123,45 +123,39 @@ public sealed class WgcCapture : IScreenCapture
|
|||
}
|
||||
}
|
||||
|
||||
public unsafe Mat? CaptureRegion(Region region)
|
||||
public unsafe ScreenFrame? CaptureFrame()
|
||||
{
|
||||
using var frame = _framePool.TryGetNextFrame();
|
||||
var frame = _framePool.TryGetNextFrame();
|
||||
if (frame == null) return null;
|
||||
|
||||
using var srcTexture = GetTextureFromSurface(frame.Surface);
|
||||
if (srcTexture == null) return null;
|
||||
var srcTexture = GetTextureFromSurface(frame.Surface);
|
||||
if (srcTexture == null) { frame.Dispose(); return null; }
|
||||
|
||||
EnsureStaging(region.Width, region.Height);
|
||||
var desc = srcTexture.Description;
|
||||
var w = (int)desc.Width;
|
||||
var h = (int)desc.Height;
|
||||
EnsureStaging(w, h);
|
||||
|
||||
_context.CopySubresourceRegion(
|
||||
_staging!, 0, 0, 0, 0,
|
||||
srcTexture, 0,
|
||||
new Vortice.Mathematics.Box(region.X, region.Y, 0,
|
||||
region.X + region.Width, region.Y + region.Height, 1));
|
||||
_context.CopySubresourceRegion(_staging!, 0, 0, 0, 0, srcTexture, 0);
|
||||
|
||||
var mapped = _context.Map(_staging!, 0, MapMode.Read);
|
||||
try
|
||||
{
|
||||
var mat = new Mat(region.Height, region.Width, MatType.CV_8UC4);
|
||||
var rowBytes = region.Width * 4;
|
||||
|
||||
for (var row = 0; row < region.Height; row++)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
(void*)(mapped.DataPointer + row * mapped.RowPitch),
|
||||
(void*)mat.Ptr(row),
|
||||
rowBytes, rowBytes);
|
||||
}
|
||||
// Zero-copy: Mat wraps mapped staging pointer, RowPitch handles GPU row padding
|
||||
var mat = Mat.FromPixelData(h, w, MatType.CV_8UC4, mapped.DataPointer, (int)mapped.RowPitch);
|
||||
|
||||
var bgr = new Mat();
|
||||
Cv2.CvtColor(mat, bgr, ColorConversionCodes.BGRA2BGR);
|
||||
mat.Dispose();
|
||||
return bgr;
|
||||
}
|
||||
finally
|
||||
return new ScreenFrame(mat, () =>
|
||||
{
|
||||
_context.Unmap(_staging!, 0);
|
||||
srcTexture.Dispose();
|
||||
frame.Dispose();
|
||||
});
|
||||
}
|
||||
|
||||
public unsafe Mat? CaptureRegion(Region region)
|
||||
{
|
||||
using var frame = CaptureFrame();
|
||||
if (frame == null) return null;
|
||||
return frame.CropBgr(region);
|
||||
}
|
||||
|
||||
private ID3D11Texture2D? GetTextureFromSurface(IDirect3DSurface surface)
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
// Minimap display: if explore loop owns capture, just render viewport
|
||||
var bytes = _bot.Navigation.IsExploring
|
||||
? _bot.Navigation.GetViewportSnapshot()
|
||||
: _bot.Navigation.ProcessFrame(SelectedMinimapStage);
|
||||
: await _bot.Navigation.ProcessFrame(SelectedMinimapStage);
|
||||
if (bytes != null)
|
||||
{
|
||||
var bmp = new Bitmap(new MemoryStream(bytes));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue