using OpenCvSharp; using Serilog; namespace Poe2Trade.Navigation; /// /// Tracks HSV distribution of confirmed wall pixels and computes an adaptive /// detection range that narrows per-map from the broad default. /// 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; // Generation counter: prevents committing samples from a previous mode after Reset() private int _generation; private int _pendGeneration; 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; } /// /// 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 only after WorldMap confirms the match. /// public void SampleFrame(Mat hsv, Mat preDilationMask) { Array.Clear(_pendH); Array.Clear(_pendS); Array.Clear(_pendV); _pendCount = 0; _pendGeneration = _generation; // 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(r, c) == 0) continue; var px = hsv.At(r, c); _pendH[px.Item0]++; _pendS[px.Item1]++; _pendV[px.Item2]++; _pendCount++; } } /// /// Commit pending samples into the cumulative histogram. /// Call only after WorldMap confirms a successful template match. /// public void Commit() { if (_pendCount == 0) return; if (_pendGeneration != _generation) return; // stale cross-mode samples 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; _generation++; } }