Распознавание изображения внутри изображения в С#
Я хотел бы найти изображение (иглу) в изображении (стог сена).
Чтобы все было просто, я делаю два скриншота своего рабочего стола. Один полный размер (стог сена) и крошечный (игла). Затем я просматриваю изображение сена и пытаюсь найти изображение иглы.
- снимок экрана захвата и стопки сена
- цикл через стог сена, глядя на стог сена [i] == первый пиксель иглы
- [if 2. is true:] прокрутите второй и последний пиксели иглы и сравните их с сторой сена [i]
Ожидаемый результат: изображение иглы находится в правильном месте.
Я уже работал над некоторыми координатами/ширинами/высотами (A).
Но иногда биты кажутся "выключенными", и поэтому совпадение не найдено (B).
Что я могу делать неправильно? Любые предложения приветствуются. Спасибо.
var needle_height = 25;
var needle_width = 25;
var haystack_height = 400;
var haystack_width = 500;
а. example input - match
var needle = screenshot(5, 3, needle_width, needle_height);
var haystack = screenshot(0, 0, haystack_width, haystack_height);
var result = findmatch(haystack, needle);
В. пример ввода - НЕТ соответствия
var needle = screenshot(5, 5, needle_width, needle_height);
var haystack = screenshot(0, 0, haystack_width, haystack_height);
var result = findmatch(haystack, needle);
1. изображение захвата и сеновальной рамы
private int[] screenshot(int x, int y, int width, int height)
{
Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
Graphics.FromImage(bmp).CopyFromScreen(x, y, 0, 0, bmp.Size);
var bmd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadOnly, bmp.PixelFormat);
var ptr = bmd.Scan0;
var bytes = bmd.Stride * bmp.Height / 4;
var result = new int[bytes];
Marshal.Copy(ptr, result, 0, bytes);
bmp.UnlockBits(bmd);
return result;
}
2. попробуйте найти совпадение
public Point findmatch(int[] haystack, int[] needle)
{
var firstpixel = needle[0];
for (int i = 0; i < haystack.Length; i++)
{
if (haystack[i] == firstpixel)
{
var y = i / haystack_height;
var x = i % haystack_width;
var matched = checkmatch(haystack, needle, x, y);
if (matched)
return (new Point(x,y));
}
}
return new Point();
}
3. проверить полное соответствие
public bool checkmatch(int[] haystack, int[] needle, int startx, int starty)
{
for (int y = starty; y < starty + needle_height; y++)
{
for (int x = startx; x < startx + needle_width; x++)
{
int haystack_index = y * haystack_width + x;
int needle_index = (y - starty) * needle_width + x - startx;
if (haystack[haystack_index] != needle[needle_index])
return false;
}
}
return true;
}
Ответы
Ответ 1
Во-первых, существует проблема с циклом findmatch
. Вы не должны просто использовать изображение сена в виде массива, потому что вам нужно вычесть ширину и высоту иглы справа и снизу соответственно:
public Point? findmatch(int[] haystack, int[] needle)
{
var firstpixel = needle[0];
for (int y = 0; y < haystack_height - needle_height; y++)
for (int x = 0; x < haystack_width - needle_width; x++)
{
if (haystack[y * haystack_width + x] == firstpixel)
{
var matched = checkmatch(haystack, needle, x, y);
if (matched)
return (new Point(x, y));
}
}
return null;
}
Это, вероятно, должно решить проблему. Кроме того, имейте в виду, что может быть несколько совпадений. Например, если "игла" представляет собой полностью белую прямоугольную часть окна, скорее всего, будет много совпадений на всем экране. Если это возможно, измените свой метод findmatch
, чтобы продолжить поиск результатов после обнаружения первого:
public IEnumerable<Point> FindMatches(int[] haystack, int[] needle)
{
var firstpixel = needle[0];
for (int y = 0; y < haystack_height - needle_height; y++)
for (int x = 0; x < haystack_width - needle_width; x++)
{
if (haystack[y * haystack_width + x] == firstpixel)
{
if (checkmatch(haystack, needle, x, y))
yield return (new Point(x, y));
}
}
}
Затем вам нужно сохранить привычку вручную удалять все объекты, которые реализуют IDisposable
, которые вы создали сами. Bitmap
и Graphics
являются такими объектами, что означает, что ваш метод screenshot
необходимо изменить, чтобы обернуть эти объекты в операторы using
:
private int[] screenshot(int x, int y, int width, int height)
{
// dispose 'bmp' after use
using (var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb))
{
// dispose 'g' after use
using (var g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(x, y, 0, 0, bmp.Size);
var bmd = bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadOnly,
bmp.PixelFormat);
var ptr = bmd.Scan0;
// as David pointed out, "bytes" might be
// a bit misleading name for a length of
// a 32-bit int array (so I've changed it to "len")
var len = bmd.Stride * bmp.Height / 4;
var result = new int[len];
Marshal.Copy(ptr, result, 0, len);
bmp.UnlockBits(bmd);
return result;
}
}
}
Остальная часть кода выглядит нормально, с замечанием о том, что она не будет очень эффективной для определенных входов. Например, у вас может быть большой сплошной цвет в качестве фона рабочего стола, что может привести к многочисленным вызовам checkmatch
.
Если вам интересна производительность, вы можете проверить разные способы ускорения поиска (что-то вроде измененного Rabin-Karp приходит в голову, но я уверен, что есть некоторые существующие алгоритмы, которые гарантируют немедленное пропущение недопустимых кандидатов).
Ответ 2
Вместо того, чтобы делать два скриншота рабочего стола с интервалом времени между ними, я бы сделал снимок экрана один раз и вырезал "иглу" и "стог сена" из того же самого растрового источника. В противном случае вы рискуете изменить содержимое рабочего стола между двумя моментами, когда будут сделаны снимки экрана.
EDIT: И когда ваша проблема по-прежнему возникает после этого, я попытаюсь сохранить изображение в файл и повторить попытку с этим файлом с помощью вашего отладчика, что даст вам воспроизводимую ситуацию.
Ответ 3
Я не думаю, что ваши уравнения для haystack_index
или needle_index
верны. Похоже, вы учитываете смещение Scan0
при копировании данных растрового изображения, но при вычислении позиции байта вам нужно использовать растровое изображение Stride
.
Кроме того, формат Format32bppArgb
использует 4 байта на пиксель. Похоже, вы принимаете 1 байт на пиксель.
Здесь сайт, который я использовал, чтобы помочь с этими уравнениями: http://www.bobpowell.net/lockingbits.htm
Format32BppArgb: с учетом координат X и Y адрес первого элемента в пиксель - Scan0 + (y * stepide) + (x * 4). Это указывает на синий байт. следующие три байта содержат зеленый, красный и альфа-байты.