switching

This commit is contained in:
Boki 2026-02-13 17:36:33 -05:00
parent 490fb8bdba
commit ec1f6274e3
5 changed files with 246 additions and 69 deletions

View file

@ -13,6 +13,8 @@ public class WorldMap : IDisposable
private int _frameCount;
private int _consecutiveMatchFails;
private Mat? _prevWallMask; // for frame deduplication
private bool _modeSwitchPending; // suppress blind bootstrap after mode switch
private bool _hasCanvasData; // true after first successful stitch
public MapPosition Position => _position;
public bool LastMatchSucceeded { get; private set; }
@ -29,21 +31,27 @@ public class WorldMap : IDisposable
/// Match current wall mask against the accumulated map to find position,
/// then stitch walls and paint explored area.
/// </summary>
public MapPosition MatchAndStitch(Mat classifiedMat, Mat wallMask)
public MapPosition MatchAndStitch(Mat classifiedMat, Mat wallMask, MinimapMode mode = MinimapMode.Overlay)
{
var sw = Stopwatch.StartNew();
_frameCount++;
var needsBootstrap = _frameCount <= _config.WarmupFrames || _consecutiveMatchFails >= 30;
var isCorner = mode == MinimapMode.Corner;
var warmupFrames = isCorner ? 2 : _config.WarmupFrames;
// After mode switch, don't blindly stitch — wait for template match to confirm alignment
var needsBootstrap = !_modeSwitchPending
&& (_frameCount <= warmupFrames || _consecutiveMatchFails >= 30);
// Block-based noise filter: zero out 50×50 blocks with >25% wall density
// Removes localized glow (waypoints, effects) while preserving real walls
var cleanFraction = FilterNoisyBlocks(wallMask, classifiedMat);
if (cleanFraction < 0.25 && !needsBootstrap)
// Block-based noise filter: only needed for overlay (game effects bleed through)
if (!isCorner)
{
Log.Information("Noise filter: {Clean:P0} clean, skipping ({Ms:F1}ms)",
cleanFraction, sw.Elapsed.TotalMilliseconds);
return _position;
var cleanFraction = FilterNoisyBlocks(wallMask, classifiedMat);
if (cleanFraction < 0.25 && !needsBootstrap)
{
Log.Information("Noise filter: {Clean:P0} clean, skipping ({Ms:F1}ms)",
cleanFraction, sw.Elapsed.TotalMilliseconds);
return _position;
}
}
// Frame deduplication: skip if minimap hasn't scrolled yet
@ -69,7 +77,8 @@ public class WorldMap : IDisposable
// Warmup / re-bootstrap: stitch at current position to seed the canvas
if (needsBootstrap)
{
StitchWithConfidence(classifiedMat, _position, boosted: true);
StitchWithConfidence(classifiedMat, _position, boosted: true, mode: mode);
_hasCanvasData = true;
if (_consecutiveMatchFails >= 30)
{
Log.Information("Re-bootstrap: stitching at current position after {Fails} match failures ({Ms:F1}ms)",
@ -93,16 +102,43 @@ 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",
dedupMs, matchMs, _consecutiveMatchFails, sw.Elapsed.TotalMilliseconds);
if (_modeSwitchPending)
{
// After 60 failures, give up on cross-mode alignment and force bootstrap
if (_consecutiveMatchFails >= 60)
{
Log.Information("Mode switch: giving up on alignment after {Fails} tries, force bootstrap",
_consecutiveMatchFails);
_modeSwitchPending = false;
_consecutiveMatchFails = 0;
_frameCount = 0; // will trigger warmup bootstrap next frame
}
else
{
Log.Information("Mode switch: match failed x{Fails}, waiting for alignment ({Ms:F1}ms)",
_consecutiveMatchFails, sw.Elapsed.TotalMilliseconds);
}
}
else
{
Log.Information("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
}
// Successful match — clear mode switch pending (alignment confirmed)
if (_modeSwitchPending)
{
Log.Information("Mode switch: alignment confirmed after {Fails} tries", _consecutiveMatchFails);
_modeSwitchPending = false;
}
_consecutiveMatchFails = 0;
LastMatchSucceeded = true;
_position = matched;
var stitchStart = sw.Elapsed.TotalMilliseconds;
StitchWithConfidence(classifiedMat, _position, boosted: false);
StitchWithConfidence(classifiedMat, _position, boosted: false, mode: mode);
var stitchMs = sw.Elapsed.TotalMilliseconds - stitchStart;
Log.Information("MatchAndStitch: dedup={Dedup:F1}ms match={Match:F1}ms stitch={Stitch:F1}ms total={Total:F1}ms",
@ -173,9 +209,12 @@ public class WorldMap : IDisposable
return new MapPosition(matchX, matchY);
}
private void StitchWithConfidence(Mat classifiedMat, MapPosition position, bool boosted)
private void StitchWithConfidence(Mat classifiedMat, MapPosition position, bool boosted,
MinimapMode mode = MinimapMode.Overlay)
{
var halfSize = _config.CaptureSize / 2;
var isCorner = mode == MinimapMode.Corner;
var frameSize = classifiedMat.Width;
var halfSize = frameSize / 2;
var canvasX = (int)Math.Round(position.X) - halfSize;
var canvasY = (int)Math.Round(position.Y) - halfSize;
@ -184,8 +223,8 @@ public class WorldMap : IDisposable
var srcY = Math.Max(0, -canvasY);
var dstX = Math.Max(0, canvasX);
var dstY = Math.Max(0, canvasY);
var w = Math.Min(_config.CaptureSize - srcX, _config.CanvasSize - dstX);
var h = Math.Min(_config.CaptureSize - srcY, _config.CanvasSize - dstY);
var w = Math.Min(frameSize - srcX, _config.CanvasSize - dstX);
var h = Math.Min(frameSize - srcY, _config.CanvasSize - dstY);
if (w <= 0 || h <= 0) return;
@ -196,15 +235,12 @@ public class WorldMap : IDisposable
var dstRoi = new Mat(_canvas, dstRect);
var confRoi = new Mat(_confidence, dstRect);
var confInc = (short)_config.ConfidenceInc;
// Corner minimap is clean — trust walls immediately, lower threshold
var confInc = isCorner ? (short)_config.ConfidenceMax : (short)_config.ConfidenceInc;
var confDec = (short)_config.ConfidenceDec;
var confThreshold = (short)_config.ConfidenceThreshold;
var confThreshold = isCorner ? (short)2 : (short)_config.ConfidenceThreshold;
var confMax = (short)_config.ConfidenceMax;
// 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++)
{
@ -219,45 +255,59 @@ public class WorldMap : IDisposable
}
else if (conf > 0)
{
// Visible area, not a wall → slow decay
conf = Math.Max((short)(conf - confDec), (short)0);
}
else
{
continue; // nothing to update
continue;
}
confRoi.Set(row, col, conf);
var current = dstRoi.At<byte>(row, col);
// Explored→Wall needs double evidence (protects already-walked areas from noise)
var needed = current == (byte)MapCell.Explored
// Corner mode: no double-evidence needed (clean data)
var needed = !isCorner && current == (byte)MapCell.Explored
? (short)(confThreshold * 2)
: confThreshold;
if (conf >= needed)
dstRoi.Set(row, col, (byte)MapCell.Wall);
else if (current == (byte)MapCell.Wall && conf < confThreshold)
dstRoi.Set(row, col, (byte)MapCell.Explored); // lost confidence → demote
dstRoi.Set(row, col, (byte)MapCell.Explored);
}
// Mark fog on canvas: only in a ring between ExploredRadius and ExploredRadius+5
var fogInner2 = _config.ExploredRadius * _config.ExploredRadius;
var fogOuter = _config.ExploredRadius + 5;
var fogOuter2 = fogOuter * fogOuter;
for (var row = 0; row < h; row++)
for (var col = 0; col < w; col++)
// Mark fog on canvas
if (isCorner)
{
if (srcRoi.At<byte>(row, col) != (byte)MapCell.Fog) continue;
// Corner minimap: fog is clean, accept all fog pixels
for (var row = 0; row < h; row++)
for (var col = 0; col < w; col++)
{
if (srcRoi.At<byte>(row, col) != (byte)MapCell.Fog) continue;
if (dstRoi.At<byte>(row, col) == (byte)MapCell.Unknown)
dstRoi.Set(row, col, (byte)MapCell.Fog);
}
}
else
{
// Overlay: restrict fog to ring around player (filters spell effect noise)
var fogInner2 = _config.ExploredRadius * _config.ExploredRadius;
var fogOuter = _config.ExploredRadius + 5;
var fogOuter2 = fogOuter * fogOuter;
var fx = srcX + col - halfSize;
var fy = srcY + row - halfSize;
var dist2 = fx * fx + fy * fy;
if (dist2 < fogInner2 || dist2 > fogOuter2) continue;
for (var row = 0; row < h; row++)
for (var col = 0; col < w; col++)
{
if (srcRoi.At<byte>(row, col) != (byte)MapCell.Fog) continue;
if (dstRoi.At<byte>(row, col) == (byte)MapCell.Unknown)
dstRoi.Set(row, col, (byte)MapCell.Fog);
var fx = srcX + col - halfSize;
var fy = srcY + row - halfSize;
var dist2 = fx * fx + fy * fy;
if (dist2 < fogInner2 || dist2 > fogOuter2) continue;
if (dstRoi.At<byte>(row, col) == (byte)MapCell.Unknown)
dstRoi.Set(row, col, (byte)MapCell.Fog);
}
}
// Mark explored area: circle around player, overwrite Unknown and Fog
@ -485,6 +535,20 @@ public class WorldMap : IDisposable
_position = new MapPosition(_config.CanvasSize / 2.0, _config.CanvasSize / 2.0);
_frameCount = 0;
_consecutiveMatchFails = 0;
_hasCanvasData = false;
}
/// <summary>
/// Re-bootstrap without clearing the canvas. Used when minimap mode switches
/// so new frames get stitched onto existing map data.
/// </summary>
public void Rebootstrap()
{
_prevWallMask?.Dispose();
_prevWallMask = null;
_frameCount = 0;
_consecutiveMatchFails = 0;
_modeSwitchPending = _hasCanvasData; // only suppress if canvas has data worth protecting
}
public void Dispose()