最近和小徒弟玩QQ遊戲中的“美女找茬”,這個遊戲也就是給你兩幅差不多的圖檔,讓你找出幾個不同的地方(一般是五個)。可惜我老眼昏花比較反應遲鈍,總是輸,被小徒弟取笑。不禁一時心血來潮,既然作為普通玩家赢不了,何不...!于是我琢磨了一下,不過就是兩幅圖檔比較一下嗎,對計算機來說當然很簡單。也不需要考慮什麼算法。
是以我就做了這樣一個小程式,純屬貪圖好玩。我首先找到遊戲視窗,然後把這個視窗“截屏”下來,在記憶體裡判斷兩幅圖檔的不同之處,然後把結果輸出到一個半透明視窗上,并且把這個半透明視窗準确的覆寫到左側圖檔上。這個半透明視窗的背景是一個白色矩形,兩幅圖不同的地方用紅色填充出來。
判斷兩幅圖檔之前,我們做一些準備工作。先用SPY++查詢出遊戲視窗的視窗類和視窗标題。我在 Photoshop 裡精确測量了兩幅圖在視窗中的起始坐标和長度寬度,很慶幸的一點是在遊戲裡這些值是固定的!現在為了簡單起見,我們先把這些值 hard code 在程式裡(如果考慮更周到,我們應該把這些資訊存到一個 ini 配置檔案裡)。
那麼截圖并且找到不同處的代碼如下所示。為了考慮容錯性,我還嘗試了以 3*3 和 2*2 的像素矩形塊為基本機關進行檢測,但經過測試後我發現意義不大,實際上僅對單個像素進行檢測就足夠了,是以下面的代碼就是逐個像素檢測。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIml2ZuUWYxYmZhR2MiNjY3QGMjFGZlNTOzUWZjJGO2YzM1MWMfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.gif)
Code_FindDifference
//When user click the icon , then Let's GO !!!
BOOL FindDifference(HWND hParentDlg)
{
RECT rc, rc2;
//hide layer wnd
ShowWindow(m_wndLayer, SW_HIDE);
m_wndGame = FindWindow(CLASSNAME, WINDOWNAME);
if(m_wndGame == NULL)
{
MessageBox(hParentDlg, "沒找到遊戲視窗哇!還沒進入房間吧?", "幫你找茬", MB_OK |MB_ICONINFORMATION);
return FALSE;
}
GetWindowRect(m_wndGame, &rc);
GetWindowRect(m_wndLayer,&rc2);
int width = rc.right -rc.left;
int height = rc.bottom - rc.top;
int width2 = rc2.right - rc2.left;
int height2 = rc2.bottom - rc2.top;
HDC gameDC = GetDC(m_wndGame);
HDC layDC = GetDC(m_wndLayer);
HDC memDC = CreateCompatibleDC(gameDC);
HDC memDC2= CreateCompatibleDC(layDC);
//create game bitmap (for the first call)
if(m_bmGame == NULL)
m_bmGame = CreateCompatibleBitmap(gameDC, width, height);
if(m_bmLayer == NULL)
m_bmLayer = CreateCompatibleBitmap(layDC, width, height);
//把截圖複制到記憶體DC
HGDIOBJ hOld1 = SelectObject(memDC, m_bmGame);
HGDIOBJ hOld2 = SelectObject(memDC2, m_bmLayer);
BitBlt(memDC, 0 , 0, rc.right-rc.left, rc.bottom-rc.top,
gameDC, 0, 0, SRCCOPY);
//把圖層填充白色
Rectangle(memDC2, 0 , 0, width2, height2);
//現在我們從起始點查找
int i, j, diff;
BYTE r1, g1, b1, r2, g2, b2;
COLORREF color1, color2;
for(j = 0; j < IMG_HEIGHT; j++)
for(i = 0; i < IMG_WIDTH; i++)
{
//單像素檢測
color1 = GetPixel(memDC, IMG_X1 + i, IMG_Y + j);
color2 = GetPixel(memDC, IMG_X2 + i, IMG_Y + j);
r1 = GetRValue(color1);
g1 = GetGValue(color1);
b1 = GetBValue(color1);
r2 = GetRValue(color2);
g2 = GetGValue(color2);
b2 = GetBValue(color2);
diff = abs(r1-r2) + abs(g1-g2) + abs(b1-b2);
if(diff > m_Threshold)
{
SetPixel(memDC2, i, j, RGB(255, 0, 0));
}
}
SelectObject(memDC, hOld1);
SelectObject(memDC2, hOld2);
DeleteDC(memDC);
DeleteDC(memDC2);
ReleaseDC(m_wndGame, gameDC);
ReleaseDC(m_wndLayer, layDC);
//把視窗移動到指定位置
SetWindowPos(m_wndLayer, HWND_TOPMOST,
rc.left + IMG_X1, rc.top + IMG_Y, 0, 0,
SWP_NOACTIVATE | SWP_NOSIZE | SWP_SHOWWINDOW
);
InvalidateRect(m_wndLayer, NULL, TRUE);
return TRUE;
}
這個函數的效率不是很高,檢測不同的時候需要等大概一秒鐘的時間(我的電腦組態是 2.67GHz * 2 CPU, 3.24G 記憶體)。我想如果改進為直接用指針通路位圖的資料塊會比這種方法的效率好很多,是以它還可以改進,不過目前的處理時間對于我來說也還是可以接受的。
然後我們需要給使用者一個接口去調用上面的函數,是以我在通知欄(Tray:系統托盤)放置了一個圖示,隻要使用者用左鍵單擊通知欄圖示,就會調用上面的函數,也就是執行一次查找,并把半透明視窗和遊戲視窗進行對齊。通過滑鼠右鍵,可以選擇顯示或者隐藏半透明視窗。
【注意】如果遊戲視窗被其他視窗遮擋,或者遊戲視窗有螢幕以外,或者遊戲視窗上有動态元素(例如遊戲的倒計時提示等),請注意這時候擷取到的視窗截圖是有問題的,那麼查找結果也會出現不準确的情況。
如圖所示,我在左側圖上疊加了一個半透明視窗,它隻是一個普通對話框(上面什麼控件也沒有),當然我們還需要把這個半透明視窗設定成頂層視窗,還要使其“滑鼠穿透”,也就是說它自身不想接收滑鼠事件,而是讓滑鼠“穿透”它傳送給其下面的視窗,這是通過設定視窗樣式來完成的。在初始化對話框時,我們用下面的代碼即可:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIml2ZuUWYxYmZhR2MiNjY3QGMjFGZlNTOzUWZjJGO2YzM1MWMfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.gif)
Code_設定滑鼠穿透
//視窗過程局部:
switch(message)
case WM_INITDIALOG:
//設定圖層視窗,不透明度
SetWindowLong(hDlg, GWL_EXSTYLE,
GetWindowLong(hDlg, GWL_EXSTYLE) |
WS_EX_TRANSPARENT | //滑鼠穿透
WS_EX_LAYERED //圖層視窗
);
//BYTE alpha = 120;
SetLayeredWindowAttributes(hDlg,0, 120, LWA_ALPHA);
break;
//其他消息。。。
【注意】VC6.0提供的PLATFORM SDK(1998年的)并不支援圖層視窗相關的API,是以編譯時會提示找不到相應函數。要正确編譯,解決方法是在 winuser.h 檔案中補充相關的定義,并用高版本的Visual Studio(例如VS2003, VS2005 )中的 user32.lib 覆寫VC中的相應檔案。
我增加了一個設定對話框如下圖所示,用于設定 FindDifference 函數中使用的全局參數 m_Threshold 的值。可以修改這個值的大小然後觀察這個值的大小對輸出結果中紅色區域形狀的影響,這個值越小,則輸出結果的紅色區域越接近為“矩形”(這個形狀主要是基于遊戲中使用的圖檔),即對差異的檢驗越嚴格。這個值越大,則輸出結果的紅色區域會産生收縮,使其更接近差異的“實際形狀”,即使“容許誤差”增加(把灰階變化較小的部分過濾,僅标示灰階差異明顯的地方)。當然這個值如果設定的過大,則紅色區域會減少到“完全消失”。
這裡是源代碼的下載下傳連結:(于 2014 年 2 月 17 日 被我撤除)
最後我要特别提示的是,遊戲的本質是娛樂,不要為了追求浮雲而失去遊戲的本意。
【補充】by hoodlum1980 @ 2011-11-19
其實對于這個工具來說,僅僅是比較兩幅位圖,找出不同之處。其實就很簡單了,我們隻要兩張圖的位圖資料塊做異或就可以得到差異結果,相當于在 photoshop 中的圖層模式設定為“內插補點”,就可以看到兩個圖層之間的差異。大緻方法如下:
為了加快效率,用 uint32* 類型的指針,分别指向兩個位圖的資料塊。一般我們截圖的結果是 bpp = 24,也就是說每 3 個位元組為一個像素,但我們依然可以每 4 個位元組一組進行異或,異或結果就是結果(同樣,可以對異或結果不為0的結果像素用顯著顔色辨別),把結果圖檔呈現出來就達到和本文中相同的效果。這種方法會比我之前實作的方式速度快很多。
但如果我們要推算出像素位置,則還需要進行一次換算,假設截圖為 24 bpp,則資料排列如下:
| 0 | 1 | 2 |
|B G R B|G R B G|R ...
| 0 | 1 | .....
設 pDest 為結果圖檔的資料塊指針,pSrc1和pSrc2分别是要比較的圖檔(注意實際上可使用同一個位圖,隻是水準方向上有不同偏移的定位)
*pDest = *pSrc1 ^ *pSrc2;
設 uint32* p0 為資料塊起始點:假設在 p1 處發現不為 0,則距離起始點的距離是:
(p1 - p0) * sizeof(uint), 即 (p1-p0)*4;
然後換成成像素坐标:
stride = (width * bpp + 31)/32 * 4;
y = (height - 1) - (p1-p0)*sizeof(uint) / stride; //如果掃描行為逆序
x = (((p1 - p0) * sizeof(uint)) % stride) / 3;
注意實際上這裡涵蓋了兩個相鄰像素(x,y)和(x+1,y)組成的(由于資料塊用int32對齊,是以一定在同一掃描行内),可能是 BGR|B, GR|BG, R|BGR 三種情況之一:是以我們還應該分析除以 3 以後的餘數。
令餘數 k = (((p1-p0)*sizeof(uint)) % stride) % 3;
k = 0: BGR|B (x, y) , (x+1, y)的B
k = 1: GR|BG (x,y)的GR, (x+1,y)的BG
k = 2: R|BGR (x,y)的R, (x+1,y)
如果我們需要更精确的知道到底似乎是那個像素不同導緻的,我們還需要對這四個位元組掃描一下,然後按照上訴情況分析不同的值位于(x,y)還是(x+1,y)。當然一般來說如果是在差異區域内部實際沒必要區分。
【對以上補充的補充說明:】