From 23c581cff9f7f8b87971acbed9ba9b21762e05a3 Mon Sep 17 00:00:00 2001 From: Boki Date: Tue, 17 Feb 2026 19:40:53 -0500 Subject: [PATCH] better --- src/Poe2Trade.Navigation/IconDetector.cs | 2 +- src/Poe2Trade.Navigation/MinimapCapture.cs | 23 ++++++++++++ .../NavigationExecutor.cs | 4 +-- src/Poe2Trade.Navigation/NavigationTypes.cs | 6 ++-- src/Poe2Trade.Navigation/PathFinder.cs | 24 ++----------- src/Poe2Trade.Navigation/WorldMap.cs | 36 +++++-------------- 6 files changed, 41 insertions(+), 54 deletions(-) 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;