poe2-bot/tools/OcrDaemon/EdgeCropHandler.cs
2026-02-12 22:07:54 -05:00

205 lines
7.3 KiB
C#

namespace OcrDaemon;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
class EdgeCropHandler
{
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int X, Y; }
[DllImport("user32.dll")]
private static extern bool GetCursorPos(out POINT lpPoint);
public (Bitmap cropped, Bitmap fullCapture, RegionRect region)? EdgeCrop(Request req, EdgeCropParams p)
{
int cursorX, cursorY;
if (req.CursorX.HasValue && req.CursorY.HasValue)
{
cursorX = req.CursorX.Value;
cursorY = req.CursorY.Value;
}
else
{
GetCursorPos(out var pt);
cursorX = pt.X;
cursorY = pt.Y;
}
var fullCapture = ScreenCapture.CaptureOrLoad(req.File, null);
int w = fullCapture.Width;
int h = fullCapture.Height;
var bmpData = fullCapture.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] px = new byte[bmpData.Stride * h];
Marshal.Copy(bmpData.Scan0, px, 0, px.Length);
fullCapture.UnlockBits(bmpData);
int stride = bmpData.Stride;
int darkThresh = p.DarkThresh;
int colGap = p.RunGapTolerance;
int maxGap = p.MaxGap;
// ── Phase 1: Per-row horizontal extent ──
// Scan left/right from cursorX per row. Gap tolerance bridges through text.
// Percentile-based filtering for robustness.
int bandHalf = p.MinDarkRun; // repurpose: half-height of horizontal scan band
int bandTop = Math.Max(0, cursorY - bandHalf);
int bandBot = Math.Min(h - 1, cursorY + bandHalf);
var leftExtents = new List<int>();
var rightExtents = new List<int>();
for (int y = bandTop; y <= bandBot; y++)
{
int rowOff = y * stride;
int ci = rowOff + cursorX * 4;
int cBright = (px[ci] + px[ci + 1] + px[ci + 2]) / 3;
if (cBright >= darkThresh) continue;
int leftEdge = cursorX;
int gap = 0;
for (int x = cursorX - 1; x >= 0; x--)
{
int i = rowOff + x * 4;
int brightness = (px[i] + px[i + 1] + px[i + 2]) / 3;
if (brightness < darkThresh) { leftEdge = x; gap = 0; }
else if (++gap > colGap) break;
}
int rightEdge = cursorX;
gap = 0;
for (int x = cursorX + 1; x < w; x++)
{
int i = rowOff + x * 4;
int brightness = (px[i] + px[i + 1] + px[i + 2]) / 3;
if (brightness < darkThresh) { rightEdge = x; gap = 0; }
else if (++gap > colGap) break;
}
leftExtents.Add(leftEdge);
rightExtents.Add(rightEdge);
}
if (leftExtents.Count < 10)
{
Console.Error.WriteLine($" edge-crop: too few dark rows ({leftExtents.Count})");
fullCapture.Dispose();
return null;
}
leftExtents.Sort();
rightExtents.Sort();
// Use RowThreshDiv/ColThreshDiv as percentile denominators
// e.g., RowThreshDiv=4 → 25th percentile for left, ColThreshDiv=4 → 75th for right
int leftPctIdx = leftExtents.Count / p.RowThreshDiv;
int rightPctIdx = rightExtents.Count * (p.ColThreshDiv - 1) / p.ColThreshDiv;
leftPctIdx = Math.Clamp(leftPctIdx, 0, leftExtents.Count - 1);
rightPctIdx = Math.Clamp(rightPctIdx, 0, rightExtents.Count - 1);
int bestColStart = leftExtents[leftPctIdx];
int bestColEnd = rightExtents[rightPctIdx];
Console.Error.WriteLine($" edge-crop: horizontal: left={bestColStart} right={bestColEnd} ({bestColEnd - bestColStart + 1}px) samples={leftExtents.Count} pctL={leftPctIdx}/{leftExtents.Count} pctR={rightPctIdx}/{rightExtents.Count}");
if (bestColEnd - bestColStart + 1 < 50)
{
Console.Error.WriteLine($" edge-crop: horizontal extent too small");
fullCapture.Dispose();
return null;
}
// ── Phase 2: Per-column vertical extent ──
int colBandHalf = (bestColEnd - bestColStart + 1) / 3;
int colBandLeft = Math.Max(bestColStart, cursorX - colBandHalf);
int colBandRight = Math.Min(bestColEnd, cursorX + colBandHalf);
var topExtents = new List<int>();
var bottomExtents = new List<int>();
// Asymmetric gap: larger upward to bridge header decorations (~30-40px bright)
int maxGapUp = maxGap * 3;
for (int x = colBandLeft; x <= colBandRight; x++)
{
int ci = cursorY * stride + x * 4;
int cBright = (px[ci] + px[ci + 1] + px[ci + 2]) / 3;
if (cBright >= darkThresh) continue;
int topEdge = cursorY;
int gap = 0;
for (int y = cursorY - 1; y >= 0; y--)
{
int i = y * stride + x * 4;
int brightness = (px[i] + px[i + 1] + px[i + 2]) / 3;
if (brightness < darkThresh) { topEdge = y; gap = 0; }
else if (++gap > maxGapUp) break;
}
int bottomEdge = cursorY;
gap = 0;
for (int y = cursorY + 1; y < h; y++)
{
int i = y * stride + x * 4;
int brightness = (px[i] + px[i + 1] + px[i + 2]) / 3;
if (brightness < darkThresh) { bottomEdge = y; gap = 0; }
else if (++gap > maxGap) break;
}
topExtents.Add(topEdge);
bottomExtents.Add(bottomEdge);
}
if (topExtents.Count < 10)
{
Console.Error.WriteLine($" edge-crop: too few dark columns ({topExtents.Count})");
fullCapture.Dispose();
return null;
}
topExtents.Sort();
bottomExtents.Sort();
int topPctIdx = topExtents.Count / p.RowThreshDiv;
int botPctIdx = topExtents.Count * (p.ColThreshDiv - 1) / p.ColThreshDiv;
topPctIdx = Math.Clamp(topPctIdx, 0, topExtents.Count - 1);
botPctIdx = Math.Clamp(botPctIdx, 0, bottomExtents.Count - 1);
int bestRowStart = topExtents[topPctIdx];
int bestRowEnd = bottomExtents[botPctIdx];
Console.Error.WriteLine($" edge-crop: vertical: top={bestRowStart} bottom={bestRowEnd} ({bestRowEnd - bestRowStart + 1}px) samples={topExtents.Count}");
if (bestRowEnd - bestRowStart + 1 < 50)
{
Console.Error.WriteLine($" edge-crop: vertical extent too small");
fullCapture.Dispose();
return null;
}
int minX = bestColStart;
int minY = bestRowStart;
int maxX = bestColEnd;
int maxY = bestRowEnd;
int rw = maxX - minX + 1;
int rh = maxY - minY + 1;
Console.Error.WriteLine($" edge-crop: result ({minX},{minY}) {rw}x{rh}");
if (rw < 50 || rh < 50)
{
Console.Error.WriteLine($" edge-crop: region too small ({rw}x{rh})");
fullCapture.Dispose();
return null;
}
var cropRect = new Rectangle(minX, minY, rw, rh);
var cropped = fullCapture.Clone(cropRect, PixelFormat.Format32bppArgb);
var region = new RegionRect { X = minX, Y = minY, Width = rw, Height = rh };
return (cropped, fullCapture, region);
}
}