385 lines
14 KiB
C#
385 lines
14 KiB
C#
namespace Poe2Trade.Screen;
|
|
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using System.Runtime.InteropServices;
|
|
using Serilog;
|
|
using Region = Poe2Trade.Core.Region;
|
|
|
|
class DiffCropHandler
|
|
{
|
|
private Bitmap? _referenceFrame;
|
|
private Region? _referenceRegion;
|
|
private readonly object _refLock = new();
|
|
|
|
public void HandleSnapshot(string? file = null, Region? region = null)
|
|
{
|
|
var newFrame = ScreenCapture.CaptureOrLoad(file, region);
|
|
lock (_refLock)
|
|
{
|
|
_referenceFrame?.Dispose();
|
|
_referenceFrame = newFrame;
|
|
_referenceRegion = region;
|
|
}
|
|
}
|
|
|
|
public void HandleScreenshot(string path, Region? region = null)
|
|
{
|
|
Bitmap? refCopy;
|
|
lock (_refLock) { refCopy = _referenceFrame != null ? (Bitmap)_referenceFrame.Clone() : null; }
|
|
var bitmap = refCopy ?? ScreenCapture.CaptureOrLoad(null, region);
|
|
var format = ImageUtils.GetImageFormat(path);
|
|
var dir = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
|
Directory.CreateDirectory(dir);
|
|
bitmap.Save(path, format);
|
|
bitmap.Dispose();
|
|
}
|
|
|
|
public byte[] HandleCapture(Region? region = null)
|
|
{
|
|
using var bitmap = ScreenCapture.CaptureOrLoad(null, region);
|
|
using var ms = new MemoryStream();
|
|
bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
|
|
return ms.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Diff detection + crop only. Returns the raw tooltip crop bitmap and region,
|
|
/// or null if no tooltip detected. Caller is responsible for disposing the bitmaps.
|
|
/// </summary>
|
|
public (Bitmap cropped, Bitmap refCropped, Bitmap current, Region region)? DiffCrop(
|
|
DiffCropParams c, string? file = null, Region? region = null)
|
|
{
|
|
Bitmap refSnapshot;
|
|
Region? refRegion;
|
|
lock (_refLock)
|
|
{
|
|
if (_referenceFrame == null)
|
|
return null;
|
|
refSnapshot = (Bitmap)_referenceFrame.Clone();
|
|
refRegion = _referenceRegion;
|
|
}
|
|
|
|
var diffRegion = region ?? refRegion;
|
|
int baseX = diffRegion?.X ?? 0;
|
|
int baseY = diffRegion?.Y ?? 0;
|
|
var current = ScreenCapture.CaptureOrLoad(file, diffRegion);
|
|
|
|
Bitmap refForDiff = refSnapshot;
|
|
|
|
if (diffRegion != null)
|
|
{
|
|
if (refRegion == null)
|
|
{
|
|
var croppedRef = CropBitmap(refSnapshot, diffRegion);
|
|
if (croppedRef == null)
|
|
{
|
|
current.Dispose();
|
|
refSnapshot.Dispose();
|
|
return null;
|
|
}
|
|
refForDiff = croppedRef;
|
|
}
|
|
else if (!RegionsEqual(diffRegion, refRegion))
|
|
{
|
|
int offX = diffRegion.X - refRegion.X;
|
|
int offY = diffRegion.Y - refRegion.Y;
|
|
if (offX < 0 || offY < 0 || offX + diffRegion.Width > refSnapshot.Width || offY + diffRegion.Height > refSnapshot.Height)
|
|
{
|
|
current.Dispose();
|
|
refSnapshot.Dispose();
|
|
return null;
|
|
}
|
|
var croppedRef = CropBitmap(refSnapshot, new Region(offX, offY, diffRegion.Width, diffRegion.Height));
|
|
if (croppedRef == null)
|
|
{
|
|
current.Dispose();
|
|
refSnapshot.Dispose();
|
|
return null;
|
|
}
|
|
refForDiff = croppedRef;
|
|
}
|
|
}
|
|
|
|
int w = Math.Min(refForDiff.Width, current.Width);
|
|
int h = Math.Min(refForDiff.Height, current.Height);
|
|
|
|
var refData = refForDiff.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
|
byte[] refPx = new byte[refData.Stride * h];
|
|
Marshal.Copy(refData.Scan0, refPx, 0, refPx.Length);
|
|
refForDiff.UnlockBits(refData);
|
|
int stride = refData.Stride;
|
|
|
|
var curData = current.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
|
byte[] curPx = new byte[curData.Stride * h];
|
|
Marshal.Copy(curData.Scan0, curPx, 0, curPx.Length);
|
|
current.UnlockBits(curData);
|
|
|
|
int diffThresh = c.DiffThresh;
|
|
|
|
// Pass 1: parallel row diff
|
|
int[] rowCounts = new int[h];
|
|
Parallel.For(0, h, y =>
|
|
{
|
|
int count = 0;
|
|
int rowOffset = y * stride;
|
|
for (int x = 0; x < w; x++)
|
|
{
|
|
int i = rowOffset + x * 4;
|
|
int darker = (refPx[i] - curPx[i]) + (refPx[i + 1] - curPx[i + 1]) + (refPx[i + 2] - curPx[i + 2]);
|
|
if (darker > diffThresh)
|
|
count++;
|
|
}
|
|
rowCounts[y] = count;
|
|
});
|
|
|
|
int totalChanged = 0;
|
|
for (int y = 0; y < h; y++) totalChanged += rowCounts[y];
|
|
|
|
if (totalChanged == 0)
|
|
{
|
|
current.Dispose();
|
|
if (refForDiff != refSnapshot) refForDiff.Dispose();
|
|
refSnapshot.Dispose();
|
|
return null;
|
|
}
|
|
|
|
int maxGap = c.MaxGap;
|
|
int rowThresh = w / c.RowThreshDiv;
|
|
int bestRowStart = 0, bestRowEnd = 0, bestRowLen = 0;
|
|
int curRowStart = -1, lastActiveRow = -1;
|
|
for (int y = 0; y < h; y++)
|
|
{
|
|
if (rowCounts[y] >= rowThresh)
|
|
{
|
|
if (curRowStart < 0) curRowStart = y;
|
|
lastActiveRow = y;
|
|
}
|
|
else if (curRowStart >= 0 && y - lastActiveRow > maxGap)
|
|
{
|
|
int len = lastActiveRow - curRowStart + 1;
|
|
if (len > bestRowLen) { bestRowStart = curRowStart; bestRowEnd = lastActiveRow; bestRowLen = len; }
|
|
curRowStart = -1;
|
|
}
|
|
}
|
|
if (curRowStart >= 0)
|
|
{
|
|
int len = lastActiveRow - curRowStart + 1;
|
|
if (len > bestRowLen) { bestRowStart = curRowStart; bestRowEnd = lastActiveRow; bestRowLen = len; }
|
|
}
|
|
|
|
// Pass 2: parallel column diff
|
|
int[] colCounts = new int[w];
|
|
int rowRangeLen = bestRowEnd - bestRowStart + 1;
|
|
if (rowRangeLen <= 200)
|
|
{
|
|
for (int y = bestRowStart; y <= bestRowEnd; y++)
|
|
{
|
|
int rowOffset = y * stride;
|
|
for (int x = 0; x < w; x++)
|
|
{
|
|
int i = rowOffset + x * 4;
|
|
int darker = (refPx[i] - curPx[i]) + (refPx[i + 1] - curPx[i + 1]) + (refPx[i + 2] - curPx[i + 2]);
|
|
if (darker > diffThresh)
|
|
colCounts[x]++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Parallel.For(bestRowStart, bestRowEnd + 1,
|
|
() => new int[w],
|
|
(y, _, localCols) =>
|
|
{
|
|
int rowOffset = y * stride;
|
|
for (int x = 0; x < w; x++)
|
|
{
|
|
int i = rowOffset + x * 4;
|
|
int darker = (refPx[i] - curPx[i]) + (refPx[i + 1] - curPx[i + 1]) + (refPx[i + 2] - curPx[i + 2]);
|
|
if (darker > diffThresh)
|
|
localCols[x]++;
|
|
}
|
|
return localCols;
|
|
},
|
|
localCols =>
|
|
{
|
|
for (int x = 0; x < w; x++)
|
|
Interlocked.Add(ref colCounts[x], localCols[x]);
|
|
});
|
|
}
|
|
|
|
int tooltipHeight = bestRowEnd - bestRowStart + 1;
|
|
int colThresh = tooltipHeight / c.ColThreshDiv;
|
|
|
|
int bestColStart = 0, bestColEnd = 0, bestColLen = 0;
|
|
int curColStart = -1, lastActiveCol = -1;
|
|
for (int x = 0; x < w; x++)
|
|
{
|
|
if (colCounts[x] >= colThresh)
|
|
{
|
|
if (curColStart < 0) curColStart = x;
|
|
lastActiveCol = x;
|
|
}
|
|
else if (curColStart >= 0 && x - lastActiveCol > maxGap)
|
|
{
|
|
int len = lastActiveCol - curColStart + 1;
|
|
if (len > bestColLen) { bestColStart = curColStart; bestColEnd = lastActiveCol; bestColLen = len; }
|
|
curColStart = -1;
|
|
}
|
|
}
|
|
if (curColStart >= 0)
|
|
{
|
|
int len = lastActiveCol - curColStart + 1;
|
|
if (len > bestColLen) { bestColStart = curColStart; bestColEnd = lastActiveCol; bestColLen = len; }
|
|
}
|
|
|
|
Log.Debug("diff-crop: changed={Changed} rows={RowStart}-{RowEnd}({RowLen}) cols={ColStart}-{ColEnd}({ColLen})",
|
|
totalChanged, bestRowStart, bestRowEnd, bestRowLen, bestColStart, bestColEnd, bestColLen);
|
|
|
|
if (bestRowLen < 50 || bestColLen < 50)
|
|
{
|
|
Log.Debug("diff-crop: no tooltip-sized region found");
|
|
current.Dispose();
|
|
if (refForDiff != refSnapshot) refForDiff.Dispose();
|
|
refSnapshot.Dispose();
|
|
return null;
|
|
}
|
|
|
|
int minX = bestColStart;
|
|
int minY = bestRowStart;
|
|
int maxX = Math.Min(bestColEnd, w - 1);
|
|
int maxY = Math.Min(bestRowEnd, h - 1);
|
|
|
|
// Boundary extension
|
|
int extRowThresh = Math.Max(1, rowThresh / 4);
|
|
int extColThresh = Math.Max(1, colThresh / 4);
|
|
|
|
int extTop = Math.Max(0, minY - maxGap);
|
|
for (int y = minY - 1; y >= extTop; y--)
|
|
{
|
|
if (rowCounts[y] >= extRowThresh) minY = y;
|
|
else break;
|
|
}
|
|
int extBottom = Math.Min(h - 1, maxY + maxGap);
|
|
for (int y = maxY + 1; y <= extBottom; y++)
|
|
{
|
|
if (rowCounts[y] >= extRowThresh) maxY = y;
|
|
else break;
|
|
}
|
|
int extLeft = Math.Max(0, minX - maxGap);
|
|
for (int x = minX - 1; x >= extLeft; x--)
|
|
{
|
|
if (colCounts[x] >= extColThresh) minX = x;
|
|
else break;
|
|
}
|
|
int extRight = Math.Min(w - 1, maxX + maxGap);
|
|
for (int x = maxX + 1; x <= extRight; x++)
|
|
{
|
|
if (colCounts[x] >= extColThresh) maxX = x;
|
|
else break;
|
|
}
|
|
|
|
// Trim low-density edges
|
|
int colSpan = maxX - minX + 1;
|
|
if (colSpan > 50)
|
|
{
|
|
int q1 = minX + colSpan / 4;
|
|
int q3 = minX + colSpan * 3 / 4;
|
|
long midSum = 0;
|
|
int midCount = 0;
|
|
for (int x = q1; x <= q3; x++) { midSum += colCounts[x]; midCount++; }
|
|
double avgMidDensity = (double)midSum / Math.Max(1, midCount);
|
|
double cutoff = avgMidDensity * c.TrimCutoff;
|
|
|
|
while (minX < maxX - 50 && colCounts[minX] < cutoff)
|
|
minX++;
|
|
while (maxX > minX + 50 && colCounts[maxX] < cutoff)
|
|
maxX--;
|
|
}
|
|
|
|
int rowSpan = maxY - minY + 1;
|
|
if (rowSpan > 50)
|
|
{
|
|
int q1 = minY + rowSpan / 4;
|
|
int q3 = minY + rowSpan * 3 / 4;
|
|
long midSum = 0;
|
|
int midCount = 0;
|
|
for (int y = q1; y <= q3; y++) { midSum += rowCounts[y]; midCount++; }
|
|
double avgMidDensity = (double)midSum / Math.Max(1, midCount);
|
|
double cutoff = avgMidDensity * c.TrimCutoff;
|
|
|
|
while (minY < maxY - 50 && rowCounts[minY] < cutoff)
|
|
minY++;
|
|
while (maxY > minY + 50 && rowCounts[maxY] < cutoff)
|
|
maxY--;
|
|
}
|
|
int rw = maxX - minX + 1;
|
|
int rh = maxY - minY + 1;
|
|
|
|
var cropped = CropFromBytes(curPx, stride, minX, minY, rw, rh);
|
|
var refCropped = CropFromBytes(refPx, stride, minX, minY, rw, rh);
|
|
var resultRegion = new Region(baseX + minX, baseY + minY, rw, rh);
|
|
|
|
Log.Debug("diff-crop: tooltip region ({X},{Y}) {W}x{H}", minX, minY, rw, rh);
|
|
|
|
if (refForDiff != refSnapshot) refForDiff.Dispose();
|
|
refSnapshot.Dispose();
|
|
return (cropped, refCropped, current, resultRegion);
|
|
}
|
|
|
|
private static bool RegionsEqual(Region a, Region b) =>
|
|
a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height;
|
|
|
|
private static Bitmap? CropBitmap(Bitmap src, Region region)
|
|
{
|
|
int cx = Math.Max(0, region.X);
|
|
int cy = Math.Max(0, region.Y);
|
|
int cw = Math.Min(region.Width, src.Width - cx);
|
|
int ch = Math.Min(region.Height, src.Height - cy);
|
|
if (cw <= 0 || ch <= 0)
|
|
return null;
|
|
return src.Clone(new Rectangle(cx, cy, cw, ch), PixelFormat.Format32bppArgb);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fast crop from raw pixel bytes.
|
|
/// </summary>
|
|
private static Bitmap CropFromBytes(byte[] px, int srcStride, int cropX, int cropY, int cropW, int cropH)
|
|
{
|
|
var bmp = new Bitmap(cropW, cropH, PixelFormat.Format32bppArgb);
|
|
var data = bmp.LockBits(new Rectangle(0, 0, cropW, cropH), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
|
|
int dstStride = data.Stride;
|
|
int rowBytes = cropW * 4;
|
|
for (int y = 0; y < cropH; y++)
|
|
{
|
|
int srcOffset = (cropY + y) * srcStride + cropX * 4;
|
|
Marshal.Copy(px, srcOffset, data.Scan0 + y * dstStride, rowBytes);
|
|
}
|
|
bmp.UnlockBits(data);
|
|
return bmp;
|
|
}
|
|
|
|
public static double LevenshteinSimilarity(string a, string b)
|
|
{
|
|
a = a.ToLowerInvariant();
|
|
b = b.ToLowerInvariant();
|
|
if (a == b) return 1.0;
|
|
|
|
int la = a.Length, lb = b.Length;
|
|
if (la == 0 || lb == 0) return 0.0;
|
|
|
|
var d = new int[la + 1, lb + 1];
|
|
for (int i = 0; i <= la; i++) d[i, 0] = i;
|
|
for (int j = 0; j <= lb; j++) d[0, j] = j;
|
|
|
|
for (int i = 1; i <= la; i++)
|
|
for (int j = 1; j <= lb; j++)
|
|
{
|
|
int cost = a[i - 1] == b[j - 1] ? 0 : 1;
|
|
d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
|
|
}
|
|
|
|
return 1.0 - (double)d[la, lb] / Math.Max(la, lb);
|
|
}
|
|
}
|