本文首發于公眾號【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.0mkdir build && cd buildcmake -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格式// resizecv::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->CHWconst 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