【CUDA】BGR2GRAY

最近剛接觸CUDA,先寫一些簡單的示例練習下。

在圖像處理中,三通道彩色圖像BGR到灰度圖Gray,常見的一般有兩種計算方式,一種是基於浮點數計算,一種是基於性能優化的通過移位的整數計算。

浮點數計算公式爲: gray = 0.1140 * B  + 0.5870 * G + 0.2989 * R

整數計算公式爲: gray = (1868 * B + 9617 * G + 4899 * R) >> 14 ,1868從二進制的角度看,向右移位14位,相當於1868\div 2^{14}=1868\div 16384\approx 0.1140,以此類推。

下面的代碼主要參考[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圖像轉爲灰度圖像

[2] CUDA Samples: Image Process: BGR to Gray

[3] OpenCv源碼解析:對HAL硬件加速層的支持

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章