This commit is contained in:
Boki 2026-02-13 13:01:55 -05:00
parent 6bc3fb6972
commit 40f013d07e
5 changed files with 129 additions and 129 deletions

View file

@ -7,14 +7,100 @@ public class WorldMap : IDisposable
{
private readonly MinimapConfig _config;
private readonly Mat _canvas;
private MapPosition _position;
private int _frameCount;
public MapPosition Position => _position;
public WorldMap(MinimapConfig config)
{
_config = config;
_canvas = new Mat(config.CanvasSize, config.CanvasSize, MatType.CV_8UC1, Scalar.Black);
_position = new MapPosition(config.CanvasSize / 2.0, config.CanvasSize / 2.0);
}
public void StitchFrame(Mat classifiedMat, MapPosition position)
/// <summary>
/// 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)
{
_frameCount++;
// First frame: just stitch at center
if (_frameCount <= 1)
{
Stitch(classifiedMat, _position);
return _position;
}
// Match wallMask against canvas to find best position
var matched = MatchPosition(wallMask, _position);
if (matched != null)
_position = matched;
Stitch(classifiedMat, _position);
return _position;
}
private MapPosition? MatchPosition(Mat wallMask, MapPosition estimate)
{
var frameSize = wallMask.Width;
var searchPad = _config.MatchSearchRadius;
var searchSize = frameSize + 2 * searchPad;
var cx = (int)Math.Round(estimate.X);
var cy = (int)Math.Round(estimate.Y);
// Search region on canvas (centered on estimate)
var sx = cx - searchSize / 2;
var sy = cy - searchSize / 2;
// Clamp to canvas bounds
var sx0 = Math.Max(0, sx);
var sy0 = Math.Max(0, sy);
var sx1 = Math.Min(_config.CanvasSize, sx + searchSize);
var sy1 = Math.Min(_config.CanvasSize, sy + searchSize);
var sw = sx1 - sx0;
var sh = sy1 - sy0;
// Search region must be larger than template
if (sw <= frameSize || sh <= frameSize)
return null;
// Extract search ROI and convert to binary wall mask
using var searchRoi = new Mat(_canvas, new Rect(sx0, sy0, sw, sh));
using var canvasWalls = new Mat();
Cv2.Compare(searchRoi, new Scalar((byte)MapCell.Wall), canvasWalls, CmpType.EQ);
// Check if canvas has enough walls to match against
var canvasWallCount = Cv2.CountNonZero(canvasWalls);
if (canvasWallCount < 50)
return null;
// Template match: find where frame's walls best align with canvas walls
using var result = new Mat();
Cv2.MatchTemplate(canvasWalls, wallMask, result, TemplateMatchModes.CCoeffNormed);
Cv2.MinMaxLoc(result, out _, out var maxVal, out _, out var maxLoc);
if (maxVal < _config.MatchConfidence)
{
Log.Debug("Map match low confidence: {Conf:F3}", maxVal);
return null;
}
// Convert match position to world coordinates
// maxLoc is the top-left of the template in the search ROI
var matchX = sx0 + maxLoc.X + frameSize / 2.0;
var matchY = sy0 + maxLoc.Y + frameSize / 2.0;
Log.Debug("Map match: ({X:F1}, {Y:F1}) conf={Conf:F3} walls={Walls}",
matchX, matchY, maxVal, canvasWallCount);
return new MapPosition(matchX, matchY);
}
private void Stitch(Mat classifiedMat, MapPosition position)
{
var halfSize = _config.CaptureSize / 2;
var canvasX = (int)Math.Round(position.X) - halfSize;
@ -72,7 +158,6 @@ public class WorldMap : IDisposable
var cx = (int)Math.Round(pos.X);
var cy = (int)Math.Round(pos.Y);
// Scan in angular sectors to find direction with most Unknown cells at frontier
var bestAngle = double.NaN;
var bestScore = 0;
const int sectorCount = 16;
@ -83,7 +168,6 @@ public class WorldMap : IDisposable
var angle = 2 * Math.PI * sector / sectorCount;
var score = 0;
// Sample along a cone in this direction, at the fog boundary and beyond
for (var r = fogRadius - 20; r <= fogRadius + searchRadius; r += 5)
{
for (var spread = -15; spread <= 15; spread += 5)
@ -132,28 +216,25 @@ public class WorldMap : IDisposable
var cy = (int)Math.Round(center.Y);
var half = viewSize / 2;
// Clamp viewport to canvas
var x0 = Math.Clamp(cx - half, 0, _config.CanvasSize - viewSize);
var y0 = Math.Clamp(cy - half, 0, _config.CanvasSize - viewSize);
var roi = new Mat(_canvas, new Rect(x0, y0, viewSize, viewSize));
// Colorize: Unknown=#0d1117, Explored=#1f4068, Wall=#3d2d1a
using var colored = new Mat(viewSize, viewSize, MatType.CV_8UC3, new Scalar(23, 17, 13)); // BGR #0d1117
using var colored = new Mat(viewSize, viewSize, MatType.CV_8UC3, new Scalar(23, 17, 13));
for (var r = 0; r < viewSize; r++)
for (var c = 0; c < viewSize; c++)
{
var v = roi.At<byte>(r, c);
if (v == (byte)MapCell.Explored)
colored.Set(r, c, new Vec3b(104, 64, 31)); // BGR #1f4068
colored.Set(r, c, new Vec3b(104, 64, 31));
else if (v == (byte)MapCell.Wall)
colored.Set(r, c, new Vec3b(26, 45, 61)); // BGR #3d2d1a
colored.Set(r, c, new Vec3b(26, 45, 61));
}
// Draw player dot
var px = cx - x0;
var py = cy - y0;
if (px >= 0 && px < viewSize && py >= 0 && py < viewSize)
Cv2.Circle(colored, new Point(px, py), 4, new Scalar(0, 140, 255), -1); // orange
Cv2.Circle(colored, new Point(px, py), 4, new Scalar(0, 140, 255), -1);
Cv2.ImEncode(".png", colored, out var buf);
return buf;
@ -162,6 +243,8 @@ public class WorldMap : IDisposable
public void Reset()
{
_canvas.SetTo(Scalar.Black);
_position = new MapPosition(_config.CanvasSize / 2.0, _config.CanvasSize / 2.0);
_frameCount = 0;
}
public void Dispose()