AI模型部署实战:利用OpenCV的CUDA模块加速视觉模型部署流程

本文首发于公众号【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版本的过程如下:

  1. 安装必要的依赖

在用源码编译安装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
  1. 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

下载好以后把两个包进行解压。

  1. 按照下面的步骤编译源码并进行安装:
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上调用的接口都是使用该数据结构作为输入或输出的。GpuMatMat的使用方式非常相似,封装的接口基本上是一致的,详细内容可以参考GpuMat的文档

CPUGPU之间的数据传输

OpenCV提供了非常简单的接口实现CPUGPU之间的数据传输,也就是cv::Matcv::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;
}

可以看到,CPUGPU版本调用的函数名是一样的,只不过GPU版本的多了一个cuda命名空间。所以使用OpenCVCUDA模块基本上是没有什么难度的,只需要查一下之前调用的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;
}

OpenCVCUDA流和原生的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代码了。

五. 参考资料

  1. CUDA-accelerated Computer Vision
  2. How To Run Inference Using TensorRT C++ API
  3. Using TensorRT with OpenCV CUDA
  4. Getting Started with OpenCV CUDA Module
  5. GPU-accelerated Computer Vision
  6. OpenCV CUDA samples
  • 22
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeepDriving

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值