使用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> 調用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實現圖像旋轉(一)