使用nvjpeg解码jpeg图像

《TensorRT全流程部署指南》专栏文章目录:

1、nvjpeg速度测试

在深度学习的应用场景中,常常会涉及接收来自多个相机的图像数据,例如在监控系统、自动驾驶多传感器融合等领域。然而,大量的图像数据传输会消耗大量的带宽,为了解决这一问题,在传输过程中通常会对图像进行JPEG编码。通过这种编码方式,可以在保证一定图像质量的前提下,大大减少数据量,从而加快数据传输速度。但这也带来新的问题:在对图像进行模型推理之前,我们需要先对这些JPEG编码的数据进行解码,然后再开展后续的预处理和推理工作。

图像的解码效率在很大程度上直接影响整个系统的执行速度。设想一下,如果解码过程过于缓慢,模型就不得不一直处于等待图像解码完成的停滞状态,这无疑会导致显卡性能的大量浪费,降低系统的整体性能和效率。

为了解决深度学习场景中JPEG图像解码效率的问题,NVIDIA 推出了一款专门利用显卡进行JPEG解码的库——NVJPEG。这款库针对高分辨率图像的解码进行了优化设计,能够显著提升JPEG图像的解码速度。

为了直观地感受NVJPEG解码的高效,我们进行了以下简单的测试。测试环境为Intel i7 - 12700KF处理器、NVIDIA 4070Ti显卡以及Ubuntu20.04操作系统。

首先,使用OpenCV库将一张图片编码为JPEG格式,得到JPEG编码数据 jpegBytes,相关代码如下:

// 编码为JPEG格式
std::vector<int> params;
params.push_back(cv::IMWRITE_JPEG_QUALITY);
params.push_back(85); // JPEG质量,范围是0 - 100,95是常用值

bool success = cv::imencode(".jpg", image, jpegBytes, params); // 得到jpeg编码数据

接着,使用OpenCV和NVJPEG分别对得到的编码数据进行解码测试,测试结果如下:

算法8000*30001024*1024
OpenCV87ms2.6ms
NVJPEG25ms0.7ms

从测试结果可以看出,无论是大分辨率(8000*3000)还是小分辨率(1024*1024)的图像解码,NVJPEG的速度都比OpenCV解码快约3.5倍。这充分证明了NVJPEG在JPEG图像解码方面具有非常高的效率。

在实际的深度学习项目中,将NVJPEG集成进来是一个优化系统性能的有效手段,可以在不牺牲图像质量的前提下,显著提升整个系统的数据处理效率和性能,让计算资源得到更充分的利用。

2、安装nvjpeg

nvjpeg官网:nvJPEG Libraries

文档:nvJPEG,但是注意右上角显示的CUDA版本信息,不同版本可能存在较大的变动(比如12.8之前的硬件加速仅支持A100, A30, H100显卡,而12.8版本支持Ampere (A100, A30)、Hopper、Ada、Blackwell架构的显卡)。

根据官网的说明,CUDA10.0或者9.0需要手动安装,更新的CUDA版本则已经集成nvJPEG库了。
在这里插入图片描述
可以通过nvjpeg头文件查询nvJPEG版本,可以看到通过cuda-11.8中的nvjpeg头文件,查询到nvjpeg版本为11.9.0.86:

cat /usr/local/cuda-11.8/include/nvjpeg.h | grep NVJPEG_VER_

#define NVJPEG_VER_MAJOR 11
#define NVJPEG_VER_MINOR 9
#define NVJPEG_VER_PATCH 0
#define NVJPEG_VER_BUILD 86

所以如果你已经安装了较新版本的CUDA,那么已经用了有nvjpeg库。

上面进行的测试是显卡软解码,效果也非常好。但如果你想使用一些nvjpeg的新特性,比如硬件加速,则需要安装指定的依赖了。根据文档描述支持硬件加速,右上角显示v12.8版本,说明需要安装CUDA12.8才可以使用具有该特性的nvjpeg库。
在这里插入图片描述
找到CUDA12.8的发行说明:CUDA 12.8 Release Notes,可以看到安装CUDA12.8需要的驱动版本。
在这里插入图片描述
至此我们知道了所需要的显卡驱动版本和CUDA版本,去搜索是否有合适自己显卡的驱动。如果没有可能就暂时没法使用指定版本的nvjpeg特性了。

3、简单的例子

如果属性CUDA或CUDA库,则使用起来非常顺手。基本流程就是创建管理对象,设置图像尺寸用于初始化接受解码后图像数据的nvjpegImage_t对象,然后解码,最后将解码数据复制到另一个显存地址。

如果使用TensorRT部署模型,模型接受的输入必须是在显存中,所以这里就很方便地将解码后的数据直接给模型推理了。如果是预处理可以使用CUDA核函数,核函数的输入(来自解码后的数据)和输出(喂给模型推理)都是在显存中,也是非常方便。

#include "nvjpeg.h"

#ifndef CUDA_CHECK
#define CUDA_CHECK(callstr)                                                                                                                                                           \
    {                                                                                                                                                                                 \
        cudaError_t error_code = callstr;                                                                                                                                             \
        if (error_code != cudaSuccess)                                                                                                                                                \
        {                                                                                                                                                                             \
            std::cerr << "\033[31mCUDA error code: " << error_code << "\nName: " << cudaGetErrorName(error_code) << "\nat " << __FILE__ << ":" << __LINE__ << "\033[0m" << std::endl; \
            ;                                                                                                                                                                         \
            throw std::runtime_error("CUDA_CHECK ERROR");                                                                                                                             \
        }                                                                                                                                                                             \
    }
#endif

int main()
{

    nvjpegHandle_t handle;
    nvjpegJpegState_t state;

    nvjpegCreate(NVJPEG_BACKEND_DEFAULT, NULL, &handle); // 创建环境上下文
    nvjpegJpegStateCreate(handle, &state);               // 创建加码状态对象,管理状态

    int Height = 8000; // 指定图像尺寸
    int Width = 3000;
    int out_step = Width;
    int out_size = out_step * Height;

    nvjpegImage_t out_buf; // 创建nvjpegImage_t对象,用于存放解码后图像数据
    out_buf.pitch[0] = out_step;
    cudaMalloc((void **)&out_buf.channel[0], out_size); // 分配显存

    nvjpegDecode(handle, state, jpegBytes.data(), jpegBytes.size(), NVJPEG_OUTPUT_UNCHANGED, &out_buf, nullptr); // 解码

    // 解码后数据复制到推理的显存中
    cudaMemcpyAsync(decoded_cuda_buffer, out_buf.channel[0], out_size * sizeof(unsigned char), cudaMemcpyDeviceToDevice);
}

4、完整的编解码

现在尝试读取jpg文件并进行解码,已经编码后写入到jpg文件中

解码代码:

#include <fstream>
#include <vector>
#include <iostream>
#include <cstdint>
#include <opencv2/opencv.hpp>
#include <nvjpeg.h>

// nvcc decode.cpp -o decode `pkg-config --cflags --libs opencv4` -lnvjpeg [-lcudart]
int main()
{
   // 以二进制方式打开文件,并关联到输入文件流in_fs
    std::ifstream in_fs("00001.jpg", std::ifstream::binary);
    in_fs.seekg(0, std::ios::end); // seekg将文件指针移动到文件尾
    auto in_size = in_fs.tellg();  // 获取文件指针的位置,对应了文件大小

   // vector.data=返回一个指向内存数组的直接指针:uchar *
    std::vector<unsigned char> in_buf(in_size); // 创建Vetor存放二进制数据
    in_fs.seekg(0);                             // 将文件指针定位到开头
    in_fs.read((char *)in_buf.data(), in_size);  //从in_fs读取in_size个数据,储存到in_buf

    // 初始化nvJPEG,建立handle与state
    nvjpegHandle_t handle;
    nvjpegJpegState_t state;
    nvjpegCreateSimple(&handle);    // 创建句柄
    nvjpegJpegStateCreate(handle, &state);  // 创建 JPEG 状态

   // 获取图像长宽、通道数、subsampling等信息
    int widths[NVJPEG_MAX_COMPONENT];
    int heights[NVJPEG_MAX_COMPONENT];
    int channels;
    nvjpegChromaSubsampling_t subsampling;
    nvjpegGetImageInfo(handle, in_buf.data(), in_size, &channels, &subsampling, widths, heights);  // 该函数从 JPEG 编码的图像中检索宽度和高度信息

     // 计算输出行宽度与总大小
    int mul = 3;
    int out_step = widths[0] * mul;
    int out_size = out_step * heights[0];  // out_size=channel*widths*height

    // 设置输出信息,并在显存上申请缓存
    nvjpegImage_t out_buf;  // 输出buffer
    out_buf.pitch[0] = out_step;   // 设置行长
    cudaMalloc((void**)&out_buf.channel[0], out_size); // out_size=channel*widths*height

    auto ret = nvjpegDecode(handle, state, in_buf.data(), in_size, NVJPEG_OUTPUT_BGRI, &out_buf, nullptr);

    // 根据长宽建立opencv图像
    cv::Mat image(heights[0], widths[0], CV_8UC3);
    //  输出格式NVJPEG_OUTPUT_BGRI与opencv的默认格式完全相同
    cudaMemcpy(image.data, out_buf.channel[0], out_size, cudaMemcpyDeviceToHost);

    cv::imshow("test",image);
    cv::waitKey(0);

    cudaFree(out_buf.channel[0]);
    nvjpegJpegStateDestroy(state);
    nvjpegDestroy(handle);
}

编码代码:

#include <fstream>
#include <vector>
#include <iostream>
#include <cstdint>
#include <opencv2/opencv.hpp>
#include <nvjpeg.h>

void encode(cv::Mat imgmat)
{

    nvjpegHandle_t nv_handle;
    nvjpegEncoderState_t nv_enc_state;
    nvjpegEncoderParams_t nv_enc_params;
    cudaStream_t stream;

    // initialize nvjpeg structures
    nvjpegCreateSimple(&nv_handle);
    nvjpegEncoderStateCreate(nv_handle, &nv_enc_state, stream);
    nvjpegEncoderParamsCreate(nv_handle, &nv_enc_params, stream);
    cudaStreamSynchronize(stream);

    // 设置编码参数
    nvjpegEncoderParamsSetEncoding(nv_enc_params, nvjpegJpegEncoding_t::NVJPEG_ENCODING_PROGRESSIVE_DCT_HUFFMAN, NULL);
    nvjpegEncoderParamsSetOptimizedHuffman(nv_enc_params, 1, NULL);
    nvjpegEncoderParamsSetQuality(nv_enc_params, 70, NULL);
    nvjpegEncoderParamsSetSamplingFactors(nv_enc_params, nvjpegChromaSubsampling_t::NVJPEG_CSS_420, NULL);

    // typedef struct
    // {
    //     unsigned char *channel[NVJPEG_MAX_COMPONENT];
    //     size_t pitch[NVJPEG_MAX_COMPONENT];    // 每个通道平面的宽度比如3*640*480,则前3个pitch值都为640。这是因为实际存储是一维,直到宽度就可以变换为二维
    // } nvjpegImage_t;
    // channel[]是uchar*类型数据的数组,4个通道分别储存,如果是RGB则前3个通道分别储存RGB分量 #define NVJPEG_MAX_COMPONENT 4
    // nvjpegImage_t数据位于cuda,所以要为使用到的通道分配显存,如cudaMalloc((void **)&(nv_image.channel[i]), channel_size);为第i个通道分配channel_size(w*h)的显存
    nvjpegImage_t nv_image;

    int image_width = imgmat.cols;
    int image_height = imgmat.rows;
    int channel_size = image_width * image_height;

    // 拆分图像为单独的通道
    std::vector<cv::Mat> cvimgchannels;  // BGR
    cv::split(imgmat, cvimgchannels);

    // nv_image.pitch=[width,width,width,width]
    // nv_image.channel=[channel1_data,channel2_data,channel3_data,channel4_data]
    for (int i = 0; i < 3; i++)
    {
        nv_image.pitch[i] = image_width;
        cudaMalloc((void **)&(nv_image.channel[i]), channel_size); // 给第i个channel分配内存
        cudaMemcpy(nv_image.channel[i], cvimgchannels[i].data, channel_size, cudaMemcpyHostToDevice);
    }

    // Compress image
    nvjpegEncodeImage(nv_handle, nv_enc_state, nv_enc_params,
                      &nv_image, NVJPEG_INPUT_BGR, image_width, image_height, stream);
    cudaStreamSynchronize(stream);

    // get compressed stream size
    size_t length;
    nvjpegEncodeRetrieveBitstream(nv_handle, nv_enc_state, NULL, &length, stream);
    // get stream itself
    cudaStreamSynchronize(stream);
    std::cout << "length: " << length << std::endl;
    std::vector<u_char> jpeg(length);
    nvjpegEncodeRetrieveBitstream(nv_handle, nv_enc_state, jpeg.data(), &length, 0);

    // write stream to file
    cudaStreamSynchronize(stream);
    std::ofstream output_file("encode.jpg", std::ios::out | std::ios::binary);
    output_file.write(reinterpret_cast<const char *>(jpeg.data()), length);
    output_file.close();
}

// nvcc -g ../encode.cpp -o encode `pkg-config --cflags --libs opencv4` -lnvjpeg -lcudart && ./encode
int main()
{
    cv::Mat img = cv::imread("flower.jpg");
    std::cout << img.channels() << std::endl;
    encode(img);
}

编译文件:

cmake_minimum_required(VERSION 3.10)

project(yolov5)

add_definitions(-std=c++11)
add_definitions(-DAPI_EXPORTS)
option(CUDA_USE_STATIC_CUDA_RUNTIME OFF)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_BUILD_TYPE Debug)
# set(CMAKE_BUILD_TYPE Release)

# TODO(Call for PR): make cmake compatible with Windows
set(CMAKE_CUDA_COMPILER /usr/local/cuda/bin/nvcc)
enable_language(CUDA)

# include and link dirs of cuda and tensorrt, you need adapt them if yours are different
# cuda
include_directories(/usr/local/cuda/include)
link_directories(/usr/local/cuda/lib64)

find_package(OpenCV)
include_directories(${OpenCV_INCLUDE_DIRS})

add_executable(encode encode.cpp ${SRCS})
target_link_libraries(encode nvjpeg cudart ${OpenCV_LIBS})
# target_link_libraries(encode nvinfer nvjpeg cudart myplugins ${OpenCV_LIBS})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我是一个对称矩阵

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

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

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

打赏作者

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

抵扣说明:

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

余额充值