提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


系统配置

操作系统:Ubuntu18.04
硬件架构:x86_64
OpenCV:4.5.1
FFmpeg:4.4.2
CUDA:11.2

前言`

最近遇到一个新项目,AI推理在CUDA上,为了方便和节省成本的考虑决定研究下NVCODEC模块。根据NVIDIA官网的说法显卡具有独立的编码解码模块,所以理论上编码解码是独立互不干涉的。以前的项目都只是把显卡当成推理工具,没有将它的编解码功能利用起来,本身也是一种浪费。事实真的有这么丝滑吗?看到这里,如果你真的觉得黄一刀是白叫的那就真的是too young to simple了,事实究竟如何,请听我娓娓道来。


一、NVCODEC是什么?

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编解码是独立的硬件,所以他们是分别独立工作的,不会相互干扰。

好了,闲话已经说的差不多了,接下来我们进入正题。将opencvnvcodec结合起来完成视频的解码,并将解码后的GpuMat送入CUDA推理。

二、OpenCV编译

1.安装Driver&CUDA

这个地方要重点讲一下,安装CUDA的时候是可以顺手把Driver装上的,这里我推荐用这种方式,因为这种方式安装的驱动自带NVCODEC的库和头文件,这样的话我们就不用专门去下载NVIDIA VIDEO CODEC SDK了。而且我发现NVIDIA VIDEO CODEC SDKDriver有版本依赖,弄不好会出各种奇葩问题。所以,这里讲的所有教程都是基于同时安装CUDA+Driver的,至于怎么安装CUDADriver请大家自行搜索教程,由于篇幅有限这里不做详述。

驱动安装好之后需要检验是否正确,如果Driver安装正确执行nvidia-smi会出现Driver的详细信息,如下图所示:
在这里插入图片描述

这里我们说说最关键的两个信息Driver Version:460.27.04,这个意思是你安装的显卡驱动版本是460.27.04CUDA Version:11.2,这个的意思是和Driver配套的CUDA版本是11.2,就算你不装CUDA这个信息也会显示的。我建议你接受它的建议,就装那个版本的·CUDA·,除非有特殊需要,你就需要去NVIDIA的官网查询下具体支持的CUDA版本了。众所周知,DriverCUDA版本是存在依赖关系的,乱装是要出事情的。

接下来验证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安装成功了,不要有所怀疑。

2.编译OpenCV

下载opencv源代码这一步就不赘述了,这里我下载的是opencv-4.5.1.zip或者opencv-4.5.1.tar.gz两个包本质没有差别,只是压缩方式有差别罢了,没有影响。我们这里是需要opencv_contrib的,所以需要下载opencv_contrib-4.5.1.zipopencv_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的,原因是以前NVCUVIDCUDA放在一起的,后来NVIDIANVCUVID独立出来了,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的安装目录,由于现在NVCUVIDCUDA不在一个目录了,所以只需要改成下面这样

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,不能是NONVIDIA 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 <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <numeric>
#include "opencv2/opencv_modules.hpp"
#include <opencv2/core/utility.hpp>
#include <opencv2/core.hpp>
#include <opencv2/core/opengl.hpp>
#include <opencv2/cudacodec.hpp>
#include <opencv2/highgui.hpp>

int main(int argc, const char* argv[])
{
   std::cout<<cv::getBuildInformation()<<std::endl;
   //将这个流改成你自己的
   const std::string fname = "rtsp://admin:Wat0ne123@10.0.20.249";
    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的检测方式就行了。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐