From b9b9a41c3ceb6ee5dc413d1622b4716b4320db37 Mon Sep 17 00:00:00 2001 From: Boki Date: Fri, 13 Feb 2026 14:35:46 -0500 Subject: [PATCH] fixes for noise --- src/Poe2Trade.Navigation/MinimapCapture.cs | 73 +++++---------------- src/Poe2Trade.Navigation/NavigationTypes.cs | 15 +++-- src/Poe2Trade.Navigation/WorldMap.cs | 44 ++++++++++--- 3 files changed, 61 insertions(+), 71 deletions(-) diff --git a/src/Poe2Trade.Navigation/MinimapCapture.cs b/src/Poe2Trade.Navigation/MinimapCapture.cs index 2ce8f82..f242d2d 100644 --- a/src/Poe2Trade.Navigation/MinimapCapture.cs +++ b/src/Poe2Trade.Navigation/MinimapCapture.cs @@ -53,78 +53,51 @@ public class MinimapCapture : IDisposable if (bgr == null || bgr.Empty()) return null; - // --- 1. HSV + extract S/V channels --- using var hsv = new Mat(); Cv2.CvtColor(bgr, hsv, ColorConversionCodes.BGR2HSV); - using var satChan = new Mat(); - using var valueChan = new Mat(); - Cv2.ExtractChannel(hsv, satChan, 1); // S - Cv2.ExtractChannel(hsv, valueChan, 2); // V - // --- 2. Player mask (orange marker) --- + // Player mask (orange marker) using var playerMask = new Mat(); Cv2.InRange(hsv, _config.PlayerLoHSV, _config.PlayerHiHSV, playerMask); var playerOffset = FindCentroid(playerMask); - // --- 3. Wall mask: bright OR saturated → structure lines --- - var wallMask = BuildWallMask(satChan, valueChan, playerMask); + // Wall mask: target #A2AEE5 blue-lavender structure lines + var wallMask = BuildWallMask(hsv, playerMask); - // --- 4. Build classified mat (walls only — for stitching) --- + // Build classified mat (walls only — for stitching) var classified = new Mat(_config.CaptureSize, _config.CaptureSize, MatType.CV_8UC1, Scalar.Black); classified.SetTo(new Scalar((byte)MapCell.Wall), wallMask); - // Raw walls used for everything: dedup, matching, and stitching. - // Temporal smoothing kills walls during movement (minimap scrolls between frames, - // 3/5 vote fails). The confidence system is the long-term noise filter instead. - - // --- 5. Gray for optical flow tracking (player zeroed) --- + // Gray for correlation tracking (player zeroed) var grayForCorr = new Mat(); Cv2.CvtColor(bgr, grayForCorr, ColorConversionCodes.BGR2GRAY); grayForCorr.SetTo(Scalar.Black, playerMask); return new MinimapFrame( GrayMat: grayForCorr, - WallMask: wallMask, // raw — for matching + dedup - ClassifiedMat: classified, // raw — for stitching (confidence filters noise) + WallMask: wallMask, + ClassifiedMat: classified, PlayerOffset: playerOffset, Timestamp: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() ); } - private Mat BuildWallMask(Mat satChan, Mat valueChan, Mat playerMask) + private Mat BuildWallMask(Mat hsv, Mat playerMask) { - // Wall = bright OR saturated pixels (minimap structure lines, icons) - using var highV = new Mat(); - Cv2.Threshold(valueChan, highV, _config.WallMinValue, 255, ThresholdTypes.Binary); - using var highS = new Mat(); - Cv2.Threshold(satChan, highS, _config.WallMinSat, 255, ThresholdTypes.Binary); - - // Exclude nameplate text: very bright (V > 230) AND unsaturated (S < 40) - // Nameplates are pure white text; minimap walls are colored (have saturation) - using var nameplateBright = new Mat(); - Cv2.Threshold(valueChan, nameplateBright, _config.NameplateMinValue, 255, ThresholdTypes.Binary); - using var nameplateLowSat = new Mat(); - Cv2.Threshold(satChan, nameplateLowSat, _config.NameplateMaxSat, 255, ThresholdTypes.BinaryInv); - using var nameplateMask = new Mat(); - Cv2.BitwiseAnd(nameplateBright, nameplateLowSat, nameplateMask); - + // Target wall color #A2AEE5 — HSV(115, 75, 229) + // This is map-independent: walls are always blue-lavender, fog is higher-saturation blue var wallMask = new Mat(); - Cv2.BitwiseOr(highV, highS, wallMask); + Cv2.InRange(hsv, _config.WallLoHSV, _config.WallHiHSV, wallMask); - // Subtract nameplates - using var notNameplate = new Mat(); - Cv2.BitwiseNot(nameplateMask, notNameplate); - Cv2.BitwiseAnd(wallMask, notNameplate, wallMask); - - // Dilate to connect wall line fragments - using var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)); - Cv2.Dilate(wallMask, wallMask, kernel); - - // Subtract player (bright/saturated marker would otherwise be classified as wall) + // 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); + // Dilate to connect thin wall line fragments + using var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)); + Cv2.Dilate(wallMask, wallMask, kernel); + FilterSmallComponents(wallMask, _config.WallMinArea); return wallMask; } @@ -176,16 +149,11 @@ public class MinimapCapture : IDisposable return result; } - using var satChan = new Mat(); - using var valueChan = new Mat(); - Cv2.ExtractChannel(hsv, satChan, 1); - Cv2.ExtractChannel(hsv, valueChan, 2); - using var playerMask = new Mat(); Cv2.InRange(hsv, _config.PlayerLoHSV, _config.PlayerHiHSV, playerMask); if (stage == MinimapDebugStage.Player) return EncodePng(playerMask); - using var wallMask = BuildWallMask(satChan, valueChan, playerMask); + using var wallMask = BuildWallMask(hsv, playerMask); if (stage == MinimapDebugStage.Walls) return EncodePng(wallMask); // Classified (walls + player only — explored is tracked by WorldMap) @@ -214,15 +182,10 @@ public class MinimapCapture : IDisposable using var hsv = new Mat(); Cv2.CvtColor(bgr, hsv, ColorConversionCodes.BGR2HSV); - using var satChan = new Mat(); - using var valueChan = new Mat(); - Cv2.ExtractChannel(hsv, satChan, 1); - Cv2.ExtractChannel(hsv, valueChan, 2); - using var playerMask = new Mat(); Cv2.InRange(hsv, _config.PlayerLoHSV, _config.PlayerHiHSV, playerMask); - using var wallMask = BuildWallMask(satChan, valueChan, playerMask); + using var wallMask = BuildWallMask(hsv, playerMask); // Colorized classified (walls + player) using var classified = new Mat(_config.CaptureSize, _config.CaptureSize, MatType.CV_8UC3, Scalar.Black); diff --git a/src/Poe2Trade.Navigation/NavigationTypes.cs b/src/Poe2Trade.Navigation/NavigationTypes.cs index feb0a7c..39b3bb3 100644 --- a/src/Poe2Trade.Navigation/NavigationTypes.cs +++ b/src/Poe2Trade.Navigation/NavigationTypes.cs @@ -70,13 +70,10 @@ public class MinimapConfig public Scalar PlayerLoHSV { get; set; } = new(5, 80, 80); public Scalar PlayerHiHSV { get; set; } = new(25, 255, 255); - // Wall detection: bright OR saturated pixels (minimap structure lines, icons) - public int WallMinValue { get; set; } = 200; - public int WallMinSat { get; set; } = 150; - - // Nameplate filter: exclude bright + unsaturated pixels (white text like "SALVAGE BENCH") - public int NameplateMinValue { get; set; } = 230; - public int NameplateMaxSat { get; set; } = 40; + // Wall detection: target #A2AEE5 (blue-lavender structure lines) + // HSV(115, 75, 229) — blue hue, low saturation, bright + public Scalar WallLoHSV { get; set; } = new(100, 20, 150); + public Scalar WallHiHSV { get; set; } = new(130, 140, 255); // Connected components: minimum area to keep (kills speckle) public int WallMinArea { get; set; } = 30; @@ -116,6 +113,10 @@ public class MinimapConfig // Frame dedup: min changed pixels to process a frame (skip near-identical minimap frames) public int FrameChangeThreshold { get; set; } = 50; + // Noise gate: skip frames where wall pixels exceed this fraction of capture area + // Normal walls ~2-5%, waypoint glow / effects spike to 10%+ + public double WallMaxDensity { get; set; } = 0.08; + // Stuck detection public double StuckThreshold { get; set; } = 2.0; public int StuckFrameCount { get; set; } = 5; diff --git a/src/Poe2Trade.Navigation/WorldMap.cs b/src/Poe2Trade.Navigation/WorldMap.cs index 56a01a9..bd526f4 100644 --- a/src/Poe2Trade.Navigation/WorldMap.cs +++ b/src/Poe2Trade.Navigation/WorldMap.cs @@ -33,6 +33,17 @@ public class WorldMap : IDisposable var sw = Stopwatch.StartNew(); _frameCount++; + // Noise gate: skip frames with too many wall pixels (waypoint glow, effects) + var wallPixels = Cv2.CountNonZero(wallMask); + var totalPixels = wallMask.Width * wallMask.Height; + var density = (double)wallPixels / totalPixels; + if (density > _config.WallMaxDensity) + { + Log.Information("Noise gate: {Density:P1} wall density ({Pixels} px), skipping ({Ms:F1}ms)", + density, wallPixels, sw.Elapsed.TotalMilliseconds); + return _position; + } + // Frame deduplication: skip if minimap hasn't scrolled yet if (_prevWallMask != null && _frameCount > 1) { @@ -173,27 +184,42 @@ public class WorldMap : IDisposable var confRoi = new Mat(_confidence, dstRect); var confInc = (short)_config.ConfidenceInc; + var confDec = (short)_config.ConfidenceDec; var confThreshold = (short)_config.ConfidenceThreshold; var confMax = (short)_config.ConfidenceMax; - // Only increment confidence for wall pixels. Don't decay on non-wall pixels because - // temporal smoothing (3/5 vote) kills walls during movement — the minimap scrolls so - // wall pixels shift across frames and fail the vote. "Not wall" in the smoothed mat - // during movement means "couldn't confirm" not "definitely not a wall". + // Wall pixels: increase confidence. Non-wall pixels in visible area: decay confidence. + // Real walls accumulate high confidence (40) and survive brief non-confirmation during + // movement. Transient noise (waypoint glow, effects) only reaches moderate confidence + // and gets removed as it decays. for (var row = 0; row < h; row++) for (var col = 0; col < w; col++) { var srcVal = srcRoi.At(row, col); - if (srcVal != (byte)MapCell.Wall) continue; - var conf = confRoi.At(row, col); - conf = boosted - ? confMax - : Math.Min((short)(conf + confInc), confMax); + + if (srcVal == (byte)MapCell.Wall) + { + conf = boosted + ? confMax + : Math.Min((short)(conf + confInc), confMax); + } + else if (conf > 0) + { + // Visible area, not a wall → slow decay + conf = Math.Max((short)(conf - confDec), (short)0); + } + else + { + continue; // nothing to update + } + confRoi.Set(row, col, conf); if (conf >= confThreshold) dstRoi.Set(row, col, (byte)MapCell.Wall); + else if (dstRoi.At(row, col) == (byte)MapCell.Wall) + dstRoi.Set(row, col, (byte)MapCell.Explored); // lost confidence → demote } // Mark explored area: circle around player, only overwrite Unknown