namespace OcrDaemon; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; class DetectGridHandler { public object HandleDetectGrid(Request req) { if (req.Region == null) return new ErrorResponse("detect-grid requires region"); int minCell = req.MinCellSize > 0 ? req.MinCellSize : 20; int maxCell = req.MaxCellSize > 0 ? req.MaxCellSize : 70; bool debug = req.Debug; Bitmap bitmap = ScreenCapture.CaptureOrLoad(req.File, req.Region); int w = bitmap.Width; int h = bitmap.Height; var bmpData = bitmap.LockBits( new Rectangle(0, 0, w, h), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb ); byte[] pixels = new byte[bmpData.Stride * h]; Marshal.Copy(bmpData.Scan0, pixels, 0, pixels.Length); bitmap.UnlockBits(bmpData); int stride = bmpData.Stride; byte[] gray = new byte[w * h]; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { int i = y * stride + x * 4; gray[y * w + x] = (byte)((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3); } bitmap.Dispose(); // ── Pass 1: Scan horizontal bands using "very dark pixel density" ── // Grid lines are nearly all very dark (density ~0.9), cell interiors are // partially dark (0.3-0.5), game world is mostly bright (density ~0.05). // This creates clear periodic peaks at grid line positions. int bandH = 200; int bandStep = 40; const int veryDarkPixelThresh = 12; // pixels below this brightness = "very dark" const double gridSegThresh = 0.25; // density above this = potential grid column var candidates = new List<(int bandY, int cellW, double hAc, int hLeft, int hRight)>(); for (int by = 0; by + bandH <= h; by += bandStep) { // "Very dark pixel density" per column: fraction of pixels below threshold double[] darkDensity = new double[w]; for (int x = 0; x < w; x++) { int count = 0; for (int y = by; y < by + bandH; y++) { if (gray[y * w + x] < veryDarkPixelThresh) count++; } darkDensity[x] = (double)count / bandH; } // Find segments where density > gridSegThresh (grid panel regions) var gridSegs = SignalProcessing.FindDarkDensitySegments(darkDensity, gridSegThresh, 200); foreach (var (segLeft, segRight) in gridSegs) { // Extract segment and run AC int segLen = segRight - segLeft; double[] segment = new double[segLen]; Array.Copy(darkDensity, segLeft, segment, 0, segLen); var (period, acScore) = SignalProcessing.FindPeriodWithScore(segment, minCell, maxCell); if (period <= 0) continue; // FindGridExtent within the segment var (extLeft, extRight) = SignalProcessing.FindGridExtent(segment, period); if (extLeft < 0) continue; // Map back to full image coordinates int absLeft = segLeft + extLeft; int absRight = segLeft + extRight; int extent = absRight - absLeft; // Require at least 8 cells wide AND 200px absolute minimum if (extent < period * 8 || extent < 200) continue; if (debug) Console.Error.WriteLine( $" Band y={by}: seg=[{segLeft}-{segRight}] period={period}, AC={acScore:F3}, " + $"extent={absLeft}-{absRight}={extent}px ({extent / period} cells)"); candidates.Add((by, period, acScore, absLeft, absRight)); } } if (debug) Console.Error.WriteLine($"Pass 1: {candidates.Count} candidates"); // Sort by score = AC * extent (prefer large strongly-periodic areas) candidates.Sort((a, b) => { double sa = a.hAc * (a.hRight - a.hLeft); double sb = b.hAc * (b.hRight - b.hLeft); return sb.CompareTo(sa); }); // ── Pass 2: Verify vertical periodicity ── foreach (var cand in candidates.Take(10)) { int colSpan = cand.hRight - cand.hLeft; if (colSpan < cand.cellW * 3) continue; // Row "very dark pixel density" within the detected column range double[] rowDensity = new double[h]; for (int y = 0; y < h; y++) { int count = 0; for (int x = cand.hLeft; x < cand.hRight; x++) { if (gray[y * w + x] < veryDarkPixelThresh) count++; } rowDensity[y] = (double)count / colSpan; } // Find grid panel vertical segment var vGridSegs = SignalProcessing.FindDarkDensitySegments(rowDensity, gridSegThresh, 100); if (vGridSegs.Count == 0) continue; // Use the largest segment var (vSegTop, vSegBottom) = vGridSegs.OrderByDescending(s => s.end - s.start).First(); int vSegLen = vSegBottom - vSegTop; double[] vSegment = new double[vSegLen]; Array.Copy(rowDensity, vSegTop, vSegment, 0, vSegLen); var (cellH, vAc) = SignalProcessing.FindPeriodWithScore(vSegment, minCell, maxCell); if (cellH <= 0) continue; var (extTop, extBottom) = SignalProcessing.FindGridExtent(vSegment, cellH); if (extTop < 0) continue; int top = vSegTop + extTop; int bottom = vSegTop + extBottom; int vExtent = bottom - top; // Require at least 3 rows tall AND 100px absolute minimum if (vExtent < cellH * 3 || vExtent < 100) continue; if (debug) Console.Error.WriteLine( $" 2D candidate: cellW={cand.cellW}, cellH={cellH}, " + $"region=({cand.hLeft},{top})-({cand.hRight},{bottom}), " + $"vAC={vAc:F3}, extent={vExtent}px ({vExtent / cellH} rows)"); // ── Found a valid 2D grid ── int gridW = cand.hRight - cand.hLeft; int gridH = bottom - top; int cols = Math.Max(2, (int)Math.Round((double)gridW / cand.cellW)); int rows = Math.Max(2, (int)Math.Round((double)gridH / cellH)); // Snap grid dimensions to exact multiples of cell size gridW = cols * cand.cellW; gridH = rows * cellH; if (debug) Console.Error.WriteLine( $" => cols={cols}, rows={rows}, gridW={gridW}, gridH={gridH}"); return new DetectGridResponse { Detected = true, Region = new RegionRect { X = req.Region.X + cand.hLeft, Y = req.Region.Y + top, Width = gridW, Height = gridH, }, Cols = cols, Rows = rows, CellWidth = Math.Round((double)gridW / cols, 1), CellHeight = Math.Round((double)gridH / rows, 1), }; } if (debug) Console.Error.WriteLine(" No valid 2D grid found"); return new DetectGridResponse { Detected = false }; } }