最近有個需求是得去點擊某個程式的一些按鈕,但寫死座標位置使程式沒有彈性不是我的風格,最後就決定結合影像技術的方向來做一個仿sikuli利用截圖來點擊的偽按鍵精靈。以下是一些完成這目的的片段程式碼:

Screenshot

```

int screenWidth = Screen.GetBounds(new Point(0, 0)).Width;

int screenHeight = Screen.GetBounds(new Point(0, 0)).Height;

Bitmap bmpScreenShot = new Bitmap(screenWidth, screenHeight);   // the final image used by memory reference

Graphics gfx = Graphics.FromImage((Image)bmpScreenShot);

gfx.CopyFromScreen(0, 0, 0, 0, new Size(screenWidth, screenHeight));


最後直接使用或回傳bmpScreenShot即可。<!--more-->
<h2><span style="color: #3366ff;">Extract SubImage for Button</span></h2>

Bitmap croppedImage = originalBitmap.Clone(theRect, originalBitmap.PixelFormat);

<h2><span style="color: #3366ff;">Mouse Click</span></h2>
一開始先設定引用一些dll與外部函式,將其放在class內與所有函式最外面:

[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]

public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);

[DllImport("User32")]

public static extern void SetCursorPos(int x, int y); [DllImport("USER32.DLL")]</pre>
private const int MOUSEEVENTF_LEFTDOWN = 0x02;

private const int MOUSEEVENTF_LEFTUP = 0x04;

private const int MOUSEEVENTF_RIGHTDOWN = 0x08;

private const int MOUSEEVENTF_RIGHTUP = 0x10;


點擊的方式這裡是先將滑鼠移到該位置後再做點擊的動作:

SetCursorPos(posX, posY);

mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);   // dx, dy from current mouse position

<h2><span style="color: #3366ff;">Keyboard Pressing</span></h2>
一樣先引入一些dll與外部函式的定義:

[DllImport("USER32.DLL")]

public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("USER32.DLL")]

public static extern bool SetForegroundWindow(IntPtr hWnd);


點擊按鈕,相關對應按鈕代碼可參照<a href="http://msdn.microsoft.com/zh-tw/library/system.windows.forms.sendkeys(v=vs.80).aspx" target="_blank">官方reference</a>。

IntPtr calculatorHandle = FindWindow(null, "提示");

// 先找到你要送 key message 到哪個視窗,第二個參數給視窗的名稱即可

if (calculatorHandle == IntPtr.Zero)  // 這裡檢查有沒有找到

{

return;

}

SetForegroundWindow(calculatorHandle); // 然後 focus 在你要送的視窗是

SendKeys.SendWait("{ENTER}");


註:當然若你不想指定視窗,想在當下最前面的視窗按鍵盤,那麼不用去引入些dll,只要呼叫SendKeys即可。
<h2><span style="color: #3366ff;">SubImage Finding</span></h2>

public static List<Point> GetSubPositions(Bitmap main, Bitmap sub)

{

List<Point> possiblepos = new List<Point>();

int mainwidth = main.Width;

int mainheight = main.Height;

int subwidth = sub.Width;

int subheight = sub.Height;

int movewidth = mainwidth - subwidth;

int moveheight = mainheight - subheight;

BitmapData bmMainData = main.LockBits(new Rectangle(0, 0, mainwidth, mainheight), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

BitmapData bmSubData = sub.LockBits(new Rectangle(0, 0, subwidth, subheight), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

int bytesMain = Math.Abs(bmMainData.Stride) * mainheight;

int strideMain = bmMainData.Stride;

System.IntPtr Scan0Main = bmMainData.Scan0;

byte[] dataMain = new byte[bytesMain];

System.Runtime.InteropServices.Marshal.Copy(Scan0Main, dataMain, 0, bytesMain);

int bytesSub = Math.Abs(bmSubData.Stride) * subheight;

int strideSub = bmSubData.Stride;

System.IntPtr Scan0Sub = bmSubData.Scan0;

byte[] dataSub = new byte[bytesSub];

System.Runtime.InteropServices.Marshal.Copy(Scan0Sub, dataSub, 0, bytesSub);

for (int y = 0; y < moveheight; ++y)

{

for (int x = 0; x < movewidth; ++x)

{

MyColor curcolor = GetColor(x, y, strideMain, dataMain);

foreach (var item in possiblepos.ToArray())

{

int xsub = x - item.X;

int ysub = y - item.Y;

if (xsub >= subwidth || ysub >= subheight || xsub < 0)

continue;

MyColor subcolor = GetColor(xsub, ysub, strideSub, dataSub);

if (!curcolor.Equals(subcolor))

{

possiblepos.Remove(item);

}

}

if (curcolor.Equals(GetColor(0, 0, strideSub, dataSub)))

possiblepos.Add(new Point(x, y));

}

}

System.Runtime.InteropServices.Marshal.Copy(dataSub, 0, Scan0Sub, bytesSub);

sub.UnlockBits(bmSubData);

System.Runtime.InteropServices.Marshal.Copy(dataMain, 0, Scan0Main, bytesMain);

main.UnlockBits(bmMainData);

return possiblepos;

}

private static MyColor GetColor(Point point, int stride, byte[] data)

{

return GetColor(point.X, point.Y, stride, data);

}

private static MyColor GetColor(int x, int y, int stride, byte[] data)

{

int pos = y * stride + x * 4;

byte a = data[pos + 3];

byte r = data[pos + 2];

byte g = data[pos + 1];

byte b = data[pos + 0];

return MyColor.FromARGB(a, r, g, b);

}

struct MyColor

{

byte A;

byte R;

byte G;

byte B;

public static MyColor FromARGB(byte a, byte r, byte g, byte b)

{

MyColor mc = new MyColor();

mc.A = a;

mc.R = r;

mc.G = g;

mc.B = b;

return mc;

}

public override bool Equals(object obj)

{

if (!(obj is MyColor))

return false;

MyColor color = (MyColor)obj;

if (color.A == this.A && color.R == this.R && color.G == this.G && color.B == this.B)

return true;

return false;

}

}


直接呼叫GetSubPositions函式,把要搜尋的畫面image與目標image當做參數傳進去即可得到所有該目標的位置所在。

註:這是網路上提供的方式,是以顏色為基底去做找尋,越複雜的目標圖像準確率會較高,若底色較單純的圖案可能會有找到很多位置的情況,最好選擇的目標image區塊能大點使複雜度提高。最好的方式還是使用影像處理中的<a href="http://en.wikipedia.org/wiki/Template_matching" target="_blank">template matching</a>,在matlab有現成函式可以使用,但C#就得自己刻了。
## ----- 20120916 updated -----
以下新增相關程式碼,即利用<a href="http://code.google.com/p/aforge/">Aforge</a>與<a href="http://file.emgu.com/wiki/index.php/Main_Page">EmguCV</a>做到template matching的功能。
<h2><span style="color: #3366ff;">Aforge</span></h2>
使用方式:下載aforge相關<a href="http://code.google.com/p/aforge/downloads/list">dll</a>、加入相關dll至專案參考內、在程式碼最上方加入"using Aforge.Imaging"即可。

List<Point> template_matching(Bitmap s, Bitmap t)

{

Bitmap sourceImage = (Bitmap)s.Clone(new Rectangle(0, 0, s.Width, s.Height), PixelFormat.Format24bppRgb);

Bitmap template = (Bitmap)t.Clone(new Rectangle(0, 0, t.Width, t.Height), PixelFormat.Format24bppRgb);

ExhaustiveTemplateMatching tm = new ExhaustiveTemplateMatching(0.9f);   // 0.9f is score threshold

TemplateMatch[] matchings = tm.ProcessImage(sourceImage, template);

List<Point> result = new List<Point>();

foreach (TemplateMatch matching in matchings)

{

result.Add(matching.Rectangle.Location);

}

return result;

}


註:aforge的template matching運算很久,在一般配備的雙核心+2g ram下需約跑20秒以上。但好處是不用額外安裝opencv, emgucv等的套件。
<h2><span style="color: #3366ff;">EmguCV</span></h2>
使用方法:安裝<a href="http://sourceforge.net/projects/opencvlibrary/files/opencv-win/2.2/">opencv2.2</a>、安裝<a href="http://sourceforge.net/projects/emgucv/files/emgucv/2.2.0.0/">emgucv2.2</a>、加入相關dll參考至專案中(Emgu.CV, Emgu.Util)、引入namespace參考(using ...)。另可參考本站有關在vs2010使用emgucv的<a href="http://blog.hothero.org/178/opencv-on-vs2010-csharp-using-emgucv">舊文</a>。

List<Point> template_matching(Bitmap s, Bitmap t)

{

List<Point> result = new List<Point>();

Image<Bgr, Byte> sourceImage = new Image<Bgr, Byte>(s);

Image<Bgr, Byte> templateImage = new Image<Bgr, Byte>(t);

Image<Gray, float> resultImage = sourceImage.MatchTemplate(templateImage,Emgu.CV.CvEnum.TM_TYPE.CV_TM_CCOEFF_NORMED);

float[,,] matches = resultImage.Data;

for (int x = 0; x < matches.GetLength(1); x++)

{

for (int y = 0; y < matches.GetLength(0); y++)

{

double matchScore = matches[y, x, 0];

if (matchScore > 0.95)

{

result.Add(new Point(x, y));

}

}

}

return result;

}


註:emgucv的template matching執行約1秒,且很準確。唯一缺點就是需要在執行機器上額外安裝opencv。BTW, 若有相關執行問題,或dll參考失敗,只要把emgu.cv, emgu.util的dll複製至與執行檔相同路徑即可。

*此篇主要著重在程式碼記錄,若有任何不清楚的歡迎留言詢問。
<h2><span style="color: #3366ff;">Reference</span></h2>
<ul>
	<li><span style="line-height: 22px;"><a title="find-subimage-in-larger-image-in-c-sharp" href="http://stackoverflow.com/questions/11343772/find-subimage-in-larger-image-in-c-sharp" target="_blank">Find Subimage in Large Image in C#</a></span></li>
	<li><a href="http://stackoverflow.com/questions/11457679/extract-sub-image-from-an-image-using-c-sharp" target="_blank">Extract sub image from an image using C#</a></li>
	<li><a href="http://roronoa.pixnet.net/blog/post/25312590-c%23-%3A-%E6%A8%A1%E6%93%AC%E6%BB%91%E9%BC%A0%E9%8D%B5%E7%9B%A4%E5%8B%95%E4%BD%9C" target="_blank">模擬滑鼠鍵盤動作</a></li>
</ul>