work on color tracker
This commit is contained in:
parent
03d3fbd1dc
commit
86c7a31231
4 changed files with 176 additions and 6 deletions
|
|
@ -9,10 +9,12 @@ public class MinimapCapture : IDisposable
|
||||||
{
|
{
|
||||||
private readonly MinimapConfig _config;
|
private readonly MinimapConfig _config;
|
||||||
private readonly IScreenCapture _backend;
|
private readonly IScreenCapture _backend;
|
||||||
|
private readonly WallColorTracker _colorTracker;
|
||||||
|
|
||||||
public MinimapCapture(MinimapConfig config)
|
public MinimapCapture(MinimapConfig config)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_colorTracker = new WallColorTracker(config.WallLoHSV, config.WallHiHSV);
|
||||||
_backend = CreateBackend();
|
_backend = CreateBackend();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,8 +63,8 @@ public class MinimapCapture : IDisposable
|
||||||
Cv2.InRange(hsv, _config.PlayerLoHSV, _config.PlayerHiHSV, playerMask);
|
Cv2.InRange(hsv, _config.PlayerLoHSV, _config.PlayerHiHSV, playerMask);
|
||||||
var playerOffset = FindCentroid(playerMask);
|
var playerOffset = FindCentroid(playerMask);
|
||||||
|
|
||||||
// Wall mask: target #A2AEE5 blue-lavender structure lines
|
// Wall mask: target #A2AEE5 blue-lavender structure lines (range adapts per-map)
|
||||||
var wallMask = BuildWallMask(hsv, playerMask);
|
var wallMask = BuildWallMask(hsv, playerMask, sample: true);
|
||||||
|
|
||||||
// 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);
|
var classified = new Mat(_config.CaptureSize, _config.CaptureSize, MatType.CV_8UC1, Scalar.Black);
|
||||||
|
|
@ -82,18 +84,24 @@ public class MinimapCapture : IDisposable
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mat BuildWallMask(Mat hsv, Mat playerMask)
|
private Mat BuildWallMask(Mat hsv, Mat playerMask, bool sample = false)
|
||||||
{
|
{
|
||||||
// Target wall color #A2AEE5 — HSV(115, 75, 229)
|
// Use adapted range if available (narrows per-map), otherwise broad default
|
||||||
// This is map-independent: walls are always blue-lavender, fog is higher-saturation blue
|
var lo = _colorTracker.AdaptedLo ?? _config.WallLoHSV;
|
||||||
|
var hi = _colorTracker.AdaptedHi ?? _config.WallHiHSV;
|
||||||
|
|
||||||
var wallMask = new Mat();
|
var wallMask = new Mat();
|
||||||
Cv2.InRange(hsv, _config.WallLoHSV, _config.WallHiHSV, wallMask);
|
Cv2.InRange(hsv, lo, hi, wallMask);
|
||||||
|
|
||||||
// Subtract player marker (orange overlaps blue range slightly on some maps)
|
// Subtract player marker (orange overlaps blue range slightly on some maps)
|
||||||
using var notPlayer = new Mat();
|
using var notPlayer = new Mat();
|
||||||
Cv2.BitwiseNot(playerMask, notPlayer);
|
Cv2.BitwiseNot(playerMask, notPlayer);
|
||||||
Cv2.BitwiseAnd(wallMask, notPlayer, wallMask);
|
Cv2.BitwiseAnd(wallMask, notPlayer, wallMask);
|
||||||
|
|
||||||
|
// Sample from pure color-selected pixels BEFORE morphological ops
|
||||||
|
if (sample)
|
||||||
|
_colorTracker.SampleFrame(hsv, wallMask);
|
||||||
|
|
||||||
// Dilate to connect thin wall line fragments
|
// Dilate to connect thin wall line fragments
|
||||||
using var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3));
|
using var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3));
|
||||||
Cv2.Dilate(wallMask, wallMask, kernel);
|
Cv2.Dilate(wallMask, wallMask, kernel);
|
||||||
|
|
@ -218,6 +226,12 @@ public class MinimapCapture : IDisposable
|
||||||
return new Point2d(cx, cy);
|
return new Point2d(cx, cy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Commit pending wall color samples (call after successful template match).</summary>
|
||||||
|
public void CommitWallColors() => _colorTracker.Commit();
|
||||||
|
|
||||||
|
/// <summary>Reset adaptive color tracking (call on area change).</summary>
|
||||||
|
public void ResetAdaptation() => _colorTracker.Reset();
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_backend.Dispose();
|
_backend.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ public class NavigationExecutor : IDisposable
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_worldMap.Reset();
|
_worldMap.Reset();
|
||||||
|
_capture.ResetAdaptation();
|
||||||
_stopped = false;
|
_stopped = false;
|
||||||
_stuckCounter = 0;
|
_stuckCounter = 0;
|
||||||
_lastPosition = null;
|
_lastPosition = null;
|
||||||
|
|
@ -81,6 +82,8 @@ public class NavigationExecutor : IDisposable
|
||||||
|
|
||||||
SetState(NavigationState.Processing);
|
SetState(NavigationState.Processing);
|
||||||
var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask);
|
var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask);
|
||||||
|
if (_worldMap.LastMatchSucceeded)
|
||||||
|
_capture.CommitWallColors();
|
||||||
|
|
||||||
// Stuck detection: position hasn't moved enough over several frames
|
// Stuck detection: position hasn't moved enough over several frames
|
||||||
if (_lastPosition != null)
|
if (_lastPosition != null)
|
||||||
|
|
@ -186,6 +189,8 @@ public class NavigationExecutor : IDisposable
|
||||||
|
|
||||||
var stitchStart = sw.Elapsed.TotalMilliseconds;
|
var stitchStart = sw.Elapsed.TotalMilliseconds;
|
||||||
var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask);
|
var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask);
|
||||||
|
if (_worldMap.LastMatchSucceeded)
|
||||||
|
_capture.CommitWallColors();
|
||||||
var stitchMs = sw.Elapsed.TotalMilliseconds - stitchStart;
|
var stitchMs = sw.Elapsed.TotalMilliseconds - stitchStart;
|
||||||
|
|
||||||
var renderStart = sw.Elapsed.TotalMilliseconds;
|
var renderStart = sw.Elapsed.TotalMilliseconds;
|
||||||
|
|
|
||||||
148
src/Poe2Trade.Navigation/WallColorTracker.cs
Normal file
148
src/Poe2Trade.Navigation/WallColorTracker.cs
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
using OpenCvSharp;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Poe2Trade.Navigation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks HSV distribution of confirmed wall pixels and computes an adaptive
|
||||||
|
/// detection range that narrows per-map from the broad default.
|
||||||
|
/// </summary>
|
||||||
|
internal class WallColorTracker
|
||||||
|
{
|
||||||
|
private readonly Scalar _defaultLo;
|
||||||
|
private readonly Scalar _defaultHi;
|
||||||
|
|
||||||
|
// Cumulative histograms (committed samples only)
|
||||||
|
private readonly long[] _hHist = new long[180];
|
||||||
|
private readonly long[] _sHist = new long[256];
|
||||||
|
private readonly long[] _vHist = new long[256];
|
||||||
|
private long _totalSamples;
|
||||||
|
|
||||||
|
// Pending samples from the latest frame (not yet committed)
|
||||||
|
private readonly int[] _pendH = new int[180];
|
||||||
|
private readonly int[] _pendS = new int[256];
|
||||||
|
private readonly int[] _pendV = new int[256];
|
||||||
|
private int _pendCount;
|
||||||
|
|
||||||
|
private const int MinSamples = 3000;
|
||||||
|
private const double LoPercentile = 0.05;
|
||||||
|
private const double HiPercentile = 0.95;
|
||||||
|
private const int HPad = 5;
|
||||||
|
private const int SPad = 15;
|
||||||
|
private const int VPad = 10;
|
||||||
|
|
||||||
|
public Scalar? AdaptedLo { get; private set; }
|
||||||
|
public Scalar? AdaptedHi { get; private set; }
|
||||||
|
|
||||||
|
public WallColorTracker(Scalar defaultLo, Scalar defaultHi)
|
||||||
|
{
|
||||||
|
_defaultLo = defaultLo;
|
||||||
|
_defaultHi = defaultHi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sample wall pixel HSV values from the current frame into pending buffers.
|
||||||
|
/// Must be called with the pre-dilation wall mask (only pure color-selected pixels).
|
||||||
|
/// Does NOT commit — call <see cref="Commit"/> only after WorldMap confirms the match.
|
||||||
|
/// </summary>
|
||||||
|
public void SampleFrame(Mat hsv, Mat preDilationMask)
|
||||||
|
{
|
||||||
|
Array.Clear(_pendH);
|
||||||
|
Array.Clear(_pendS);
|
||||||
|
Array.Clear(_pendV);
|
||||||
|
_pendCount = 0;
|
||||||
|
|
||||||
|
// Downsample: every 4th pixel in each direction (~10K samples for 400x400)
|
||||||
|
for (var r = 0; r < hsv.Rows; r += 4)
|
||||||
|
for (var c = 0; c < hsv.Cols; c += 4)
|
||||||
|
{
|
||||||
|
if (preDilationMask.At<byte>(r, c) == 0) continue;
|
||||||
|
var px = hsv.At<Vec3b>(r, c);
|
||||||
|
_pendH[px.Item0]++;
|
||||||
|
_pendS[px.Item1]++;
|
||||||
|
_pendV[px.Item2]++;
|
||||||
|
_pendCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Commit pending samples into the cumulative histogram.
|
||||||
|
/// Call only after WorldMap confirms a successful template match.
|
||||||
|
/// </summary>
|
||||||
|
public void Commit()
|
||||||
|
{
|
||||||
|
if (_pendCount == 0) return;
|
||||||
|
|
||||||
|
for (var i = 0; i < 180; i++) _hHist[i] += _pendH[i];
|
||||||
|
for (var i = 0; i < 256; i++) _sHist[i] += _pendS[i];
|
||||||
|
for (var i = 0; i < 256; i++) _vHist[i] += _pendV[i];
|
||||||
|
_totalSamples += _pendCount;
|
||||||
|
|
||||||
|
if (_totalSamples >= MinSamples)
|
||||||
|
Recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Recompute()
|
||||||
|
{
|
||||||
|
var hLo = Percentile(_hHist, 180, LoPercentile);
|
||||||
|
var hHi = Percentile(_hHist, 180, HiPercentile);
|
||||||
|
var sLo = Percentile(_sHist, 256, LoPercentile);
|
||||||
|
var sHi = Percentile(_sHist, 256, HiPercentile);
|
||||||
|
var vLo = Percentile(_vHist, 256, LoPercentile);
|
||||||
|
var vHi = Percentile(_vHist, 256, HiPercentile);
|
||||||
|
|
||||||
|
// Clamp to default bounds — adaptation can only narrow, never broaden
|
||||||
|
var newLo = new Scalar(
|
||||||
|
Math.Max(_defaultLo.Val0, hLo - HPad),
|
||||||
|
Math.Max(_defaultLo.Val1, sLo - SPad),
|
||||||
|
Math.Max(_defaultLo.Val2, vLo - VPad));
|
||||||
|
var newHi = new Scalar(
|
||||||
|
Math.Min(_defaultHi.Val0, hHi + HPad),
|
||||||
|
Math.Min(_defaultHi.Val1, sHi + SPad),
|
||||||
|
Math.Min(_defaultHi.Val2, vHi + VPad));
|
||||||
|
|
||||||
|
// Safety: if any channel inverted (lo > hi), fall back to default for that channel
|
||||||
|
if (newLo.Val0 > newHi.Val0) { newLo.Val0 = _defaultLo.Val0; newHi.Val0 = _defaultHi.Val0; }
|
||||||
|
if (newLo.Val1 > newHi.Val1) { newLo.Val1 = _defaultLo.Val1; newHi.Val1 = _defaultHi.Val1; }
|
||||||
|
if (newLo.Val2 > newHi.Val2) { newLo.Val2 = _defaultLo.Val2; newHi.Val2 = _defaultHi.Val2; }
|
||||||
|
|
||||||
|
// Only log when the range actually changes
|
||||||
|
if (AdaptedLo == null || AdaptedHi == null ||
|
||||||
|
AdaptedLo.Value.Val0 != newLo.Val0 || AdaptedLo.Value.Val1 != newLo.Val1 ||
|
||||||
|
AdaptedHi.Value.Val0 != newHi.Val0 || AdaptedHi.Value.Val1 != newHi.Val1)
|
||||||
|
{
|
||||||
|
Log.Information(
|
||||||
|
"Wall color adapted: H({HLo}-{HHi}) S({SLo}-{SHi}) V({VLo}-{VHi}) from {Samples} samples",
|
||||||
|
newLo.Val0, newHi.Val0, newLo.Val1, newHi.Val1, newLo.Val2, newHi.Val2, _totalSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdaptedLo = newLo;
|
||||||
|
AdaptedHi = newHi;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Percentile(long[] hist, int size, double p)
|
||||||
|
{
|
||||||
|
var target = (long)(_totalSamples * p);
|
||||||
|
long cum = 0;
|
||||||
|
for (var i = 0; i < size; i++)
|
||||||
|
{
|
||||||
|
cum += hist[i];
|
||||||
|
if (cum >= target) return i;
|
||||||
|
}
|
||||||
|
return size - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Array.Clear(_hHist);
|
||||||
|
Array.Clear(_sHist);
|
||||||
|
Array.Clear(_vHist);
|
||||||
|
_totalSamples = 0;
|
||||||
|
Array.Clear(_pendH);
|
||||||
|
Array.Clear(_pendS);
|
||||||
|
Array.Clear(_pendV);
|
||||||
|
_pendCount = 0;
|
||||||
|
AdaptedLo = null;
|
||||||
|
AdaptedHi = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ public class WorldMap : IDisposable
|
||||||
private Mat? _prevWallMask; // for frame deduplication
|
private Mat? _prevWallMask; // for frame deduplication
|
||||||
|
|
||||||
public MapPosition Position => _position;
|
public MapPosition Position => _position;
|
||||||
|
public bool LastMatchSucceeded { get; private set; }
|
||||||
|
|
||||||
public WorldMap(MinimapConfig config)
|
public WorldMap(MinimapConfig config)
|
||||||
{
|
{
|
||||||
|
|
@ -94,12 +95,14 @@ public class WorldMap : IDisposable
|
||||||
if (matched == null)
|
if (matched == null)
|
||||||
{
|
{
|
||||||
_consecutiveMatchFails++;
|
_consecutiveMatchFails++;
|
||||||
|
LastMatchSucceeded = false;
|
||||||
Log.Information("MatchAndStitch: dedup={Dedup:F1}ms match={Match:F1}ms (FAILED x{Fails}) total={Total:F1}ms",
|
Log.Information("MatchAndStitch: dedup={Dedup:F1}ms match={Match:F1}ms (FAILED x{Fails}) total={Total:F1}ms",
|
||||||
dedupMs, matchMs, _consecutiveMatchFails, sw.Elapsed.TotalMilliseconds);
|
dedupMs, matchMs, _consecutiveMatchFails, sw.Elapsed.TotalMilliseconds);
|
||||||
return _position; // don't stitch — wrong position would corrupt the canvas
|
return _position; // don't stitch — wrong position would corrupt the canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
_consecutiveMatchFails = 0;
|
_consecutiveMatchFails = 0;
|
||||||
|
LastMatchSucceeded = true;
|
||||||
_position = matched;
|
_position = matched;
|
||||||
var stitchStart = sw.Elapsed.TotalMilliseconds;
|
var stitchStart = sw.Elapsed.TotalMilliseconds;
|
||||||
StitchWithConfidence(classifiedMat, _position, boosted: false);
|
StitchWithConfidence(classifiedMat, _position, boosted: false);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue