XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWorkflow

【WinUI】XFE基于windows的工作流

公开
关注 0 Fork 0 Star 0
UTF-8
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); }
    }
}