diff --git a/src/Poe2Trade.Navigation/IconDetector.cs b/src/Poe2Trade.Navigation/IconDetector.cs
index f0f2b09..8e915c0 100644
--- a/src/Poe2Trade.Navigation/IconDetector.cs
+++ b/src/Poe2Trade.Navigation/IconDetector.cs
@@ -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)
{
diff --git a/src/Poe2Trade.Navigation/MinimapCapture.cs b/src/Poe2Trade.Navigation/MinimapCapture.cs
index 015db6b..576422a 100644
--- a/src/Poe2Trade.Navigation/MinimapCapture.cs
+++ b/src/Poe2Trade.Navigation/MinimapCapture.cs
@@ -20,6 +20,13 @@ public class MinimapCapture : IFrameConsumer, IDisposable
public MinimapMode DetectedMode => _detectedMode;
public MinimapFrame? LastFrame => _lastFrame;
+
+ ///
+ /// Atomically take ownership of the last frame (sets _lastFrame to null).
+ /// Caller is responsible for disposing the returned frame.
+ ///
+ public MinimapFrame? TakeFrame() => Interlocked.Exchange(ref _lastFrame, null);
+
public event Action? 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);
diff --git a/src/Poe2Trade.Navigation/NavigationExecutor.cs b/src/Poe2Trade.Navigation/NavigationExecutor.cs
index 58e6e84..32771b7 100644
--- a/src/Poe2Trade.Navigation/NavigationExecutor.cs
+++ b/src/Poe2Trade.Navigation/NavigationExecutor.cs
@@ -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)
diff --git a/src/Poe2Trade.Navigation/NavigationTypes.cs b/src/Poe2Trade.Navigation/NavigationTypes.cs
index 2d05dd2..408e7ab 100644
--- a/src/Poe2Trade.Navigation/NavigationTypes.cs
+++ b/src/Poe2Trade.Navigation/NavigationTypes.cs
@@ -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;
diff --git a/src/Poe2Trade.Navigation/PathFinder.cs b/src/Poe2Trade.Navigation/PathFinder.cs
index df6f372..ea783ac 100644
--- a/src/Poe2Trade.Navigation/PathFinder.cs
+++ b/src/Poe2Trade.Navigation/PathFinder.cs
@@ -8,8 +8,6 @@ namespace Poe2Trade.Navigation;
///
internal record BfsResult(
List Path, // canvas coords: player → target frontier (subsampled)
- List BestFrontier, // frontier cells in the chosen direction (canvas coords)
- List 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? bestFrontierPts = null;
- var otherFrontier = new List();
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(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);
diff --git a/src/Poe2Trade.Navigation/WorldMap.cs b/src/Poe2Trade.Navigation/WorldMap.cs
index 0295efe..f697d57 100644
--- a/src/Poe2Trade.Navigation/WorldMap.cs
+++ b/src/Poe2Trade.Navigation/WorldMap.cs
@@ -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;