switching
This commit is contained in:
parent
490fb8bdba
commit
ec1f6274e3
5 changed files with 246 additions and 69 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue