本文首发于公众号【DeepDriving】,欢迎关注。
一. 前言
我在之前的文章《AI模型部署实战:利用CV-CUDA加速视觉模型部署流程》中介绍了如何使用CV-CUDA
库来加速视觉模型部署的流程,但是CV-CUDA
对系统版本和CUDA
版本的要求比较高,在一些低版本的系统中可能无法使用。对于像我这种不会写CUDA
代码又想用CUDA
来加速模型部署流程的人来说要怎么办呢,其实还有一种方式,那就是使用OpenCV的CUDA接口。
本文将介绍OpenCV CUDA
模块的基本使用方法(C++
),以及如何使用这些接口来加速视觉模型部署。
二. 安装CUDA
版本OpenCV
在Ubuntu 20.04
系统中使用apt install
命令安装OpenCV
是不会安装CUDA
模块的,要想使用CUDA
模块只能用源码进行编译安装。在Ubuntu
系统中用源码编译安装OpenCV 4.6
版本的过程如下:
- 安装必要的依赖
在用源码编译安装OpenCV
之前,需要先执行下面一系列命令安装必要的依赖:
sudo apt update
sudo apt upgrade
sudo apt install build-essential cmake pkg-config unzip yasm git checkinstall
sudo apt install libjpeg-dev libpng-dev libtiff-dev
sudo apt install libavcodec-dev libavformat-dev libswscale-dev libavresample-dev
sudo apt install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt install libxvidcore-dev x264 libx264-dev libfaac-dev libmp3lame-dev libtheora-dev
sudo apt install libfaac-dev libmp3lame-dev libvorbis-dev
sudo apt install libopencore-amrnb-dev libopencore-amrwb-dev
sudo apt-get install libdc1394-22 libdc1394-22-dev libxine2-dev libv4l-dev v4l-utils
cd /usr/include/linux
sudo ln -s -f ../libv4l1-videodev.h videodev.h
cd -
sudo apt-get install libgtk-3-dev
sudo apt-get install libtbb-dev
sudo apt-get install libatlas-base-dev gfortran
sudo apt-get install libprotobuf-dev protobuf-compiler
sudo apt-get install libgoogle-glog-dev libgflags-dev
sudo apt-get install libgphoto2-dev libeigen3-dev libhdf5-dev doxygen
sudo apt-get install opencl-headers
sudo apt-get install ocl-icd-libopencl1
- 从
GitHub
网站分别下载OpenCV 4.6.0
的源码包和扩展模块源码包
# 下载opencv-4.6.0源码包
https://github.com/opencv/opencv/archive/refs/tags/4.6.0.zip
#下载4.6.0对应的扩展模块源码包
https://github.com/opencv/opencv_contrib/archive/refs/tags/4.6.0.zip
下载好以后把两个包进行解压。
- 按照下面的步骤编译源码并进行安装:
cd opencv-4.6.0
mkdir build && cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D INSTALL_PYTHON_EXAMPLES=OFF \
-D INSTALL_C_EXAMPLES=OFF \
-D WITH_TBB=ON \
-D WITH_CUDA=ON \
-D BUILD_opencv_cudacodec=OFF \
-D ENABLE_FAST_MATH=1 \
-D CUDA_FAST_MATH=1 \
-D WITH_CUBLAS=1 \
-D WITH_V4L=OFF \
-D WITH_LIBV4L=ON \
-D WITH_QT=OFF \
-D WITH_GTK=ON \
-D WITH_GTK_2_X=ON \
-D WITH_OPENGL=ON \
-D WITH_GSTREAMER=ON \
-D OPENCV_GENERATE_PKGCONFIG=ON \
-D OPENCV_PC_FILE_NAME=opencv.pc \
-D OPENCV_ENABLE_NONFREE=ON \
-D CUDA_nppicom_LIBRARY=stdc++ \
-D OPENCV_PYTHON3_INSTALL_PATH=/usr/lib/python3/dist-packages \
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-4.6.0/modules \
-D PYTHON_EXECUTABLE=/usr/bin/python3 \
-D BUILD_EXAMPLES=OFF ..
make -j8 && sudo make install
CMake
的几个参数需要注意一下:
-D WITH_CUDA=ON # 这里必须设置为ON,否则无法使用CUDA模块
-D CMAKE_INSTALL_PREFIX=/usr/local # OpenCV的安装路径,可以按照自己的需求指定
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-4.6.0/modules # 扩展模型源码包的路径
因为
CMake
过程中要下载很多依赖文件,如果速度很慢,可以加上配置选项-DOPENCV_DOWNLOAD_MIRROR_ID=gitcode
,这样就可以从国内镜像下载了,速度会快很多。
安装成功后,还需要设置一下环境变量:
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/usr/local/opencv-4.6/lib/
三. OpenCV CUDA
模块的基本使用方法
OpenCV CUDA
模块的官方文档详细阐述了CUDA
模块提供的函数接口以及使用方法,在写代码之前我们应该好好学习一下这些文档。
基础数据结构GpuMat
在使用CPU
的时候,OpenCV
是使用数据结构cv::Mat
来作为数据容器的;而在GPU
上,则是使用一个新的数据结构cv::gpu::GpuMat
,所有在GPU
上调用的接口都是使用该数据结构作为输入或输出的。GpuMat
与Mat
的使用方式非常相似,封装的接口基本上是一致的,详细内容可以参考GpuMat的文档。
CPU
与GPU
之间的数据传输
OpenCV
提供了非常简单的接口实现CPU
与GPU
之间的数据传输,也就是cv::Mat
与cv::gpu::GpuMat
之间的转换:
upload
: 把数据从CPU
拷贝到GPU
上;download
: 把数据从GPU
拷贝到CPU
上;
下面是一个简单的示例:
#include <opencv2/opencv.hpp>
#include <opencv2/cudaimgproc.hpp>
cv::Mat img = cv::imread("test.jpg");
// 把数据从CPU拷贝到GPU上
cv::cuda::GpuMat gpu_mat;
gpu_mat.upload(img);
// 在GPU上对数据做处理
// 把结果从GPU拷贝到CPU上
cv::Mat result;
gpu_mat.download(result);
使用GPU
做图像预处理
在做视觉AI
模型部署时,图像数据预处理的基本流程如下:
1. 把OpenCV读取的BGR格式的图片转换为RGB格式;
2. 把图片resize到模型输入尺寸;
3. 对像素值做归一化操作;
4. 把图像数据的通道顺序由HWC调整为CHW;
以部署YOLOv6
模型为例,在CPU
上做图像预处理的的代码如下:
bool ImagePreProcessCpu(const cv::Mat &input_image, const int resize_width,
const int resize_height, const double alpha,
const double beta, float *const input_blob) {
if (input_image.empty()) {
return false;
}
if (input_blob == nullptr) {
return false;
}
// 这里默认输入图像是RGB格式
// resize
cv::Mat resize_image;
cv::resize(input_image, resize_image,cv::Size(resize_width, resize_height));
// 像素值归一化
cv::Mat float_image;
resize_image.convertTo(float_image, CV_32FC3, alpha, beta);
// 调整通道顺序,HWC->CHW
const int size = resize_width * resize_height;
std::vector<cv::Mat> input_channels;
cv::split(float_image, input_channels);
for (int c = 0; c < resize_image.channels(); ++c) {
std::memcpy(input_blob + c * size, input_channels[c].data,
size * sizeof(float));
}
return true;
}
调用OpenCV CUDA
模块的接口做预处理的代码如下:
bool ImagePreProcessGpu(const cv::Mat &input_image,const int resize_width,
const int resize_height,const double alpha,
const double beta,float *const input_blob) {
if (input_image.empty()) {
return false;
}
// 注意,这里input_blob是指向GPU内存
if (input_blob == nullptr) {
return false;
}
cv::cuda::GpuMat gpu_image, resize_image,float_image;
gpu_image.upload(input_image);
cv::cuda::resize(gpu_image, resize_image,
cv::Size(resize_width, resize_height), 0, 0,
cv::INTER_LINEAR);
resize_image.convertTo(float_image, CV_32FC3, alpha, beta);
const int size = resize_width * resize_height;
std::vector<cv::cuda::GpuMat> split_channels;
for (int i = 0; i < float_image.channels(); ++i) {
split_channels.emplace_back(
cv::cuda::GpuMat(cv::Size(resize_width, resize_height), CV_32FC1,
input_blob + i * size));
}
cv::cuda::split(float_image, split_channels);
return true;
}
可以看到,CPU
和GPU
版本调用的函数名是一样的,只不过GPU
版本的多了一个cuda
命名空间。所以使用OpenCV
的CUDA
模块基本上是没有什么难度的,只需要查一下之前调用的CPU
接口是否有对应的GPU
版本就可以了。
使用CUDA
流
CUDA
流是一系列异步操作的集合,通过在一个设备上并发地运行多个内核任务来实现任务的并发执行,这种方式使得设备的利用率更高。上面代码调用的OpenCV CUDA
模块接口都是没有使用CUDA
流的,不过CUDA
模块为每个函数都提供了一个使用CUDA
流的版本,使用起来也非常简单。
OpenCV CUDA
模块的CUDA
流封装在cv::cuda::Stream
类中,使用之前首先创建一个类对象
cv::cuda::Stream stream;
然后在调用每个CUDA
接口的时候传入该对象
gpu_image.upload(input_image,stream);
再在最后调用waitForCompletion()
函数进行同步,确保该流上的所有操作都已完成。
使用CUDA
流的图像预处理代码如下:
bool ImagePreProcessGpuStream(const cv::Mat &input_image,const int resize_width,
const int resize_height,const double alpha,
const double beta,float *const input_blob) {
if (input_image.empty()) {
return false;
}
if (input_blob == nullptr) {
return false;
}
cv::cuda::Stream stream;
cv::cuda::GpuMat gpu_image, resize_image,float_image;
gpu_image.upload(input_image,stream);
cv::cuda::resize(gpu_image, resize_image,
cv::Size(resize_width, resize_height), 0, 0,
cv::INTER_LINEA,stream);
resize_image.convertTo(float_image, CV_32FC3, alpha, beta,stream);
const int size = resize_width * resize_height;
std::vector<cv::cuda::GpuMat> split_channels;
for (int i = 0; i < float_image.channels(); ++i) {
split_channels.emplace_back(
cv::cuda::GpuMat(cv::Size(resize_width, resize_height), CV_32FC1,
input_blob + i * size));
}
cv::cuda::split(float_image, split_channels,stream);
stream.waitForCompletion();
return true;
}
OpenCV
的CUDA
流和原生的CUDA
流可以通过结构体cv::cuda::StreamAccessor
提供的两个静态函数进行转换:
// 把OpenCV的CUDA流转换为原生CUDA流
static cudaStream_t cv::cuda::StreamAccessor::getStream (const Stream & stream )
// 把原生CUDA流转换为OpenCV的CUDA流
static Stream cv::cuda::StreamAccessor::wrapStream (cudaStream_t stream)
如果对
CUDA
流不了解,可以参考我之前写的这篇文章。
四. 总结
本文介绍了OpenCV CUDA
模块中图像处理接口的基本使用方法,用这些CUDA
接口基本上可以满足视觉AI
模型的部署需求,在嵌入式平台上可以有效减少CPU
资源的消耗。当然,OpenCV
提供的CUDA
版本接口也有限,必要的时候也只能自己手搓CUDA
代码了。
五. 参考资料
- CUDA-accelerated Computer Vision
- How To Run Inference Using TensorRT C++ API
- Using TensorRT with OpenCV CUDA
- Getting Started with OpenCV CUDA Module
- GPU-accelerated Computer Vision
- OpenCV CUDA samples