This commit is contained in:
Boki 2026-02-17 19:40:53 -05:00
parent 2565028ad0
commit 23c581cff9
6 changed files with 41 additions and 54 deletions

View file

@ -14,7 +14,7 @@ internal class IconDetector : IDisposable
private readonly Mat _checkpointOnTemplate;
private const double DoorThreshold = 0.65;
private const double CheckpointThreshold = 0.60;
private const double CheckpointThreshold = 0.75;
public IconDetector(string assetsDir)
{

View file

@ -20,6 +20,13 @@ public class MinimapCapture : IFrameConsumer, IDisposable
public MinimapMode DetectedMode => _detectedMode;
public MinimapFrame? LastFrame => _lastFrame;
/// <summary>
/// Atomically take ownership of the last frame (sets _lastFrame to null).
/// Caller is responsible for disposing the returned frame.
/// </summary>
public MinimapFrame? TakeFrame() => Interlocked.Exchange(ref _lastFrame, null);
public event Action<MinimapMode>? ModeChanged;
public MinimapCapture(MinimapConfig config, IScreenCapture backend, string? assetsDir = null)
@ -259,6 +266,22 @@ public class MinimapCapture : IFrameConsumer, IDisposable
Cv2.BitwiseNot(playerMask, notPlayer);
Cv2.BitwiseAnd(wallMask, notPlayer, wallMask);
// Overlay mode: exclude game text labels bleeding through the minimap.
// Text color #E0E0F6 → HSV(120, 23, 246): same blue hue as walls but
// much lower saturation. AA edges blend with the background pushing S
// above the wall floor. Detect the text core, dilate to cover the AA
// fringe, then subtract from wall mask before morphology amplifies it.
if (!isCorner)
{
using var textMask = new Mat();
Cv2.InRange(hsv, new Scalar(0, 0, 200), new Scalar(180, 50, 255), textMask);
using var textKernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(11, 11));
Cv2.Dilate(textMask, textMask, textKernel);
using var notText = new Mat();
Cv2.BitwiseNot(textMask, notText);
Cv2.BitwiseAnd(wallMask, notText, wallMask);
}
if (sample && !isCorner)
_colorTracker.SampleFrame(hsv, wallMask);

View file

@ -130,7 +130,7 @@ public class NavigationExecutor : IDisposable
// 1. Capture + process via pipeline (single full-screen capture)
SetState(NavigationState.Capturing);
await _pipeline.ProcessOneFrame();
var frame = _capture.LastFrame;
using var frame = _capture.TakeFrame();
if (frame == null)
{
Log.Warning("Failed to capture minimap frame");
@ -489,7 +489,7 @@ public class NavigationExecutor : IDisposable
{
Log.Warning(ex, "Pipeline capture failed");
}
var frame = _capture.LastFrame;
using var frame = _capture.TakeFrame();
var captureMs = sw.Elapsed.TotalMilliseconds - captureStart;
if (frame == null)

View file

@ -141,11 +141,11 @@ public class MinimapConfig
public double MatchConfidence { get; set; } = 0.25;
// Wall confidence (canvas-level): per-pixel counters to filter transient noise
// Walls need ceil(Threshold/Inc) = 2 frames of reinforcement to appear.
// Walls need ceil(Threshold/Inc) = 3 frames of reinforcement to appear.
// Block noise filter handles glow; confidence just smooths frame-to-frame jitter.
public int ConfidenceInc { get; set; } = 6;
public int ConfidenceInc { get; set; } = 5;
public int ConfidenceDec { get; set; } = 1;
public int ConfidenceThreshold { get; set; } = 10;
public int ConfidenceThreshold { get; set; } = 13;
public int ConfidenceMax { get; set; } = 30;
public int WarmupFrames { get; set; } = 5;

View file

@ -8,8 +8,6 @@ namespace Poe2Trade.Navigation;
/// </summary>
internal record BfsResult(
List<Point> Path, // canvas coords: player → target frontier (subsampled)
List<Point> BestFrontier, // frontier cells in the chosen direction (canvas coords)
List<Point> OtherFrontier, // frontier cells in other directions (canvas coords)
double DirX, double DirY, // chosen direction (unit vector)
int PlayerCx, int PlayerCy // player position on canvas
);
@ -130,8 +128,6 @@ internal class PathFinder
var bestClusterScore = -1.0;
var bestEntryGx = -1;
var bestEntryGy = -1;
List<Point>? bestFrontierPts = null;
var otherFrontier = new List<Point>();
var clusterQueue = new Queue<(int gx, int gy)>(256);
foreach (var (fgx, fgy) in frontierCells)
@ -179,27 +175,13 @@ internal class PathFinder
var cost = (int)minDist;
var score = gain / (cost + 1.0);
// Convert cluster cells to canvas coords
var pts = new List<Point>(gain);
foreach (var (cgx, cgy) in clusterCells)
pts.Add(new Point(cx + (cgx - rr) * step, cy + (cgy - rr) * step));
if (score > bestClusterScore)
{
// Demote previous best to "other"
if (bestFrontierPts != null)
otherFrontier.AddRange(bestFrontierPts);
bestClusterScore = score;
bestClusterGain = gain;
bestClusterCost = cost;
bestEntryGx = entryGx;
bestEntryGy = entryGy;
bestFrontierPts = pts;
}
else
{
otherFrontier.AddRange(pts);
}
}
@ -237,7 +219,7 @@ internal class PathFinder
dirY /= len;
// Step F: Store result for visualization
LastResult = new BfsResult(path, bestFrontierPts!, otherFrontier, dirX, dirY, cx, cy);
LastResult = new BfsResult(path, dirX, dirY, cx, cy);
Log.Debug("BFS: {ClusterCount} clusters, best={Gain} cells cost={Cost} score={Score:F1}, dir=({Dx:F2},{Dy:F2}), path={PathLen} waypoints",
clusterCount, bestClusterGain, bestClusterCost, bestClusterScore, dirX, dirY, path.Count);
@ -358,8 +340,8 @@ internal class PathFinder
dirX /= len;
dirY /= len;
// Store result for visualization (reuse BfsResult — frontiers empty for target mode)
LastResult = new BfsResult(path, [], [], dirX, dirY, cx, cy);
// Store result for visualization
LastResult = new BfsResult(path, dirX, dirY, cx, cy);
Log.Debug("BFS target: path={PathLen} waypoints to ({TX},{TY}), dir=({Dx:F2},{Dy:F2})",
path.Count, target.X, target.Y, dirX, dirY);

View file

@ -104,7 +104,7 @@ public class WorldMap : IDisposable
if (wallCountAfter < 50)
{
_frameCount--;
Log.Information("Warmup waiting for minimap ({Ms:F1}ms)", sw.Elapsed.TotalMilliseconds);
Log.Debug("Warmup waiting for minimap ({Ms:F1}ms)", sw.Elapsed.TotalMilliseconds);
return _position;
}
@ -120,7 +120,7 @@ public class WorldMap : IDisposable
}
else
{
Log.Information("Warmup frame {N}/{Total}: walls={WallsBefore}→{WallsAfter}(filtered) frameSize={FS} stitch={Ms:F1}ms",
Log.Debug("Warmup frame {N}/{Total}: walls={WallsBefore}→{WallsAfter}(filtered) frameSize={FS} stitch={Ms:F1}ms",
_frameCount, _config.WarmupFrames, wallCountBefore, wallCountAfter,
wallMask.Width, sw.Elapsed.TotalMilliseconds);
}
@ -136,7 +136,7 @@ public class WorldMap : IDisposable
{
_consecutiveMatchFails++;
LastMatchSucceeded = false;
Log.Information("MatchAndStitch: dedup={Dedup:F1}ms match={Match:F1}ms (FAILED x{Fails}) total={Total:F1}ms",
Log.Debug("MatchAndStitch: dedup={Dedup:F1}ms match={Match:F1}ms (FAILED x{Fails}) total={Total:F1}ms",
dedupMs, matchMs, _consecutiveMatchFails, sw.Elapsed.TotalMilliseconds);
return _position; // don't stitch — wrong position would corrupt the canvas
}
@ -170,7 +170,7 @@ public class WorldMap : IDisposable
var cleanFraction = FilterNoisyBlocks(wallMask, classifiedMat);
if (cleanFraction < 0.25)
{
Log.Information("Noise filter: {Clean:P0} clean, skipping ({Ms:F1}ms)",
Log.Debug("Noise filter: {Clean:P0} clean, skipping ({Ms:F1}ms)",
cleanFraction, sw.Elapsed.TotalMilliseconds);
return true;
}
@ -228,14 +228,14 @@ public class WorldMap : IDisposable
var frameWallCount = Cv2.CountNonZero(wallMask);
if (frameWallCount < 50)
{
Log.Information("Match fail: too few frame walls ({FrameWalls}) frameSize={FS}",
Log.Debug("Match fail: too few frame walls ({FrameWalls}) frameSize={FS}",
frameWallCount, frameSize);
return null;
}
if (canvasWallCount < 50)
{
Log.Information("Match fail: too few canvas walls ({CanvasWalls}) frame walls={FrameWalls} frameSize={FS} searchROI={SW}x{SH}",
Log.Debug("Match fail: too few canvas walls ({CanvasWalls}) frame walls={FrameWalls} frameSize={FS} searchROI={SW}x{SH}",
canvasWallCount, frameWallCount, frameSize, sw, sh);
return null;
}
@ -247,7 +247,7 @@ public class WorldMap : IDisposable
if (maxVal < _config.MatchConfidence)
{
Log.Information("Match fail: low confidence {Conf:F3} (need {Min:F2}) frameSize={FS} searchROI={SW}x{SH} canvas={CanvasWalls} frame={FrameWalls}",
Log.Debug("Match fail: low confidence {Conf:F3} (need {Min:F2}) frameSize={FS} searchROI={SW}x{SH} canvas={CanvasWalls} frame={FrameWalls}",
maxVal, _config.MatchConfidence, frameSize, sw, sh, canvasWallCount, frameWallCount);
return null;
}
@ -259,7 +259,7 @@ public class WorldMap : IDisposable
var deltaX = matchX - estimate.X;
var deltaY = matchY - estimate.Y;
Log.Information("Match OK: conf={Conf:F3} pos=({X:F1},{Y:F1}) delta=({Dx:F1},{Dy:F1}) frameSize={FS} searchROI={SW}x{SH} canvas={CanvasWalls} frame={FrameWalls}",
Log.Debug("Match OK: conf={Conf:F3} pos=({X:F1},{Y:F1}) delta=({Dx:F1},{Dy:F1}) frameSize={FS} searchROI={SW}x{SH} canvas={CanvasWalls} frame={FrameWalls}",
maxVal, matchX, matchY, deltaX, deltaY, frameSize, sw, sh, canvasWallCount, frameWallCount);
return new MapPosition(matchX, matchY);
@ -467,28 +467,10 @@ public class WorldMap : IDisposable
colored.Set(r, c, new Vec3b(120, 70, 40));
}
// BFS overlay: frontier cells + direction line
// BFS overlay: direction arrow + path line
var bfs = _pathFinder.LastResult;
if (bfs != null)
{
// Other frontier directions (dim cyan)
foreach (var pt in bfs.OtherFrontier)
{
var vx = pt.X - x0;
var vy = pt.Y - y0;
if (vx >= 0 && vx < viewSize && vy >= 0 && vy < viewSize)
colored.Set(vy, vx, new Vec3b(100, 80, 0));
}
// Best frontier direction (bright green)
foreach (var pt in bfs.BestFrontier)
{
var vx = pt.X - x0;
var vy = pt.Y - y0;
if (vx >= 0 && vx < viewSize && vy >= 0 && vy < viewSize)
colored.Set(vy, vx, new Vec3b(0, 220, 0));
}
// Direction line from player
var px2 = bfs.PlayerCx - x0;
var py2 = bfs.PlayerCy - y0;