Как программисты ищут отличия

3e2a7be52b147c13e57e08a6ea2f305f.jpgЧасто за собой замечаю, что при виде какой-нибудь программы, игры или сайта у меня возникают странные мысли. И мысли эти меня пугают. А думаю я всякий раз о том, как эту программу/сайт/игру можно подхачить, взломать, обойти защиту, автоматизировать, расширить функциональность. Наверное, профессиональная деформация дает о себе знать. Или это подсознательное желание использовать накопленные знания, не находящие применения на работе. Как правило, эти желания остаются на уровне мыслей, но бывают исключения. Об одном таком случае я и расскажу вам сегодня…

Было это давно. Году, эдак, в 2008. Был обычный зимний день. Ничего не предвещало бессонной ночи. Но тут я заметил, как будущая жена играет на компе в одну игру…cb4c4a4507060f5ba8613a995f30427f.png

То была игра «Найди 5 отличий» (в оригинале »5 Spots»). При виде пользовательского интерфейса игры у меня сразу возникло вышеуказанное желание — «А можно ли написать программу, которая бы искала отличия и подсказывала игроку куда жать мышкой, а то и сама бы двигала ей и жала сама?». Как оказалось, возможно все.

Сама игра довольно старая и примитивная. Как видно из скриншота, она показывает 2 картинки с отличиями и ждет пока юзер прокликает их мышкой. Все просто. Такой подход избрал и я в своем решении:1. юзер запускает программу-подсказчика (ПП)2. запускает целевую игру3. жмет волшебную комбинацию клавиш4. в нужным местах картинки ПП подсвечивает различия

Мне нравится, когда программы разговаривают со мной: пишут логи, отчитываются о своих действиях, сообщают об ошибках. Тогда создается впечатление, что программа не бездушный сухой алгоритм, просто делающий свою работу, а живой организм. Он может быть молчаливым, изредка выводящим сообщения, либо разговорчивым, активно фигача в консоль…

В общем, я выбрал консольное приложение как основу для ПП. Зарегистрировал комбинацию горячих клавиш Ctrl + F1 (типа, «помощь»), повесил обработчик. Но как найти отличия в 2х картинках из игры? Для начала, картинки нужно было «увидеть» программно. Тут тоже все просто — «фотографируем» окно в фокусе в память по нажатию на горячие клавиши:

Фотографирование экрана HWND targetWindow = :: GetForegroundWindow (); HDC targetWindowDC = :: GetWindowDC (targetWindow); if (targetWindowDC!= NULL) { HDC memoryDC = :: CreateCompatibleDC (targetWindowDC); if (memoryDC!= NULL) { CRect targetWindowRectangle; :: GetWindowRect (targetWindow, &targetWindowRectangle);

HBITMAP memoryBitmap = :: CreateCompatibleBitmap (targetWindowDC, targetWindowRectangle.Width (), targetWindowRectangle.Height ()); if (memoryBitmap!= NULL) { :: SelectObject (memoryDC, memoryBitmap); :: BitBlt (memoryDC, 0, 0, targetWindowRectangle.Width (), targetWindowRectangle.Height (), targetWindowDC, 0, 0, SRCCOPY); Позиции картинок с отличиями в игре постоянные, размеры окна игры тоже — поэтому тут решает хардкод смещений и размеров (ведь наша ПП работает только с этой игрой). В памяти берем 2 картинки и «ксорим» их одна на другую:

XOR двух половинок #define BITMAP_WIDTH 375 #define BITMAP_HEIGHT 292

#define COORD_X_LEFT_IMAGE_UPPER_LEFT 19 #define COORD_Y_LEFT_IMAGE_UPPER_LEFT 152

#define COORD_X_RIGHT_IMAGE_UPPER_LEFT 405 #define COORD_Y_RIGHT_IMAGE_UPPER_LEFT COORD_Y_LEFT_IMAGE_UPPER_LEFT

:: BitBlt ( memoryDC, COORD_X_LEFT_IMAGE_UPPER_LEFT, COORD_Y_LEFT_IMAGE_UPPER_LEFT, BITMAP_WIDTH, BITMAP_HEIGHT, memoryDC, COORD_X_RIGHT_IMAGE_UPPER_LEFT, COORD_Y_RIGHT_IMAGE_UPPER_LEFT, SRCINVERT ); ВыXORивается следующая картина: 0f63e8f9fe0eedc9f254203690b224d8.png

А дальше начинается поиск отличий.

Сейчас, когда пишу эту статью, вспоминаю, что была у меня какая-то либо лаба, либо курсовой проект в универе на эту тему. На тему обработки похожих изображений. И там я написал этот алгоритм. Я прекрасно понимаю, что ничего нового не изобрел — скорее всего, у этого алгоритма даже есть какое-то специальное название. Да и не привязан он к изображениям вовсе. В общем, кто знает, что это, подскажите.

Итак, мы имеет черную картинку с нечерными пикселями в местах, где были отличия. Причем пиксели эти расположены не вплотную друг к другу, а, в общем случае, с какими-то промежутками. Но, как видно из скриншота, области отличий достаточно локализованы. Алгоритм поиска этих областей состоит в следующем:1. проходим по картинке2. находим нечерный пиксель3. смотрим в его окрестность и ищем его нечерных соседей — все это помещаем в найденную область (если рассматриваемые пиксели не были обработаны ранее)

Настраиваемым параметром тут служит «размер» окрестности пикселя — на сколько далеко можно от него заглядывать. Это позволяет искать более «размазанные» области отличий. Понятное дело, что все это неидеально и, в общем случае, найденных областей будет больше, чем отличий в картинках — ведь в самих картинках-заданиях возможен шум от сжатия, затесавшийся курсор мыши или что-то еще, выглядещее как различие на программном уровне, но незаметное с точки зрения игрока. Поэтому найденные различия нужно отсортировать по площади — чем больше нечерных пикселей вмещает область, тем больше вероятность того, что это не шум, а именно различие.

Уже потом я узнал и попробовал OpenCV (возможно, и о ней будет статья). Думаю, что есть более быстрые и оптимизированные алгоритмы. Но тогда меня хватило именно на такой вариант.

Исходник поиска различий (код старый, публикую без изменений):

Поиск различий #include «StdAfx.h» #include ».\bitmapinfo.h» #include

const CPixel CBitmapInfo: m_defaultPixel;

CBitmapInfo: CBitmapInfo (void) { m_uWidth = 0; m_uHeight = 0; }

CBitmapInfo::~CBitmapInfo (void) { Clear (); }

HRESULT CBitmapInfo: Clear () { m_uWidth = 0; m_uHeight = 0;

// Pixel clearing for (CPixelAreaIterator pixelAreaIterator = m_arPixels.begin (); pixelAreaIterator!= m_arPixels.end (); ++pixelAreaIterator) { delete (*pixelAreaIterator); } m_arPixels.clear ();

return S_OK; }

HRESULT CBitmapInfo: LoadBitmap (HDC hDC, const CRect &bitmapRect) { Clear ();

m_uWidth = bitmapRect.Width (); m_uHeight = bitmapRect.Height ();

m_arPixels.assign (m_uHeight * m_uWidth, NULL);

for (INT nPixelY = 0; nPixelY < m_uHeight; ++nPixelY) { for (INT nPixelX = 0; nPixelX < m_uWidth; ++nPixelX) { CPixel *pPixel = new CPixel(nPixelX, nPixelY, ::GetPixel(hDC, nPixelX + bitmapRect.left, nPixelY + bitmapRect.top)); SetPixel(nPixelX, nPixelY, pPixel); } }

return S_OK; }

HRESULT CBitmapInfo: GetPixelAreas (INT nPixelVicinityWidth, CPixelAreaList &arPixelAreaList) { arPixelAreaList.clear ();

if (m_uHeight > 0) { // Reinitialize all pixel reserved values (if needed) const CPixel *pFirstPixel = GetPixel (0, 0); if (pFirstPixel→IsValid () != FALSE && pFirstPixel→GetReserved () != CBitmapInfo: m_defaultPixel.GetReserved ()) { for (INT nPixelY = 0; nPixelY < m_uHeight; ++nPixelY) { for (INT nPixelX = 0; nPixelX < m_uWidth; ++nPixelX) { CPixel *pPixel = GetPixel(nPixelX, nPixelY); pPixel->SetReserved (-1); } } }

// Process pixels typedef stack CPixelStack;

// Look through all bitmap pixels const UINT uPixelCount = m_uWidth * m_uHeight; UINT uPixelAreaIndex = 0; for (INT nPixelY = 0; nPixelY < (INT)m_uHeight; ++nPixelY) { for (INT nPixelX = 0; nPixelX < (INT)m_uWidth; ++nPixelX) { CPixel *pPixel = GetPixel(nPixelX, nPixelY);

// If this pixel is valid (belongs to bitmap) if (pPixel→IsValid () != FALSE) { // If this current pixel is not already processed if (pPixel→GetReserved () == CBitmapInfo: m_defaultPixel.GetReserved ()) { // Set this pixel as processed pPixel→SetReserved (uPixelAreaIndex);

// If this pixel matches localization criteria if (pPixel→GetColor () != COLOR_BITMAP_BACKGROUND) { // Add pixel to its area CPixelArea *pPixelArea = new CPixelArea (); pPixelArea→push_back (pPixel);

// Push pixel to its stack CPixelStack pixelStack; pixelStack.push (pPixel);

do { CPixel *pVicinityPixel = pixelStack.top (); pixelStack.pop ();

INT nStartingX = pVicinityPixel→GetX (); INT nStartingY = pVicinityPixel→GetY (); for (INT nVicinityY = nStartingY — nPixelVicinityWidth; nVicinityY <= nStartingY + nPixelVicinityWidth; ++nVicinityY) { for (INT nVicinityX = nStartingX - nPixelVicinityWidth; nVicinityX <= nStartingX + nPixelVicinityWidth; ++nVicinityX) { pVicinityPixel = GetPixel(nVicinityX, nVicinityY);

// If this pixel is valid (belongs to bitmap) if (pVicinityPixel→IsValid () != FALSE) { // If this current pixel is not already processed if (pVicinityPixel→GetReserved () == CBitmapInfo: m_defaultPixel.GetReserved ()) { // Set this pixel as processed pVicinityPixel→SetReserved (uPixelAreaIndex);

// If this pixel matches localization criteria if (pVicinityPixel→GetColor () != COLOR_BITMAP_BACKGROUND) { pPixelArea→push_back (pVicinityPixel); pixelStack.push (pVicinityPixel); } } } } } } while (pixelStack.size () > 0);

arPixelAreaList.push_back (pPixelArea); ++uPixelAreaIndex; } } } } } }

return S_OK; } Дальше еще проще — подсветить найденные области на экране. Так как программа игры не использует никаких DirectX’ов (на сколько я могу судить), то тут помог простой вывод графики на окно игры. В общем-то, если бы был DirectX, то так просто «сфоткать» экран не получилось бы, не говоря уже о подсветке различий поверх игры. Но тут WinAPI рулит (функция :: Rectangle ()). Результат подсветки: 42e984606086f4a16273e22e0889a81b.png

От полностью программной игры пришлось отказаться — ПП и так слишком облегчала игру, если бы она еще и за тебя играла, то было бы вообще неинтересно. Но докрутить ПП до бота проще простого — зная координаты областей-отличий можно прокликать их мышкой, дождаться следующего уровня, распознать отличия и так далее…

Это все возможно, но, судя по всему, тогда меня хватило только на одну бессонную ночь.

© Habrahabr.ru