博客專欄

EEPW首頁 > 博客 > 一文詳解OpenCV中的CUDA模塊

一文詳解OpenCV中的CUDA模塊

發(fā)布人:計算機視覺工坊 時間:2021-03-20 來源:工程師 發(fā)布文章

如果您使用OpenCV已有一段時間,那么您應(yīng)該已經(jīng)注意到,在大多數(shù)情況下,OpenCV都使用CPU,這并不總能保證您所需的性能。為了解決這個問題,OpenCV在2010年增加了一個新模塊,該模塊使用CUDA提供GPU加速。您可以在下面找到一個展示GPU模塊優(yōu)勢的基準(zhǔn)測試:

2.png

簡單列舉下本文要交代的幾個事情:

  • 概述已經(jīng)支持CUDA的OpenCV模塊。

  • 看一下cv :: gpu :: GpuMat(cv2.cuda_GpuMat)。

  • 了解如何在CPU和GPU之間傳輸數(shù)據(jù)。

  • 了解如何利用多個GPU。

  • 編寫一個簡單的演示(C ++和Python),以了解OpenCV提供的CUDA API接口并計算我們可以獲得的性能提升。

一、支持的模塊

據(jù)稱,盡管并未涵蓋所有庫的功能,但該模塊“仍在繼續(xù)增長,并正在適應(yīng)新的計算技術(shù)和GPU架構(gòu)?!?/p>

讓我們看一下CUDA加速的OpenCV的官方文檔。在這里,我們可以看到已支持的模塊:

Core part

Operations on Matrices

Background Segmentation

Video Encoding/Decoding

Feature Detection and Description

Image Filtering

Image Processing

Legacy support

Object Detection

Optical Flow

Stereo Correspondence

Image Warping

Device layer

二、GpuMat

為了將數(shù)據(jù)保留在GPU內(nèi)存中,OpenCV引入了一個新的類cv :: gpu :: GpuMat(或Python中的cv2.cuda_GpuMat)作為主要數(shù)據(jù)容器。其界面類似于cv :: Mat(cv2.Mat),從而使向GPU模塊的過渡盡可能平滑。值得一提的是,所有GPU函數(shù)都將GpuMat接收為輸入和輸出參數(shù)。通過這種在代碼中鏈接了GPU算法的設(shè)計,您可以減少在CPU和GPU之間復(fù)制數(shù)據(jù)的開銷。

三、CPU/GUP數(shù)據(jù)傳遞

要將數(shù)據(jù)從GpuMat傳輸?shù)組at,反之亦然,OpenCV提供了兩個函數(shù):

  • 上傳,將數(shù)據(jù)從主機內(nèi)存復(fù)制到設(shè)備內(nèi)存

  • 下載,將數(shù)據(jù)從設(shè)備內(nèi)存復(fù)制到主機內(nèi)存。

以下是用C ++寫的一個簡單示例:

#include <opencv2/highgui.hpp> 
#include <opencv2/cudaimgproc.hpp> 
cv::Mat img = cv::imread("image.png", IMREAD_GRAYSCALE); 
cv::cuda::GpuMat dst, src; 
src.upload(img); 
cv::Ptr<cv::cuda::CLAHE> ptr_clahe = cv::cuda::createCLAHE(5.0, cv::Size(8, 8)); 
ptr_clahe->apply(src, dst); 
cv::Mat result; 
dst.download(result); 
cv::imshow("result", result); 
cv::waitKey();

四、多個GPU的使用

默認(rèn)情況下,每種OpenCV CUDA算法都使用單個GPU。如果需要利用多個GPU,則必須在GPU之間手動分配工作。要切換活動設(shè)備,請使用cv :: cuda :: setDevice(cv2.cuda.SetDevice)函數(shù)。

五、代碼示例

OpenCV提供了有關(guān)如何使用C ++ API在GPU支持下與已實現(xiàn)的方法一起使用的示例。讓我們在使用Farneback的算法進(jìn)行密集光流計算的示例中,實現(xiàn)一個簡單的演示,演示如何將CUDA加速的OpenCV與C ++一起使用。

我們首先來看一下如何使用CPU來完成此操作。然后,我們將使用GPU進(jìn)行相同的操作。最后,我們將比較經(jīng)過的時間以計算獲得的加速比。

FPS計算

由于我們的主要目標(biāo)是找出算法在不同設(shè)備上的運行速度,因此我們需要選擇測量方法。在計算機視覺中,這樣做的常用方法是計算每秒處理的幀數(shù)(FPS)。

CPU端

1.視頻及其屬性

我們將從視頻捕獲初始化開始,并獲取其屬性,例如幀頻和幀數(shù)。這部分是CPU和GPU部分的通用部分:

// init video capture with video 
VideoCapture capture(videoFileName); 
if (!capture.isOpened()) 
{ 
    // error in opening the video file 
    cout << "Unable to open file!" << endl; 
    return; 
} 
// get default video FPS 
double fps = capture.get(CAP_PROP_FPS); 
// get total number of video frames 
int num_frames = int(capture.get(CAP_PROP_FRAME_COUNT));

2.讀取第一幀

由于算法的特殊性,該算法使用兩幀進(jìn)行計算,因此我們需要先讀取第一幀,然后再繼續(xù)。還需要一些預(yù)處理,例如調(diào)整大小并轉(zhuǎn)換為灰度:

// read the first frame 
cv::Mat frame, previous_frame; 
capture >> frame; 
if (device == "cpu") 
{ 
    // resize frame 
    cv::resize(frame, frame, Size(960, 540), 0, 0, INTER_LINEAR); 
    // convert to gray 
    cv::cvtColor(frame, previous_frame, COLOR_BGR2GRAY); 
    // declare outputs for optical flow 
    cv::Mat magnitude, normalized_magnitude, angle; 
    cv::Mat hsv[3], merged_hsv, hsv_8u, bgr; 
    // set saturation to 1 
    hsv[1] = cv::Mat::ones(frame.size(), CV_32F);

3.讀取并預(yù)處理其他幀

在循環(huán)讀取其余幀之前,我們啟動兩個計時器:一個計時器將跟蹤整個流程的工z作時間,第二個計時器–讀取幀時間。由于Farneback的光流法適用于灰度幀,因此我們需要確保將灰度視頻作為輸入傳遞。這就是為什么我們首先對其進(jìn)行預(yù)處理以將每幀從BGR格式轉(zhuǎn)換為灰度的原因。另外,由于原始分辨率可能太大,因此我們將其調(diào)整為較小的尺寸,就像對第一幀所做的一樣。我們再設(shè)置一個計時器來計算在預(yù)處理階段花費的時間:

while (true) 
{ 
    // start full pipeline timer 
    auto start_full_time = high_resolution_clock::now(); 
    // start reading timer 
    auto start_read_time = high_resolution_clock::now(); 
    // capture frame-by-frame 
    capture >> frame; 
    if (frame.empty()) 
        break; 
    // end reading timer 
    auto end_read_time = high_resolution_clock::now(); 
    // add elapsed iteration time 
    timers["reading"].push_back(duration_cast<milliseconds>(end_read_time - start_read_time).count() / 1000.0); 
    // start pre-process timer 
    auto start_pre_time = high_resolution_clock::now(); 
    // resize frame 
    cv::resize(frame, frame, Size(960, 540), 0, 0, INTER_LINEAR); 
    // convert to gray 
    cv::Mat current_frame; 
    cv::cvtColor(frame, current_frame, COLOR_BGR2GRAY); 
    // end pre-process timer 
    auto end_pre_time = high_resolution_clock::now(); 
    // add elapsed iteration time 
    timers["pre-process"].push_back(duration_cast<milliseconds>(end_pre_time - start_pre_time).count() / 1000.0);

4.計算密集光流

我們使用稱為calcOpticalFlowFarneback的方法來計算兩幀之間的密集光流:

// start optical flow timer 
auto start_of_time = high_resolution_clock::now(); 
// calculate optical flow 
cv::Mat flow; 
calcOpticalFlowFarneback(previous_frame, current_frame, flow, 0.5, 5, 15, 3, 5, 1.2, 0); 
// end optical flow timer 
auto end_of_time = high_resolution_clock::now(); 
// add elapsed iteration time 
timers["optical flow"].push_back(duration_cast<milliseconds>(end_of_time - start_of_time).count() / 1000.0);

5.后處理

Farneback的“光流法“輸出二維流矢量。我們將這些輸出轉(zhuǎn)換為極坐標(biāo),以通過色相獲得流動的角度(方向),并通過HSV顏色表示的值獲得流動的距離(幅度)。對于可視化,我們現(xiàn)在要做的就是將結(jié)果轉(zhuǎn)換為BGR空間。之后,我們停止所有剩余的計時器以獲取經(jīng)過的時間:

// start post-process timer 
auto start_post_time = high_resolution_clock::now(); 
// split the output flow into 2 vectors 
cv::Mat flow_xy[2], flow_x, flow_y; 
split(flow, flow_xy); 
// get the result 
flow_x = flow_xy[0]; 
flow_y = flow_xy[1]; 
// convert from cartesian to polar coordinates 
cv::cartToPolar(flow_x, flow_y, magnitude, angle, true); 
// normalize magnitude from 0 to 1 
cv::normalize(magnitude, normalized_magnitude, 0.0, 1.0, NORM_MINMAX); 
// get angle of optical flow 
angle *= ((1 / 360.0) * (180 / 255.0)); 
// build hsv image 
hsv[0] = angle; 
hsv[2] = normalized_magnitude; 
merge(hsv, 3, merged_hsv); 
// multiply each pixel value to 255 
merged_hsv.convertTo(hsv_8u, CV_8U, 255); 
// convert hsv to bgr 
cv::cvtColor(hsv_8u, bgr, COLOR_HSV2BGR); 
// update previous_frame value 
previous_frame = current_frame; 
// end post pipeline timer 
auto end_post_time = high_resolution_clock::now(); 
// add elapsed iteration time 
timers["post-process"].push_back(duration_cast<milliseconds>(end_post_time - start_post_time).count() / 1000.0); 
// end full pipeline timer 
auto end_full_time = high_resolution_clock::now(); 
// add elapsed iteration time 
timers["full pipeline"].push_back(duration_cast<milliseconds>(end_full_time - start_full_time).count() / 1000.0);

6.可視化

我們將尺寸調(diào)整為960×540的原始幀可視化,并使用imshow函數(shù)顯示結(jié)果:

// visualization 
imshow("original", frame); 
imshow("result", bgr); 
int keyboard = waitKey(1); 
if (keyboard == 27) 
    break;

這是一個示例“ boat.mp4”視頻的內(nèi)容:

1.png

7.時間和FPS計算

我們要做的就是計算流程中每一步花費的時間,并測量光流部分和整個流程的FPS:

// elapsed time at each stage 
cout << "Elapsed time" << std::endl; 
for (auto const& timer : timers) 
{ 
    cout << "- " << timer.first << " : " << accumulate(timer.second.begin(),         timer.second.end(), 0.0) << " seconds"<< endl; 
} 
// calculate frames per second 
cout << "Default video FPS : "  << fps << endl; 
float optical_flow_fps  = (num_frames - 1) / accumulate(timers["optical flow"].begin(),  timers["optical flow"].end(),  0.0); 
cout << "Optical flow FPS : "   << optical_flow_fps  << endl; 
float full_pipeline_fps = (num_frames - 1) / accumulate(timers["full pipeline"].begin(), timers["full pipeline"].end(), 0.0); 
cout << "Full pipeline FPS : "  << full_pipeline_fps << endl;

GPU端

該算法在將其移至CUDA時保持不變,但在GPU使用方面存在一些差異。讓我們再次遍歷整個流程,看看有什么變化:

1.視頻及其屬性

此部分在CPU和GPU部分都是通用的,因此保持不變。

2.讀取第一幀

注意,我們使用相同的CPU函數(shù)來讀取和調(diào)整大小,但是將結(jié)果上傳到cv :: cuda :: GpuMat(cuda_GpuMat)實例:

// resize frame 
cv::resize(frame, frame, Size(960, 540), 0, 0, INTER_LINEAR); 
// convert to gray 
cv::cvtColor(frame, previous_frame, COLOR_BGR2GRAY); 
// upload pre-processed frame to GPU 
cv::cuda::GpuMat gpu_previous; 
gpu_previous.upload(previous_frame); 
// declare cpu outputs for optical flow 
cv::Mat hsv[3], angle, bgr; 
// declare gpu outputs for optical flow 
cv::cuda::GpuMat gpu_magnitude, gpu_normalized_magnitude, gpu_angle; 
cv::cuda::GpuMat gpu_hsv[3], gpu_merged_hsv, gpu_hsv_8u, gpu_bgr; 
// set saturation to 1 
hsv[1] = cv::Mat::ones(frame.size(), CV_32F); 
gpu_hsv[1].upload(hsv[1]);

3.讀取和預(yù)處理其它幀

while (true) 
{ 
    // start full pipeline timer 
    auto start_full_time = high_resolution_clock::now(); 
    // start reading timer 
    auto start_read_time = high_resolution_clock::now(); 
    // capture frame-by-frame 
    capture >> frame; 
    if (frame.empty()) 
        break; 
    // upload frame to GPU 
    cv::cuda::GpuMat gpu_frame; 
    gpu_frame.upload(frame); 
    // end reading timer 
    auto end_read_time = high_resolution_clock::now(); 
    // add elapsed iteration time 
    timers["reading"].push_back(duration_cast<milliseconds>(end_read_time - start_read_time).count() / 1000.0); 
    // start pre-process timer 
    auto start_pre_time = high_resolution_clock::now(); 
    // resize frame 
    cv::cuda::resize(gpu_frame, gpu_frame, Size(960, 540), 0, 0, INTER_LINEAR); 
    // convert to gray 
    cv::cuda::GpuMat gpu_current; 
    cv::cuda::cvtColor(gpu_frame, gpu_current, COLOR_BGR2GRAY); 
    // end pre-process timer 
    auto end_pre_time = high_resolution_clock::now(); 
    // add elapsed iteration time 
    timers["pre-process"].push_back(duration_cast<milliseconds>(end_pre_time - start_pre_time).count() / 1000.0);

4.計算密集光流

我們首先使用cv :: cuda :: FarnebackOpticalFlow :: create(cv2.cudaFarnebackOpticalFlow.create)創(chuàng)建cudaFarnebackOpticalFlow類的實例,然后調(diào)用cv :: cuda:FarnebackOpticalFlow :: calc(cv2.cuda_FarnebackOpticalFlow.calc)計算兩個幀之間的光流,而不是使用cv :: calcOpticalFlowFarneback(cv2.calcOpticalFlowFarneback)函數(shù)調(diào)用。

// start optical flow timer 
auto start_of_time = high_resolution_clock::now(); 
// create optical flow instance 
Ptr<cuda::FarnebackOpticalFlow> ptr_calc = cuda::FarnebackOpticalFlow::create(5, 0.5, false, 15, 3, 5, 1.2, 0); 
// calculate optical flow 
cv::cuda::GpuMat gpu_flow; 
ptr_calc->calc(gpu_previous, gpu_current, gpu_flow); 
// end optical flow timer 
auto end_of_time = high_resolution_clock::now(); 
// add elapsed iteration time 
timers["optical flow"].push_back(duration_

cast<milliseconds>(end_of_time - start_of_time).count() / 1000.0);

5.后處理

對于后處理,我們使用與CPU端使用的功能相同的GPU變體:

// start post-process timer 
auto start_post_time = high_resolution_clock::now(); 
// split the output flow into 2 vectors 
cv::cuda::GpuMat gpu_flow_xy[2]; 
cv::cuda::split(gpu_flow, gpu_flow_xy); 
// convert from cartesian to polar coordinates 
cv::cuda::cartToPolar(gpu_flow_xy[0], gpu_flow_xy[1], gpu_magnitude, gpu_angle, true); 
// normalize magnitude from 0 to 1 
cv::cuda::normalize(gpu_magnitude, gpu_normalized_magnitude, 0.0, 1.0, NORM_MINMAX, -1); 
// get angle of optical flow 
gpu_angle.download(angle); 
angle *= ((1 / 360.0) * (180 / 255.0)); 
// build hsv image 
gpu_hsv[0].upload(angle); 
gpu_hsv[2] = gpu_normalized_magnitude; 
cv::cuda::merge(gpu_hsv, 3, gpu_merged_hsv); 
// multiply each pixel value to 255 
gpu_merged_hsv.cv::cuda::GpuMat::convertTo(gpu_hsv_8u, CV_8U, 255.0); 
// convert hsv to bgr 
cv::cuda::cvtColor(gpu_hsv_8u, gpu_bgr, COLOR_HSV2BGR); 
// send original frame from GPU back to CPU 
gpu_frame.download(frame); 
// send result from GPU back to CPU 
gpu_bgr.download(bgr); 
// update previous_frame value 
gpu_previous = gpu_current; 
// end post pipeline timer 
auto end_post_time = high_resolution_clock::now(); 
// add elapsed iteration time 
timers["post-process"].push_back(duration_cast<milliseconds>(end_post_time - start_post_time).count() / 1000.0); 
// end full pipeline timer 
auto end_full_time = high_resolution_clock::now(); 
// add elapsed iteration time 
timers["full pipeline"].push_back(duration_cast<milliseconds>(end_full_time - start_full_time).count() / 1000.0);

可視化、時間和FPS計算與CPU端相同。

結(jié)果

現(xiàn)在,我們可以在示例視頻中比較來自CPU和GPU版本的指標(biāo)。 

我們用于CPU的配置為:

Intel Core i7-8700

Configuration 

- device : cpu 

- video file : video/boat.mp4 

Number of frames: 320 

Elapsed time 

- full pipeline : 37.355 seconds 

- reading : 3.327 seconds 

- pre-process : 0.027 seconds 

- optical flow : 32.706 seconds 

- post-process : 0.641 seconds 

Default video FPS : 29.97 

Optical flow FPS : 9.75356 

Full pipeline FPS : 8.53969

用于GPU的配置為:

Nvidia GeForce GTX 1080 Ti

Configuration 

- device : gpu 

- video file : video/boat.mp4 

Number of frames: 320 

Elapsed time 

- full pipeline : 8.665 seconds 

- reading : 4.821 seconds 

- pre-process : 0.035 seconds 

- optical flow : 1.874 seconds 

- post-process : 0.631 seconds 

Default video FPS : 29.97 

Optical flow FPS : 170.224 

Full pipeline FPS : 36.8148

當(dāng)我們使用CUDA加速時,這使光流計算的速度提高了約17倍!但是不幸的是,我們生活在現(xiàn)實世界中,并不是所有的流程階段都可以加速。因此,對于整個流程,我們只能獲得約4倍的加速。

總結(jié)

本文我們概述了GPU OpenCV模塊并編寫了一個簡單的演示,以了解如何加速Farneback的Optical Flow算法。我們研究了OpenCV為該模塊提供的API,您也可以重用該API來嘗試使用CUDA加速OpenCV算法。

*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。



關(guān)鍵詞:

相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉