提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
操作系统:Ubuntu18.04
硬件架构:x86_64
OpenCV:4.5.1
FFmpeg:4.4.2
CUDA:11.2
最近遇到一个新项目,AI推理在CUDA
上,为了方便和节省成本的考虑决定研究下NVCODEC
模块。根据NVIDIA
官网的说法显卡具有独立的编码
和解码
模块,所以理论上编码
和解码
是独立互不干涉的。以前的项目都只是把显卡当成推理工具,没有将它的编解码
功能利用起来,本身也是一种浪费。事实真的有这么丝滑吗?看到这里,如果你真的觉得黄一刀
是白叫的那就真的是too young to simple
了,事实究竟如何,请听我娓娓道来。
NVCODEC全称是 NVIDIA VIDEO CODEC,是NVIDIA为绝大多数显卡配置的硬件编解码单元。不一定是所有显卡都有,一般来讲越新的显卡硬件编解码就越强,支持的格式也就越新。比如,拿消费端的显卡来讲,只有RTX3000
以上的显卡才支持AV1
这种新格式,像RTX2000
以下的显卡目前无缘。当然以后还有可能会出更多格式。目前我所知道的NVIDIA解码支持两种方式:
1、NVCODEC编解码
这种解码方式是我们这篇文章要讲的主角,也就是硬件编解码。这里的硬件编解码指的是利用专门运算单元
来完成编解码过程,这种专门运算单元
被设计出来就是固定只干这一种活的硬件,跟软解码最大的差别是,软编解码是利用通用运算单元
来硬算
。
2、CUDA编解码
通常情况下CUDA
是拿来做AI推理
的,但是有些特殊情况下CUDA
也是可以直接拿来编解码
的,这个时候CUDA
就是通用运算单元
了,因为它不是专门设计来做编解码
工作的,属于被迫上岗。
我们看看NVIDIA自己怎么介绍自家的东西的:
NVIDIA GPUs contain one or more hardware-based decoder and encoder(s) (separate from the CUDA cores) which provides fully-accelerated hardware-based video decoding and encoding for several popular codecs. With decoding/encoding offloaded, the graphics engine and the CPU are free for other operations.
GPU hardware accelerator engines for video decoding (referred to as NVDEC) and video encoding (referred to as NVENC) support faster than real-time video processing which makes them suitable to be used for transcoding applications, in addition to video playback.
从上图我们看出,CUDA
和编解码
是独立的硬件,所以他们是分别独立工作的,不会相互干扰。
好了,闲话已经说的差不多了,接下来我们进入正题。将opencv
和nvcodec
结合起来完成视频的解码,并将解码后的GpuMat
送入CUDA
推理。
这个地方要重点讲一下,安装CUDA
的时候是可以顺手把Driver
装上的,这里我推荐用这种方式,因为这种方式安装的驱动自带NVCODEC
的库和头文件,这样的话我们就不用专门去下载NVIDIA VIDEO CODEC SDK
了。而且我发现NVIDIA VIDEO CODEC SDK
和Driver
有版本依赖,弄不好会出各种奇葩问题。所以,这里讲的所有教程都是基于同时安装CUDA
+Driver
的,至于怎么安装CUDA
和Driver
请大家自行搜索教程,由于篇幅有限这里不做详述。
驱动安装好之后需要检验是否正确,如果Driver
安装正确执行nvidia-smi
会出现Driver
的详细信息,如下图所示:
这里我们说说最关键的两个信息Driver Version:460.27.04
,这个意思是你安装的显卡驱动版本是460.27.04
;CUDA Version:11.2
,这个的意思是和Driver
配套的CUDA版本是11.2
,就算你不装CUDA
这个信息也会显示的。我建议你接受它的建议,就装那个版本的·CUDA·,除非有特殊需要,你就需要去NVIDIA
的官网查询下具体支持的CUDA
版本了。众所周知,Driver
和CUDA
版本是存在依赖关系的,乱装是要出事情的。
接下来验证CUDA
是不是装好了,命令行输入nvcc -V
会看到以下的提示信息,说明CUDA
安装成功了,
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2020 NVIDIA Corporation
Built on Mon_Nov_30_19:08:53_PST_2020
Cuda compilation tools, release 11.2, V11.2.67
Build cuda_11.2.r11.2/compiler.29373293_0
如果报了类似于command not fount
一类的错误就说明CUDA
没有装对或者环境变量没配置对,请检查下操作是否正确。
差点忘了,CuDNN
也是需要的,千万别忘了装。CuDNN
的验证比较简单,那就是不需要验证,你从官网下载3个deb
包全部成功安装后就说明CuDNN
安装成功了,不要有所怀疑。
下载opencv
源代码这一步就不赘述了,这里我下载的是opencv-4.5.1.zip
或者opencv-4.5.1.tar.gz
两个包本质没有差别,只是压缩方式有差别罢了,没有影响。我们这里是需要opencv_contrib
的,所以需要下载opencv_contrib-4.5.1.zip
或opencv_contrib-4.5.1.tar.gz
,这两个压缩包也是一样的,一样用。
在这里要特别注意下opencv和opencv_contrib是有版本对应关系的,不能下错。
opencv的编译我不在这里详述了,网络上一抓一大把,这里我贴出来我的配置。
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D ENABLE_PRECOMPILED_HEADERS=OFF \
-D INSTALL_C_EXAMPLES=OFF \
-D INSTALL_PYTHON_EXAMPLES=OFF \
-D BUILD_opencv_python2=OFF \
-D BUILD_opencv_python3=ON \
-D PYTHON_DEFAULT_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") \
-D PYTHON3_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") \
-D PYTHON3_NUMPY_INCLUDE_DIRS=$(python3 -c "import numpy; print (numpy.get_include())") \
-D PYTHON3_PACKAGES_PATH=$(python3 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") \
-D WITH_TBB=ON \
-D BUILD_TBB=ON \
-D ENABLE_FAST_MATH=1 \
-D CUDA_FAST_MATH=1 \
-D WITH_CUBLAS=1 \
-D WITH_V4L=ON \
-D WITH_LIBV4L=ON \
-D WITH_CUDA=ON \
-D WITH_CUDNN=ON \
-D WITH_GTK_2_X=ON \
-D WITH_NVCUVID=ON \
-D WITH_OPENGL=ON \
-D WITH_FFMPEG=ON \
-D CUDA_ARCH_BIN=7.5 \
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-4.5.1/modules .
由于这里我用到了python cv2
所以我编译了python
模块。我的需求里面有拉流的需要,编译了ffmpeg
模块。CUDA_ARCH_BIN=7.5
不同的显卡不一样,我的显卡是RTX2080Ti
,我是7.5,具体值都可以在NVIDIA官网查到。WITH_NVCUVID=ON
这个一定要加上,这个就是编解码模块。
友情提示:
有个问题需要特别注意下,这种方式OpenCV
是找不到NVCUVID
的,原因是以前NVCUVID
和CUDA
放在一起的,后来NVIDIA
将NVCUVID
独立出来了,OpenCV
的检测方式还是老的方式,所以找不到NVCUVID
,解决方法也很简单,你只需要到opencv-4.5.1/cmake
文件夹里找到OpenCVDetectCUDA.cmake
if(WITH_NVCUVID)
macro(ocv_cuda_SEARCH_NVCUVID_HEADER _filename _result)
# place header file under CUDA_TOOLKIT_TARGET_DIR or CUDA_TOOLKIT_ROOT_DIR
find_path(_header_result
${_filename}
PATHS "${CUDA_TOOLKIT_TARGET_DIR}" "${CUDA_TOOLKIT_ROOT_DIR}"
ENV CUDA_PATH
ENV CUDA_INC_PATH
PATH_SUFFIXES include
NO_DEFAULT_PATH
)
if("x${_header_result}" STREQUAL "x_header_result-NOTFOUND")
set(${_result} 0)
else()
set(${_result} 1)
endif()
unset(_header_result CACHE)
endmacro()
ocv_cuda_SEARCH_NVCUVID_HEADER("nvcuvid.h" HAVE_NVCUVID_HEADER)
ocv_cuda_SEARCH_NVCUVID_HEADER("dynlink_nvcuvid.h" HAVE_DYNLINK_NVCUVID_HEADER)
find_cuda_helper_libs(nvcuvid)
if(WIN32)
find_cuda_helper_libs(nvcuvenc)
endif()
if(CUDA_nvcuvid_LIBRARY AND (${HAVE_NVCUVID_HEADER} OR ${HAVE_DYNLINK_NVCUVID_HEADER}))
# make sure to have both header and library before enabling
set(HAVE_NVCUVID 1)
endif()
if(CUDA_nvcuvenc_LIBRARY)
set(HAVE_NVCUVENC 1)
endif()
endif()
注意这一句
PATHS "${CUDA_TOOLKIT_TARGET_DIR}" "${CUDA_TOOLKIT_ROOT_DIR}"
是不是感觉很熟悉,这就是CUDA
的安装目录,由于现在NVCUVID
和CUDA
不在一个目录了,所以只需要改成下面这样
PATHS "${CUDA_TOOLKIT_TARGET_DIR}" "${CUDA_TOOLKIT_ROOT_DIR}" "/usr/include"
/usr/include是NVCUVID的头文件位置,你的在哪里就写什么目录就行了,改完执行cmake配置
如果cmake
出错就缺什么安装什么就行了,这个我是亲身体验的,没有问题的。假如没有任何错误,你看到的应该是这样的:
Video I/O:
DC1394: NO
FFMPEG: YES
avcodec: YES (58.134.100)
avformat: YES (58.76.100)
avutil: YES (56.70.100)
swscale: YES (5.9.100)
avresample: YES (4.0.0)
GStreamer: YES (1.14.5)
v4l/v4l2: YES (linux/videodev2.h)
NVIDIA CUDA: YES (ver 11.2, CUFFT CUBLAS NVCUVID FAST_MATH)
NVIDIA GPU arch: 75
NVIDIA PTX archs:
cuDNN: YES (ver 8.1.0)
Python 3:
Interpreter: /usr/bin/python3 (ver 3.6.9)
Libraries: /usr/lib/x86_64-linux-gnu/libpython3.6m.so (ver 3.6.9)
numpy: /usr/local/lib/python3.6/dist-packages/numpy/core/include (ver 1.19.5)
install path: /usr/lib/python3/dist-packages/cv2/python-3.6
Python (for build): /usr/bin/python3
FFMPEG
一定都要是YES
,不能是NO
;NVIDIA CUDA: YES (ver 11.2, CUFFT CUBLAS NVCUVID FAST_MATH)
这一行一定要有NVCUVID
,不然就是错误了。配置好后执行make -j$(nproc)
等待编译完成执行sudo make install
就可以了。这里有个地方需要注意,一定要把旧的opencv
卸载干净,不然就会引发冲突或者未知错误。
注:ffmpeg的安装可以参考这篇文章
解决opencv源代码编译找不到ffmpeg
到这里我们的教程就算完了,如果一切顺利的话就可以使用NVCODEC
来处理视频流了,RTSP
格式的实时视频也是支持的,取出来的帧保存在GpuMat
里面,可以送进去推理了。实测CPU
解码帧率只有30
左右(和CPU
性能相关),GPU
解码帧率7600
多(和解码器
性能相关),差距还是蛮大的。下面贴出测试代码:
opencv_test.cpp
#include
#include
#include
#include
#include
#include "opencv2/opencv_modules.hpp"
#include
#include
#include
#include
#include
int main(int argc, const char* argv[])
{
std::cout<<cv::getBuildInformation()<<std::endl;
//将这个流改成你自己的
const std::string fname = "rtsp://admin:[email protected]";
cv::cuda::setGlDevice();
//cv::cuda::setGlDevice(1);
cv::Mat frame;
cv::VideoCapture reader(fname);
cv::cuda::GpuMat d_frame;
cv::Ptr<cv::cudacodec::VideoReader> d_reader = cv::cudacodec::createVideoReader(fname);
cv::TickMeter tm;
std::vector<double> cpu_times;
std::vector<double> gpu_times;
for (int i = 0;i<100;i++)
{
tm.reset(); tm.start();
if (!reader.read(frame))
break;
tm.stop();
cpu_times.push_back(tm.getTimeMilli());
tm.reset(); tm.start();
if (!d_reader->nextFrame(d_frame))
break;
tm.stop();
gpu_times.push_back(tm.getTimeMilli());
}
if (!cpu_times.empty() && !gpu_times.empty())
{
std::cout << std::endl << "Results:" << std::endl;
std::sort(cpu_times.begin(), cpu_times.end());
std::sort(gpu_times.begin(), gpu_times.end());
double cpu_avg = std::accumulate(cpu_times.begin(), cpu_times.end(), 0.0) / cpu_times.size();
double gpu_avg = std::accumulate(gpu_times.begin(), gpu_times.end(), 0.0) / gpu_times.size();
std::cout << "CPU : Avg : " << cpu_avg << " ms FPS : " << 1000.0 / cpu_avg << std::endl;
std::cout << "GPU : Avg : " << gpu_avg << " ms FPS : " << 1000.0 / gpu_avg << std::endl;
}
return 0;
}
// #endif
CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)
project(opencv_test)
SET(CMAKE_BUILD_TYPE "Debug")
include_directories(include)
find_package( OpenCV REQUIRED )
#find_package(OpenGL REQUIRED)
include_directories(
${OpenCV_INCLUDE_DIRS}
#${OPENGL_INCLUDE_DIR}
)
add_executable( opencv_test opencv_test.cpp )
#add_executable( opencv_test gpu_mat.cpp )
target_link_libraries( opencv_test
${OpenCV_LIBS}
#${OPENGL_LIBRARIES}
)
mkdir build && cd build
cmake ..
make
./opencv_test
特别说明:
//默认执行设备,如果是单显卡请忽略,如果多显卡需要指定哪一个设备执行,默认是0
cv::cuda::setGlDevice();
//cv::cuda::setGlDevice(1);
写到这里不得不感叹下,虽然只有区区几行命令却足足搞了一个星期。期间什么奇葩的错误都遇到了,甚至有些我认为是不必要出现的。
最典型的一个例子就是NVCODEC SDK,很多博主都说要从官网下载然后复制到系统目录,一开始我是这么做的,看起来好像全程没有遇到问题,直到最后一步跑程序的时候,终于所有的错误都来了,甚至遇到的最奇葩的问题是每次执行程序返回的错误码都是不一样的,这种BUG我也是生平仅见。最后甚至连Deiver都需要重新安装来解决。
事实真的如此吗?我真的需要每一步都照抄别人吗?实际上下载的CUDA Driver里面就有NVCODEC的SDK,不需要去官方下。只不过安装位置被从CUDA目录移除出来了,只需要修改opencv的检测方式就行了。