1. 程式人生 > >使用OpenCL+OpenCV實現影象旋轉(一)

使用OpenCL+OpenCV實現影象旋轉(一)

[題外話]近期申請了一個微信公眾號:平凡程式人生。有興趣的朋友可以關注,那裡將會涉及更多更新OpenCL+OpenCV以及影象處理方面的文章。

最近在學習《OPENCL異構計算》,其中有一個例項是使用OpenCL實現影象旋轉。這個例項中並沒有涉及讀取、儲存、顯示影象等操作,其中也存在一些小bug。在學習OpenCL之初,完整地實現這個例項還是很有意義的事情。

1、影象旋轉原理

所謂影象旋轉是指影象以某一點為中心旋轉一定的角度,形成一幅新的影象的過程。這個點通常就是影象的中心。

由於是按照中心旋轉,所以有這樣一個屬性:旋轉前和旋轉後的點離中心的位置不變.

根據這個屬性,可以得到旋轉後的點的座標與原座標的對應關係。

原影象的座標一般是以左上角為原點的,我們先把座標轉換為以影象中心為原點。假設原影象的寬為w,高為h,(x0,y0)為原座標內的一點,轉換座標後的點為(x1,y1)。可以得到:

X0’ = x0 -w/2;

y1’ =-y0 + h/2;

在新的座標系下,假設點(x0,y0)距離原點的距離為r,點與原點之間的連線與x軸的夾角為b,旋轉的角度為a,旋轉後的點為(x1,y1), 如下圖所示。


那麼有以下結論:

x0=r*cosb;y0=r*sinb

x1 = r*cos(b-a)= r*cosb*cosa+r*sinb*sina=x0*cosa+y0*sina;

y1=r*sin(b-a)=r*sinb*cosa-r*cosb*sina=-x0*sina+y0*cosa;

得到了轉換後的座標,我們只需要把這些座標再轉換為原座標系即可。

x1’ = x1+w/2= x0*cosa+y0*sina+w/2

y1’=-y1+h/2=-(-x0*sina+y0*cosa)+h/2=x0*sina-y0*cosa+h/2

此處的x0/y0是新的座標系中的值,轉換為原座標系為:

x1’ = x0*cosa+y0*sina+w/2=(x00-w/2)*consa+(-y00+h/2)*sina+w/2

y1’= x0*sina-y0*cosa+h/2=(x00-w/2)*sina-(-y00+h/2)*cosa+h/2

=(y00-h/2)*cosa+( x00-w/2)*sina+h/2

2、程式設計

對於影象旋轉這個例項,為了處理簡單,我將在灰度圖上去做旋轉。 大致的處理流程如下:

1>     呼叫OpenCVAPI imread()讀取一張彩色JPEG圖片,將它儲存在MAT變數中。該變數的data成員中儲存著將JPEG圖片解碼後的RGB資料。

2>     呼叫OpenCVAPI cvtColor()將儲存RGB資料的MAT變數轉換為只儲存灰度影象資料的MAT物件。也可以使用函式imread()時直接將JPEG影象解碼轉換為灰度影象。

3>     MAT物件的成員width和height儲存著解碼後圖像的解析度資訊。根據當前解析度,分配處理影象時所用的輸入buffer和輸出buffer。它們都按照儲存char型資料進行空間申請。

4>     將MAT物件的成員data中資料copy到輸入buffer中。同時將輸出buffer初始化為全0。到此,我們呼叫OpenCV的API所要做的事情告一段落了。接下來就要呼叫OpenCL的API做事情了。

5>     呼叫OpenCLAPI clGetPlatformIDs()直接獲取第一個可用的平臺資訊。該函式一般是先用它獲取支援OpenCL平臺的數目,然後再次呼叫它獲取某個平臺的資訊。兩次呼叫,通過傳遞不同引數區分。

6>     呼叫OpenCLAPI clGetDeviceIDs()獲取第一個平臺中第一個可用的裝置。同樣,這個函式也可以呼叫兩次,分別獲取當前平臺的裝置數目,再獲取某個裝置資訊。

7>     呼叫OpenCL API clCreateContext()建立上下文。

8>     呼叫OpenCL API clCreateCommandQueue()建立host與device之間互動的command佇列。

9>     呼叫OpenCL API clCreateBuffer()在裝置端分配儲存輸入影象的buffer。

10> 呼叫OpenCL API clEnqueueWriteBuffer()將之前儲存灰度影象資料的輸入buffer記憶體copy到裝置端buffer中。

11> 呼叫OpenCL API clCreateBuffer()在裝置端分配處理完資料的儲存buffer。

12> 呼叫檔案讀取函式,將kernel檔案ImageRotate.cl中的內容讀取到string變數中。

13> 呼叫OpenCL APIclCreateProgramWithSource(),使用kernel的原始碼建立program物件。

14> 呼叫OpenCL APIclBuildProgram()編譯program物件。

15> 呼叫OpenCL APIclCreateKernel(),使用編譯完的程式物件建立kernel。

16> 呼叫OpenCL APIclSetKernelArg()為kernel程式傳遞引數,包括輸入輸出buffer地址,影象解析度和sin()\cos()值。

17> 呼叫OpenCL APIclEnqueueNDRangeKernel()執行kernel。

18> 呼叫OpenCL APIclEnqueueReadBuffer,將處理完的影象資料已經從裝置端傳遞到了host端的輸出buffer中。

19> 將輸出buffer中的資料copy到MAT物件的成員data中。

20> 呼叫OpenCV APIimwrite()將旋轉後的灰度影象儲存到檔案中,編碼為JPEG儲存起來。

21> 釋放輸入輸出buffer空間,釋放OpenCL建立的各個物件。

3、kernel程式程式碼

我們先看一下kernel程式。Kernel程式是每個workitem需要執行的,它需要儲存在以cl為字尾的檔案中,比如:ImageRotate.cl

Kernel程式定義如下:

       __kernel voidimg_rotate(

       __global unsigned char*dest_data,

       __global unsigned char*src_data,

       int W,

       int H,

       floatsinTheta,

       floatcosTheta)

         有幾點需要注意的地方:

1〉  必須帶著關鍵字__kernel;

2〉  返回值必須為void;

3〉  區分清楚所傳引數的儲存型別,比如帶__global表示儲存在globalmemory中;什麼都不帶的W、H等表示儲存在work item的private memory中。

Kernel程式如下:
1.	__kernel void img_rotate(  
2.	    __global unsigned char *dest_data,  
3.	    __global unsigned char *src_data,  
4.	    int W,  
5.	    int H,  
6.	    float sinTheta,  
7.	    float cosTheta){  
8.	        //work item gets its index within index space  
9.	        const int ix = get_global_id(0);  
10.	        const int iy = get_global_id(1);  
11.	          
12.	        //calculate location of data to move int (ix, iy)  
13.	        //output decomposition as mentioned  
14.	        float xpos = ((float)(ix - W / 2)) * cosTheta + ((float)(-iy + H / 2)) * sinTheta + W / 2;  
15.	        float ypos = ((float)(ix - W / 2)) * sinTheta + ((float)(iy - H / 2)) * cosTheta + H / 2;  
16.	  
17.	        //bound checking  
18.	        if (((int)xpos >=0) && ((int)xpos < W) &&  
19.	            ((int)ypos >= 0) && ((int)ypos < H)) {  
20.	                dest_data[(int)ypos * W + (int)xpos] = src_data[iy * W + ix];  
21.	        }  
22.	}  

(未完待續)