1. 程式人生 > >cuda與openCV結合程式設計(一)

cuda與openCV結合程式設計(一)

    學習計算機影象處理演算法的童鞋,就不得不學習cuda,為啥呢?因為影象處理一般都是矩陣運算,動不動就是百萬的計算量這個時候優化計算時間是必不可少的。openCV本身提供了很多cuda函式,能夠滿足大多數使用者的需求。但是也不絕對,有時候我們需要自己定義一個核心函式進行優化,當然你也可以用openGL或者多執行緒,openCV也提供較好的支援,掌握一種或多種加速演算法,對程式設計師特別是演算法工程師來講很重要。
    閒話不多說,再學習了cuda的基礎以後【cuda並行程式設計基礎(一)cuda並行程式設計基礎(二)】,我們其實就具備與opencv聯合程式設計的能力,雖然不是最佳優化,但已經可以滿足大多數需求了。
一、cuda與openCV結合方法
    (下面僅涉及windows環境)
    1.我們知道,cuda程式碼一般以.cu結尾(windows,其他系統除外,下同),它的編譯器是nvcc,編譯時它會將CPU程式碼和GPU程式碼分開,CPU部分其實與gcc編譯差不多,GPU部分就按照nvcc的規則編譯,這玩意其實並不複雜;
    2.openCV的程式碼一般都是以.cpp結尾,它的編譯器一般是gcc、g++(或者其他相似編譯器),那麼可不可以將openCV程式碼用nvcc編譯呢?答案是肯定的,但在windows系統,你得把它改為.cu結尾.
    3.所以,在windows系統裡面,你有兩種辦法讓openCV結合cuda程式設計:
        a.openCV正常編譯,cuda程式碼編譯好後,作為靜態庫引入openCV呼叫;
        b.openCV與cuda程式碼混在一起,統一用nvcc編譯。
二、如何編寫程式碼

    (下面openCV基於3.2.0版本)
    openCV是一個非常強大的視覺演算法庫,當然也支援cuda咯。
    cv::cuda是一個專門處理cuda的名稱空間,你在這個名稱空間裡面可以看到很多已經整合好的函式。
        如:cuda::remap()、cuda::add()等
    我們要用到的是cuda::PtrStepSz<T>的模板,以及cuda::GpuMat
    比如:如果我們有一個cuda::GpuMat型別的img,我們怎麼傳入cuda裡面呢?答案就是,直接將img傳到cuda::PtrStepSz裡面,他們是不是等同,但是可以互傳資料,具體見樣例。至於傳到cuda::PtrStepSz裡面如何操作,那就跟cuda差不多了。
    除了cuda::PtrStepSz,openCV還有其他介面可以提供互傳,自己去摸索啦,這裡就不囉嗦了。     至於cuda與openCV的結合程式設計效率問題?哈,誰用誰知道,你不用也無需知道,有興趣自己去測一下咯,反正筆者是牆裂推薦的,後面有空再講效率問題。
三、常見錯誤

    1.cudaErrorMemoryAllocation,主要是申請空間太大,超出了GPU限制;
    2.cudaErrorLaunchFailure,訪問了非法地址,比如index超過了陣列大小;
    3.cuda與vs2015結合程式設計,偶爾會出現抽筋的問題,比如你這次編譯出錯,改正了以後再編譯還出錯,建議要重新編譯時,把以前的編譯生成的東西全刪掉,這樣就保險多了,筆者遇見多次這種情況;
    4.<<<>>>核心符號報錯,要確定它出現在cu檔案裡而不是cpp檔案裡,cu檔案會顯示紅色,不用管它;
    5.靜態庫的編寫規範,額,自己上網研究吧,其實我寫得也不太規範,吐槽一下,網上好多技術文章抄來抄去很沒意思,很多大牛又寫得太過高深,研究不出個所以然來,也希望能夠在各個層次都有合適的文章介紹吧,這樣入門和進階也不會太困難。
第一個程式,直接在cu檔案實現cuda與opencv結合程式設計,非常重要哦

一般我們不這麼使用,因為cuda作為獨立的程式設計方式,放在一起容易混亂,而且為支援高速運算,一般都使用c運算,而不是c++
//opencv_cuda.cu:使用自定義函式,實現cuda版本圖片翻轉
//authored by alpc40
//version:visual studio 2015\cuda toolkit 8.0\openCV 3.2.0
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
#ifdef _DEBUG
#pragma comment ( lib,"opencv_core320d.lib")
#pragma comment ( lib,"opencv_highgui320d.lib")
#pragma comment ( lib,"opencv_calib3d320d.lib")
#pragma comment ( lib,"opencv_imgcodecs320d.lib")
#pragma comment ( lib,"opencv_imgproc320d.lib")
#pragma comment ( lib,"opencv_cudaimgproc320d.lib")
#pragma comment ( lib,"opencv_cudaarithm320d.lib")
#pragma comment ( lib,"cudart.lib")
#else
#pragma comment ( lib,"opencv_core320.lib")
#pragma comment ( lib,"opencv_highgui320.lib")
#pragma comment ( lib,"opencv_calib3d320.lib")
#pragma comment ( lib,"opencv_imgcodecs320.lib")
#pragma comment ( lib,"opencv_imgproc320.lib")
#pragma comment ( lib,"opencv_cudaimgproc320.lib")
#pragma comment ( lib,"opencv_cudaarithm320.lib")
#pragma comment ( lib,"cudart.lib")
#endif
//出錯處理函式
#define CHECK_ERROR(call){\
    const cudaError_t err = call;\
    if (err != cudaSuccess)\
    {\
        printf("Error:%s,%d,",__FILE__,__LINE__);\
        printf("code:%d,reason:%s\n",err,cudaGetErrorString(err));\
        exit(1);\
    }\
}
//核心函式:實現上下翻轉
__global__ void swap_image_kernel(cuda::PtrStepSz<uchar3> cu_src, cuda::PtrStepSz<uchar3> cu_dst, int h, int w)
{
    //計算的方法:參看前面兩文
    unsigned int x = blockDim.x * blockIdx.x + threadIdx.x;
    unsigned int y = blockDim.y * blockIdx.y + threadIdx.y;
    //為啥要這樣限制:參看前面兩文
    if (x < cu_src.cols && y < cu_src.rows)
    {
        //為何不是h-y-1,而不是h-y,自己思考哦
        cu_dst(y, x) = cu_src(h - y - 1, x);
    }
}
//呼叫函式,主要處理block和grid的關係
void swap_image(cuda::GpuMat src,cuda::GpuMat dst,int h, int w)
{
    assert(src.cols == w && src.rows ==h);
    int uint = 32;
    //參考前面兩文的block和grid的計算方法,注意不要超過GPU限制
    dim3 block(uint, uint);
    dim3 grid((w + block.x - 1) / block.x, (h + block.y - 1) / block.y);
    printf("grid = %4d %4d %4d\n",grid.x,grid.y,grid.z);
    printf("block= %4d %4d %4d\n",block.x,block.y,block.z);
    swap_image_kernel << <grid, block >> > (src,dst,h,w);
    //同步一下,因為計算量可能很大
    CHECK_ERROR(cudaDeviceSynchronize());
}
int main(int argc,char **argv)
{
    Mat src, dst;
    cuda::GpuMat cu_src, cu_dst;
    int h, w;
    //根據argv[1]讀入圖片資料,BGR格式讀進來
    src = imread(argv[1]);
    //檢測是否正確讀入
    if (src.data == NULL)
    {
        cout << "Read image error" << endl;
        return -1;
    }
    h = src.rows; w = src.cols;
    cout <<"圖片高:" << h << ",圖片寬:" << w << endl;
    //上傳CPU影象資料到GPU,跟cudaMalloc和cudaMemcpy很像哦,其實upload裡面就是這麼寫的
    cu_src.upload(src);
    //申請GPU空間,也可以到函式裡申請,不管怎樣總要申請,要不然核心函式會爆掉哦
    cu_dst = cuda::GpuMat(h, w, CV_8UC3, Scalar(0, 0, 0));
    //申請CPU空間
    dst = Mat(h, w, CV_8UC3, Scalar(0, 0, 0));
    //呼叫函式swap_image,由該函式呼叫核心函式,這樣層次分明,不容易出錯
    //當然你也可以直接在這裡呼叫核心函式,東西太多程式碼容易亂
    swap_image(cu_src,cu_dst,h, w);
    //下載GPU資料到CPU,與upload()對應
    cu_dst.download(dst);
    //顯示cpu影象,如果安裝了openCV集成了openGL,那可以直接顯示GpuMat
    imshow("dst",dst);
    //等待按鍵
    waitKey();
    //寫圖片到檔案
    if(argc==3)
        imwrite(argv[2],dst);
    return 0;
}
第二個程式,使用靜態庫的方式實現cuda與openCV的結合,非常重要哦
這種方式兩相分離,更好實現了這種功能 //swap_image.cu:生成swap_image.lib,供主函式呼叫
//authored by alpc40
//version:visual studio 2015\cuda toolkit 8.0\openCV 3.2.0
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include<opencv2/opencv.hpp>
using namespace cv;
//出錯處理函式
#define CHECK_ERROR(call){\
    const cudaError_t err = call;\
    if (err != cudaSuccess)\
    {\
        printf("Error:%s,%d,",__FILE__,__LINE__);\
        printf("code:%d,reason:%s\n",err,cudaGetErrorString(err));\
        exit(1);\
    }\
}
//核心函式:實現上下翻轉
__global__ void swap_image_kernel(cuda::PtrStepSz<uchar3> cu_src, cuda::PtrStepSz<uchar3> cu_dst, int h, int w)
{
    //計算的方法:參看前面兩文
    unsigned int x = blockDim.x * blockIdx.x + threadIdx.x;
    unsigned int y = blockDim.y * blockIdx.y + threadIdx.y;
    //為啥要這樣限制:參看前面兩文
    if (x < cu_src.cols && y < cu_src.rows)
    {
        //為何不是h-y-1,而不是h-y,自己思考哦
        cu_dst(y, x) = cu_src(h - y - 1, x);
    }
}
//呼叫函式,主要處理block和grid的關係,注意extern哦,它是庫檔案編寫規範
extern "C" void swap_image(cuda::GpuMat src, cuda::GpuMat dst, int h, int w)
{
    assert(src.cols == w && src.rows == h);
    int uint = 32;
    //參考前面兩文的block和grid的計算方法,注意不要超過GPU限制
    dim3 block(uint, uint);
    dim3 grid((w + block.x - 1) / block.x, (h + block.y - 1) / block.y);
    printf("grid = %4d %4d %4d\n", grid.x, grid.y, grid.z);
    printf("block= %4d %4d %4d\n", block.x, block.y, block.z);
    swap_image_kernel << <grid, block >> > (src, dst, h, w);
    //同步一下,因為計算量可能很大
    CHECK_ERROR(cudaDeviceSynchronize());
}
//opencv_cuda.cpp:第二個程式主函式,使用自編靜態庫,實現cuda版本圖片翻轉
//authored by alpc40
//version:visual studio 2015\cuda toolkit 8.0\openCV 3.2.0
#include <stdio.h>
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
#ifdef _DEBUG
#pragma comment ( lib,"opencv_core320d.lib")
#pragma comment ( lib,"opencv_highgui320d.lib")
#pragma comment ( lib,"opencv_calib3d320d.lib")
#pragma comment ( lib,"opencv_imgcodecs320d.lib")
#pragma comment ( lib,"opencv_imgproc320d.lib")
#pragma comment ( lib,"opencv_cudaimgproc320d.lib")
#pragma comment ( lib,"opencv_cudaarithm320d.lib")
#pragma comment ( lib,"cudart.lib")
#pragma comment ( lib,"swap_image.lib")//別忘了加庫
#else
#pragma comment ( lib,"opencv_core320.lib")
#pragma comment ( lib,"opencv_highgui320.lib")
#pragma comment ( lib,"opencv_calib3d320.lib")
#pragma comment ( lib,"opencv_imgcodecs320.lib")
#pragma comment ( lib,"opencv_imgproc320.lib")
#pragma comment ( lib,"opencv_cudaimgproc320.lib")
#pragma comment ( lib,"opencv_cudaarithm320.lib")
#pragma comment ( lib,"cudart.lib")
#pragma comment ( lib,"swap_image.lib")//別忘了加庫
#endif
//這個宣告很重要,呼叫靜態庫
extern "C" void swap_image(cuda::GpuMat src,cuda::GpuMat dst,int w,int h);
int main(int argc, char **argv)
{
    Mat src, dst;
    cuda::GpuMat cu_src, cu_dst;
    int h, w;
    //根據argv[1]讀入圖片資料,BGR格式讀進來
    src = imread(argv[1]);
    //檢測是否正確讀入
    if (src.data == NULL)
    {
        cout << "Read image error" << endl;
        return -1;
    }
    h = src.rows; w = src.cols;
    cout << "圖片高:" << h << ",圖片寬:" << w << endl;
    //上傳CPU影象資料到GPU,跟cudaMalloc和cudaMemcpy很像哦,其實upload裡面就是這麼寫的
    cu_src.upload(src);
    //申請GPU空間,也可以到函式裡申請,不管怎樣總要申請,要不然核心函式會爆掉哦
    cu_dst = cuda::GpuMat(h, w, CV_8UC3, Scalar(0, 0, 0));
    //申請CPU空間
    dst = Mat(h, w, CV_8UC3, Scalar(0, 0, 0));
    //呼叫函式swap_image,由該函式呼叫核心函式,這樣層次分明,不容易出錯
    //當然你也可以直接在這裡呼叫核心函式,東西太多程式碼容易亂
    swap_image(cu_src, cu_dst, h, w);
    //下載GPU資料到CPU,與upload()對應
    cu_dst.download(dst);
    //顯示cpu影象,如果安裝了openCV集成了openGL,那可以直接顯示GpuMat
    imshow("dst", dst);
    //等待按鍵
    waitKey();
    //寫圖片到檔案
    if (argc == 3)
        imwrite(argv[2], dst);
    return 0;
}
PS:以下為圖片效果,順便為我家贛南臍橙打個廣告