using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
using XFEWorkflow.Abstractions;
namespace XFEWorkflow.Plugins.Vision;
public static partial class ImageMatcher
{
// Every template pixel must be within the chosen per-channel tolerance.
// Anchor rejection is exact under that rule; no sampled-score false positives.
public static JsonArray Find(Bitmap source, Bitmap template, ScreenRegion origin, int tolerance, int maxMatches, CancellationToken ct)
{
if (tolerance is < 0 or > 255 || maxMatches is < 1 or > 100) throw new ArgumentException("图像容差或匹配数量超出范围。");
JsonArray matches = [];
var occupied = new List<ScreenRegion>();
if (template.Width > source.Width || template.Height > source.Height) return matches;
var screen = ReadPixels(source); var target = ReadPixels(template);
var anchors = (from y in new[] { 0, template.Height / 4, template.Height / 2, template.Height * 3 / 4, template.Height - 1 }
from x in new[] { 0, template.Width / 4, template.Width / 2, template.Width * 3 / 4, template.Width - 1 }
select (X: x, Y: y)).Distinct().ToArray();
bool PixelMatches(int a, int b) => Math.Abs(screen[a] - target[b]) <= tolerance && Math.Abs(screen[a + 1] - target[b + 1]) <= tolerance && Math.Abs(screen[a + 2] - target[b + 2]) <= tolerance;
var searchWidth = source.Width - template.Width; var searchHeight = source.Height - template.Height;
for (var y = 0; y <= searchHeight; y++)
{
ct.ThrowIfCancellationRequested();
for (var x = 0; x <= searchWidth; x++)
{
if ((x & 255) == 0) ct.ThrowIfCancellationRequested();
var overlaps = false;
foreach (var existing in occupied)
if (x + origin.X < existing.Right && x + origin.X + template.Width > existing.X && y + origin.Y < existing.Bottom && y + origin.Y + template.Height > existing.Y) { overlaps = true; break; }
if (overlaps) continue;
var valid = true;
foreach (var anchor in anchors)
{
if (PixelMatches(((y + anchor.Y) * source.Width + x + anchor.X) * 3, (anchor.Y * template.Width + anchor.X) * 3)) continue;
valid = false; break;
}
if (!valid) continue;
long error = 0;
for (var row = 0; row < template.Height && valid; row++)
{
ct.ThrowIfCancellationRequested();
for (var col = 0; col < template.Width; col++)
{
var a = ((y + row) * source.Width + x + col) * 3; var b = (row * template.Width + col) * 3;
if (!PixelMatches(a, b)) { valid = false; break; }
error += Math.Abs(screen[a] - target[b]) + Math.Abs(screen[a + 1] - target[b + 1]) + Math.Abs(screen[a + 2] - target[b + 2]);
}
}
if (!valid) continue;
var region = new ScreenRegion(origin.X + x, origin.Y + y, template.Width, template.Height);
occupied.Add(region);
var match = region.ToJson(); match["point"] = VisionParameters.Point(region.CenterX, region.CenterY);
match["score"] = Math.Round(1 - error / ((double)template.Width * template.Height * 3 * 255), 4); matches.Add(match);
if (matches.Count >= maxMatches) return matches;
}
}
return matches;
}
private static byte[] ReadPixels(Bitmap bitmap)
{
using var normalized = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb);
using (var graphics = Graphics.FromImage(normalized))
{
graphics.Clear(Color.White);
// DrawImageUnscaled preserves inches, not pixels, across DPI contexts.
graphics.DrawImage(bitmap, new Rectangle(0, 0, bitmap.Width, bitmap.Height), new Rectangle(0, 0, bitmap.Width, bitmap.Height), GraphicsUnit.Pixel);
}
var data = normalized.LockBits(new Rectangle(0, 0, normalized.Width, normalized.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
try
{
var bytes = new byte[checked(bitmap.Width * bitmap.Height * 3)];
for (var row = 0; row < bitmap.Height; row++) Marshal.Copy(data.Scan0 + row * data.Stride, bytes, row * bitmap.Width * 3, bitmap.Width * 3);
return bytes;
}
finally { normalized.UnlockBits(data); }
}
}
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
using XFEWorkflow.Abstractions;
namespace XFEWorkflow.Plugins.Vision;
public static partial class ImageMatcher
{
// Every template pixel must be within the chosen per-channel tolerance.
// Anchor rejection is exact under that rule; no sampled-score false positives.
public static JsonArray Find(Bitmap source, Bitmap template, ScreenRegion origin, int tolerance, int maxMatches, CancellationToken ct)
{
if (tolerance is < 0 or > 255 || maxMatches is < 1 or > 100) throw new ArgumentException("图像容差或匹配数量超出范围。");
JsonArray matches = [];
var occupied = new List<ScreenRegion>();
if (template.Width > source.Width || template.Height > source.Height) return matches;
var screen = ReadPixels(source); var target = ReadPixels(template);
var anchors = (from y in new[] { 0, template.Height / 4, template.Height / 2, template.Height * 3 / 4, template.Height - 1 }
from x in new[] { 0, template.Width / 4, template.Width / 2, template.Width * 3 / 4, template.Width - 1 }
select (X: x, Y: y)).Distinct().ToArray();
bool PixelMatches(int a, int b) => Math.Abs(screen[a] - target[b]) <= tolerance && Math.Abs(screen[a + 1] - target[b + 1]) <= tolerance && Math.Abs(screen[a + 2] - target[b + 2]) <= tolerance;
var searchWidth = source.Width - template.Width; var searchHeight = source.Height - template.Height;
for (var y = 0; y <= searchHeight; y++)
{
ct.ThrowIfCancellationRequested();
for (var x = 0; x <= searchWidth; x++)
{
if ((x & 255) == 0) ct.ThrowIfCancellationRequested();
var overlaps = false;
foreach (var existing in occupied)
if (x + origin.X < existing.Right && x + origin.X + template.Width > existing.X && y + origin.Y < existing.Bottom && y + origin.Y + template.Height > existing.Y) { overlaps = true; break; }
if (overlaps) continue;
var valid = true;
foreach (var anchor in anchors)
{
if (PixelMatches(((y + anchor.Y) * source.Width + x + anchor.X) * 3, (anchor.Y * template.Width + anchor.X) * 3)) continue;
valid = false; break;
}
if (!valid) continue;
long error = 0;
for (var row = 0; row < template.Height && valid; row++)
{
ct.ThrowIfCancellationRequested();
for (var col = 0; col < template.Width; col++)
{
var a = ((y + row) * source.Width + x + col) * 3; var b = (row * template.Width + col) * 3;
if (!PixelMatches(a, b)) { valid = false; break; }
error += Math.Abs(screen[a] - target[b]) + Math.Abs(screen[a + 1] - target[b + 1]) + Math.Abs(screen[a + 2] - target[b + 2]);
}
}
if (!valid) continue;
var region = new ScreenRegion(origin.X + x, origin.Y + y, template.Width, template.Height);
occupied.Add(region);
var match = region.ToJson(); match["point"] = VisionParameters.Point(region.CenterX, region.CenterY);
match["score"] = Math.Round(1 - error / ((double)template.Width * template.Height * 3 * 255), 4); matches.Add(match);
if (matches.Count >= maxMatches) return matches;
}
}
return matches;
}
private static byte[] ReadPixels(Bitmap bitmap)
{
using var normalized = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb);
using (var graphics = Graphics.FromImage(normalized))
{
graphics.Clear(Color.White);
// DrawImageUnscaled preserves inches, not pixels, across DPI contexts.
graphics.DrawImage(bitmap, new Rectangle(0, 0, bitmap.Width, bitmap.Height), new Rectangle(0, 0, bitmap.Width, bitmap.Height), GraphicsUnit.Pixel);
}
var data = normalized.LockBits(new Rectangle(0, 0, normalized.Width, normalized.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
try
{
var bytes = new byte[checked(bitmap.Width * bitmap.Height * 3)];
for (var row = 0; row < bitmap.Height; row++) Marshal.Copy(data.Scan0 + row * data.Stride, bytes, row * bitmap.Width * 3, bitmap.Width * 3);
return bytes;
}
finally { normalized.UnlockBits(data); }
}
}