前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文详解OpenCV中的CUDA模块

一文详解OpenCV中的CUDA模块

作者头像
3D视觉工坊
发布2021-03-19 12:07:51
4.9K0
发布2021-03-19 12:07:51
举报

如果您使用OpenCV已有一段时间,那么您应该已经注意到,在大多数情况下,OpenCV都使用CPU,这并不总能保证您所需的性能。为了解决这个问题,OpenCV在2010年增加了一个新模块,该模块使用CUDA提供GPU加速。您可以在下面找到一个展示GPU模块优势的基准测试:

简单列举下本文要交代的几个事情:

  • 概述已经支持CUDA的OpenCV模块。
  • 看一下cv :: gpu :: GpuMat(cv2.cuda_GpuMat)。
  • 了解如何在CPU和GPU之间传输数据。
  • 了解如何利用多个GPU。
  • 编写一个简单的演示(C ++和Python),以了解OpenCV提供的CUDA API接口并计算我们可以获得的性能提升。

一、支持的模块

据称,尽管并未涵盖所有库的功能,但该模块“仍在继续增长,并正在适应新的计算技术和GPU架构。”

让我们看一下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

为了将数据保留在GPU内存中,OpenCV引入了一个新的类cv :: gpu :: GpuMat(或Python中的cv2.cuda_GpuMat)作为主要数据容器。其界面类似于cv :: Mat(cv2.Mat),从而使向GPU模块的过渡尽可能平滑。值得一提的是,所有GPU函数都将GpuMat接收为输入和输出参数。通过这种在代码中链接了GPU算法的设计,您可以减少在CPU和GPU之间复制数据的开销。

三、CPU/GUP数据传递

要将数据从GpuMat传输到Mat,反之亦然,OpenCV提供了两个函数:

  • 上传,将数据从主机内存复制到设备内存
  • 下载,将数据从设备内存复制到主机内存。

以下是用C ++写的一个简单示例:

代码语言:javascript
复制
#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的使用

默认情况下,每种OpenCV CUDA算法都使用单个GPU。如果需要利用多个GPU,则必须在GPU之间手动分配工作。要切换活动设备,请使用cv :: cuda :: setDevice(cv2.cuda.SetDevice)函数。

五、代码示例

OpenCV提供了有关如何使用C ++ API在GPU支持下与已实现的方法一起使用的示例。让我们在使用Farneback的算法进行密集光流计算的示例中,实现一个简单的演示,演示如何将CUDA加速的OpenCV与C ++一起使用。

我们首先来看一下如何使用CPU来完成此操作。然后,我们将使用GPU进行相同的操作。最后,我们将比较经过的时间以计算获得的加速比。

FPS计算

由于我们的主要目标是找出算法在不同设备上的运行速度,因此我们需要选择测量方法。在计算机视觉中,这样做的常用方法是计算每秒处理的帧数(FPS)。

CPU端

1.视频及其属性

我们将从视频捕获初始化开始,并获取其属性,例如帧频和帧数。这部分是CPU和GPU部分的通用部分:

代码语言:javascript
复制
// 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.读取第一帧

由于算法的特殊性,该算法使用两帧进行计算,因此我们需要先读取第一帧,然后再继续。还需要一些预处理,例如调整大小并转换为灰度:

代码语言:javascript
复制
// 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.读取并预处理其他帧

在循环读取其余帧之前,我们启动两个计时器:一个计时器将跟踪整个流程的工z作时间,第二个计时器–读取帧时间。由于Farneback的光流法适用于灰度帧,因此我们需要确保将灰度视频作为输入传递。这就是为什么我们首先对其进行预处理以将每帧从BGR格式转换为灰度的原因。另外,由于原始分辨率可能太大,因此我们将其调整为较小的尺寸,就像对第一帧所做的一样。我们再设置一个计时器来计算在预处理阶段花费的时间:

代码语言:javascript
复制
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的方法来计算两帧之间的密集光流:

代码语言:javascript
复制
// 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的“光流法“输出二维流矢量。我们将这些输出转换为极坐标,以通过色相获得流动的角度(方向),并通过HSV颜色表示的值获得流动的距离(幅度)。对于可视化,我们现在要做的就是将结果转换为BGR空间。之后,我们停止所有剩余的计时器以获取经过的时间:

代码语言:javascript
复制
// 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.可视化

我们将尺寸调整为960×540的原始帧可视化,并使用imshow函数显示结果:

代码语言:javascript
复制
// visualization 
imshow("original", frame); 
imshow("result", bgr); 
int keyboard = waitKey(1); 
if (keyboard == 27) 
    break;

这是一个示例“ boat.mp4”视频的内容:

7.时间和FPS计算

我们要做的就是计算流程中每一步花费的时间,并测量光流部分和整个流程的FPS:

代码语言:javascript
复制
// 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函数来读取和调整大小,但是将结果上传到cv :: cuda :: GpuMat(cuda_GpuMat)实例:

代码语言:javascript
复制
// 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.读取和预处理其它帧

代码语言:javascript
复制
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)创建cudaFarnebackOpticalFlow类的实例,然后调用cv :: cuda:FarnebackOpticalFlow :: calc(cv2.cuda_FarnebackOpticalFlow.calc)计算两个帧之间的光流,而不是使用cv :: calcOpticalFlowFarneback(cv2.calcOpticalFlowFarneback)函数调用。

代码语言:javascript
复制
// 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变体:

代码语言:javascript
复制
// 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端相同。

结果

现在,我们可以在示例视频中比较来自CPU和GPU版本的指标。

我们用于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

当我们使用CUDA加速时,这使光流计算的速度提高了约17倍!但是不幸的是,我们生活在现实世界中,并不是所有的流程阶段都可以加速。因此,对于整个流程,我们只能获得约4倍的加速。

总结

本文我们概述了GPU OpenCV模块并编写了一个简单的演示,以了解如何加速Farneback的Optical Flow算法。我们研究了OpenCV为该模块提供的API,您也可以重用该API来尝试使用CUDA加速OpenCV算法。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 3D视觉工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档