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

使用OpenCL+OpenCV實現圖像旋轉(一)

posit 段落 大致 pro 什麽 string cpp base wechat

[題外話]近期申請了一個微信公眾號:平凡程式人生。有興趣的朋友可以關註,那裏將會涉及更多更新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> 調用OpenCV API imread()讀取一張彩色JPEG圖片,將它存儲在MAT變量中。該變量的data成員中存儲著將JPEG圖片解碼後的RGB數據。

2> 調用OpenCV API 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> 調用OpenCL API clGetPlatformIDs()直接獲取第一個可用的平臺信息。該函數一般是先用它獲取支持OpenCL平臺的數目,然後再次調用它獲取某個平臺的信息。兩次調用,通過傳遞不同參數區分。

6> 調用OpenCL API 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 API clCreateProgramWithSource(),使用kernel的源碼創建program對象。

14> 調用OpenCL API clBuildProgram()編譯program對象。

15> 調用OpenCL API clCreateKernel(),使用編譯完的程序對象創建kernel。

16> 調用OpenCL API clSetKernelArg()為kernel程序傳遞參數,包括輸入輸出buffer地址,圖像分辨率和sin()\cos()值。

17> 調用OpenCL API clEnqueueNDRangeKernel()執行kernel。

18> 調用OpenCL API clEnqueueReadBuffer,將處理完的圖像數據已經從設備端傳遞到了host端的輸出buffer中。

19> 將輸出buffer中的數據copy到MAT對象的成員data中。

20> 調用OpenCV API imwrite()將旋轉後的灰度圖像保存到文件中,編碼為JPEG保存起來。

21> 釋放輸入輸出buffer空間,釋放OpenCL創建的各個對象。

3、kernel程序代碼

我們先看一下kernel程序。Kernel程序是每個work item需要執行的,它需要存儲在以cl為後綴的文件中,比如:ImageRotate.cl。

Kernel程序定義如下:

__kernel void img_rotate(

__global unsigned char *dest_data,

__global unsigned char *src_data,

int W,

int H,

float sinTheta,

float cosTheta)

有幾點需要註意的地方:

1〉 必須帶著關鍵字__kernel;

2〉 返回值必須為void;

3〉 區分清楚所傳參數的存儲類型,比如帶__global表示存儲在global memory中;什麽都不帶的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.	}  

  (未完待續)

使用OpenCL+OpenCV實現圖像旋轉(一)