最近剛接觸CUDA,先寫一些簡單的示例練習下。
在圖像處理中,三通道彩色圖像BGR到灰度圖Gray,常見的一般有兩種計算方式,一種是基於浮點數計算,一種是基於性能優化的通過移位的整數計算。
浮點數計算公式爲: gray = 0.1140 * B + 0.5870 * G + 0.2989 * R
整數計算公式爲: gray = (1868 * B + 9617 * G + 4899 * R) >> 14 ,1868從二進制的角度看,向右移位14位,相當於,以此類推。
下面的代碼主要參考[2]進行修改得到:
頭文件:funset.hpp
#include <cuda_runtime.h> // For the CUDA runtime routines (prefixed with "cuda_")
#include <device_launch_parameters.h>
#include <cstdlib>
#include <vector>
int bgr2gray_cpu(const unsigned char* src, int width, int height, unsigned char* dst);
int bgr2gray_gpu(const unsigned char* src, int width, int height, unsigned char* dst);
基於CPU的BRG2GRAY:bgr2gray.cpp
#include "funset.hpp"
int bgr2gray_cpu(const unsigned char* src, int width, int height, unsigned char* dst)
{
const int R2Y{ 4899 }, G2Y{ 9617 }, B2Y{ 1868 }, yuv_shift{ 14 };
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
dst[y * width + x] = (unsigned char)((src[y*width * 3 + 3 * x + 0] * B2Y +
src[y*width * 3 + 3 * x + 1] * G2Y + src[y*width * 3 + 3 * x + 2] * R2Y) >> yuv_shift);
}
}
return 0;
}
基於GPU的BRG2GRAY:bgr2gray.cu
#include "funset.hpp"
/* __global__: 函數類型限定符;在GPU設備上運行;在主機端CPU調用,計算能力3.2及以上可以在
設備端調用;聲明的函數的返回值必須是void類型;對此類型函數的調用是異步的,即在
設備完全完成它的運行之前就返回了;對此類型函數的調用必須指定執行配置,即用於在
設備上執行函數時的grid和block的維度,以及相關的流(即插入<<< >>>運算符);
a kernel,表示此函數爲內核函數(運行在GPU上的CUDA並行計算函數稱爲kernel(內核函
數),內核函數必須通過__global__函數類型限定符定義);*/
__global__ static void bgr2gray(const unsigned char* src, int B2Y, int G2Y, int R2Y, int shift, int width, int height, unsigned char* dst)
{
/* gridDim: 內置變量,用於描述線程網格的維度,對於所有線程塊來說,這個
變量是一個常數,用來保存線程格每一維的大小,即每個線程格中線程塊的數量.
一個grid爲三維,爲dim3類型;
blockDim: 內置變量,用於說明每個block的維度與尺寸.爲dim3類型,包含
了block在三個維度上的尺寸信息;對於所有線程塊來說,這個變量是一個常數,
保存的是線程塊中每一維的線程數量;
blockIdx: 內置變量,變量中包含的值就是當前執行設備代碼的線程塊的索引;用
於說明當前thread所在的block在整個grid中的位置,blockIdx.x取值範圍是
[0,gridDim.x-1],blockIdx.y取值範圍是[0, gridDim.y-1].爲uint3類型,
包含了一個block在grid中各個維度上的索引信息;
threadIdx: 內置變量,變量中包含的值就是當前執行設備代碼的線程索引;用於
說明當前thread在block中的位置;如果線程是一維的可獲取threadIdx.x,如果
是二維的還可獲取threadIdx.y,如果是三維的還可獲取threadIdx.z;爲uint3類
型,包含了一個thread在block中各個維度的索引信息 */
int x = threadIdx.x + blockIdx.x * blockDim.x;
int y = threadIdx.y + blockIdx.y * blockDim.y;
if (x < width && y < height) {
dst[y * width + x] = (unsigned char)((src[y*width * 3 + 3 * x + 0] * B2Y +
src[y*width * 3 + 3 * x + 1] * G2Y + src[y*width * 3 + 3 * x + 2] * R2Y) >> shift);
}
}
int bgr2gray_gpu(const unsigned char* src, int width, int height, unsigned char* dst)
{
const int R2Y{ 4899 }, G2Y{ 9617 }, B2Y{ 1868 }, yuv_shift{ 14 };
unsigned char *dev_src{ nullptr }, *dev_dst{ nullptr };
// cudaMalloc: 在設備端分配內存
cudaMalloc(&dev_src, width * height * 3 * sizeof(unsigned char));
cudaMalloc(&dev_dst, width * height * sizeof(unsigned char));
/* cudaMemcpy(void dst, void src, size_t nbytes, enum cudaMemcpyKind direction)
在主機端和設備端拷貝數據,此函數第四個參數僅能是下面之一:
(1). cudaMemcpyHostToHost: 拷貝數據從主機端到主機端
(2). cudaMemcpyHostToDevice: 拷貝數據從主機端到設備端
(3). cudaMemcpyDeviceToHost: 拷貝數據從設備端到主機端
(4). cudaMemcpyDeviceToDevice: 拷貝數據從設備端到設備端
(5). cudaMemcpyDefault: 從指針值自動推斷拷貝數據方向,需要支持
統一虛擬尋址(CUDA6.0及以上版本)
cudaMemcpy函數對於主機是同步的, 這裏是把CPU上的src拷貝到GPU上的dev_src */
cudaMemcpy(dev_src, src, width * height * 3 * sizeof(unsigned char), cudaMemcpyHostToDevice);
/* cudaMemset: 存儲器初始化函數,在GPU內存上執行。用指定的值初始化或設置
設備內存 */
cudaMemset(dev_dst, 0, width * height * sizeof(unsigned char));
/* dim3: 基於uint3定義的內置矢量類型,相當於由3個unsigned int類型組成的
結構體,可表示一個三維數組,在定義dim3類型變量時,凡是沒有賦值的元素都
會被賦予默認值1 */
// Note:每一個線程塊支持的最大線程數量爲1024,即threads.x*threads.y必須小於等於1024
dim3 threads(32, 32);
dim3 blocks((width + 31) / 32, (height + 31) / 32);
/* <<< >>>: 爲CUDA引入的運算符,指定線程網格和線程塊維度等,傳遞執行參
數給CUDA編譯器和運行時系統,用於說明內核函數中的線程數量,以及線程是如何
組織的;尖括號中這些參數並不是傳遞給設備代碼的參數,而是告訴運行時如何
啓動設備代碼,傳遞給設備代碼本身的參數是放在圓括號中傳遞的,就像標準的函
數調用一樣;不同計算能力的設備對線程的總數和組織方式有不同的約束;必須
先爲kernel中用到的數組或變量分配好足夠的空間,再調用kernel函數,否則在
GPU計算時會發生錯誤,例如越界等 ;
使用運行時API時,需要在調用的內核函數名與參數列表直接以<<<Dg,Db,Ns,S>>>
的形式設置執行配置,其中:Dg是一個dim3型變量,用於設置grid的維度和各個
維度上的尺寸.設置好Dg後,grid中將有Dg.x*Dg.y*Dg.z個block;Db是
一個dim3型變量,用於設置block的維度和各個維度上的尺寸.設置好Db後,每個
block中將有Db.x*Db.y*Db.z個thread;Ns是一個size_t型變量,指定各塊爲此調
用動態分配的共享存儲器大小,這些動態分配的存儲器可供聲明爲外部數組
(extern __shared__)的其他任何變量使用;Ns是一個可選參數,默認值爲0;S爲
cudaStream_t類型,用於設置與內核函數關聯的流.S是一個可選參數,默認值0. */
// Note: 核函數不支持傳入參數爲vector的data()指針,需要cudaMalloc和cudaMemcpy,因爲vector是在主機內存中
bgr2gray << <blocks, threads >> >(dev_src, B2Y, G2Y, R2Y, yuv_shift, width, height, dev_dst);
/* cudaDeviceSynchronize: kernel的啓動是異步的, 爲了定位它是否出錯, 一
般需要加上cudaDeviceSynchronize函數進行同步; 將會一直處於阻塞狀態,直到
前面所有請求的任務已經被全部執行完畢,如果前面執行的某個任務失敗,將會
返回一個錯誤;當程序中有多個流,並且流之間在某一點需要通信時,那就必須
在這一點處加上同步的語句,即cudaDeviceSynchronize;異步啓動
reference: https://stackoverflow.com/questions/11888772/when-to-call-cudadevicesynchronize */
cudaDeviceSynchronize();
// 將處理結果從GPU設備中拷貝出來到CPU上
cudaMemcpy(dst, dev_dst, width * height * sizeof(unsigned char), cudaMemcpyDeviceToHost);
// cudaFree: 釋放GPU設備上由cudaMalloc函數分配的內存
cudaFree(dev_dst);
cudaFree(dev_src);
return 0;
}
主程序:main.cpp
#include "funset.hpp"
#include <chrono>
#include <opencv2/highgui.hpp>
#include <opencv2/opencv.hpp>
int main()
{
cv::Mat srcImage = cv::imread("../test5.jpg");
const uint imgheight = srcImage.rows;
const uint imgwidth = srcImage.cols;
cv::Mat grayImage(imgheight, imgwidth, CV_8UC1, cv::Scalar(0));
double all_count = 0;
for(int idx = 0; idx < 101; idx++)
{
auto start = std::chrono::system_clock::now();
//bgr2gray_cpu(srcImage.data, imgwidth, imgheight, grayImage.data);
//cv::cvtColor(srcImage, grayImage, cv::COLOR_BGR2GRAY);
bgr2gray_gpu(srcImage.data, imgwidth, imgheight, grayImage.data);
auto end = std::chrono::system_clock::now();
std::chrono::duration<double> diff = end-start;
if(idx > 0)
{
all_count += diff.count();
if(idx%10 == 0)
std::cout << idx <<" Time: " << all_count/idx << " s\n";
}
else
cv::imwrite("/home/lzhr/workspace/code/cuda_demo/opencv2cuda/CUDA_Test/bgr2gray_gpu.png", grayImage);
}
return 0;
}
CMake:CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
project(image_process)
find_package(OpenCV REQUIRED)
find_package(CUDA REQUIRED)
# 定義用戶自定義變量,根據自己的代碼路徑進行修改
SET(PATH_CPP_FILES /home/lzhr/workspace/code/cuda_demo/opencv2cuda/CUDA_Test/demo)
SET(PATH_CU_FILES /home/lzhr/workspace/code/cuda_demo/opencv2cuda/CUDA_Test/demo)
# 遞歸查詢所有匹配的文件:*.cpp和*.cu
FILE(GLOB_RECURSE CPP_LIST ${PATH_CPP_FILES}/*.cpp)
FILE(GLOB_RECURSE CU_LIST ${PATH_CU_FILES}/*.cu)
# 使CMake支持C++11特性
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu++0x")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++0x")
# 使CUDA NVCC 編譯器支持C++11特性
LIST(APPEND CUDA_NVCC_FLAGS -std=c++11;-O2)
LIST(APPEND CUDA_NVCC_FLAGS -Xcompiler;-fPIC)
cuda_add_executable(image_process ${CPP_LIST} ${CU_LIST})
target_link_libraries(image_process ${OpenCV_LIBS})
編譯運行:
cd /home/lzhr/workspace/code/cuda_demo/opencv2cuda/CUDA_Test/
mkdir build
# CMakeLists.txt放在build目錄下
cd build
cmake .
make
./image_process
根據[1],cv::cvtColor()第一次執行會比較慢的原因,可能是動態加載庫的問題,即大部分時間花在了將函數加載進內存上面,所以這裏的計時並不把第一次算進去求平均,而是取第2次~101次的耗時取平均。
處理720P的圖像,最終的計時結果是bgr2gray_cpu: 3.8ms,bgr2gray_gpu: 0.8ms,opencv的cvtColor: 0.25ms ; 這裏bgr2gray_gpu的速度不如opencv的cvtColor,根據[1]中的說法,OpenCV的cvtColor是經過硬件加速的,參見[3]。
參考:
[1] cuda練習(一):使用cuda將rbg圖像轉爲灰度圖像