yolov5-tensorrt推理框架实现

logger.h

#ifndef LOGGER_H
#define LOGGER_H

#include <NvInfer.h>
#include <NvInferRuntime.h>


inline const char* severity_string(nvinfer1::ILogger::Severity t)
{
	switch (t)
	{
	case nvinfer1::ILogger::Severity::kINTERNAL_ERROR: return "internal_error";
	case nvinfer1::ILogger::Severity::kERROR:   return "error";
	case nvinfer1::ILogger::Severity::kWARNING: return "warning";
	case nvinfer1::ILogger::Severity::kINFO:    return "info";
	case nvinfer1::ILogger::Severity::kVERBOSE: return "verbose";
	default: return "unknow";
	}
}


class TRTLogger : public nvinfer1::ILogger
{
public:
	virtual void log(Severity severity, nvinfer1::AsciiChar const* msg) noexcept override
	{
		if (severity <= Severity::kINFO)
		{
			if (severity == Severity::kWARNING)
				printf("\033[33m%s: %s\033[0m\n", severity_string(severity), msg);
			else if (severity <= Severity::kERROR)
				printf("\033[31m%s: %s\033[0m\n", severity_string(severity), msg);
			else
				printf("%s: %s\n", severity_string(severity), msg);
		}
	}
} logger;

#endif // LOGGER_H

preprocess.h

#ifndef PREPROCESS_H
#define PREPROCESS_H

#include <opencv2/opencv.hpp>


void LetterBox(const cv::Mat& image, cv::Mat& outImage, const cv::Size& newShape = cv::Size(640, 640), const cv::Scalar& color = cv::Scalar(114, 114, 114));

#endif // PREPROCESS_H

preprocess.cpp

#include "preprocess.h"


void LetterBox(const cv::Mat& image, cv::Mat& outImage, const cv::Size& newShape, const cv::Scalar& color)
{
	cv::Size shape = image.size();
	float r = std::min((float)newShape.height / (float)shape.height, (float)newShape.width / (float)shape.width);
	float ratio[2]{ r, r };
	int new_un_pad[2] = { (int)std::round((float)shape.width * r),(int)std::round((float)shape.height * r) };

	auto dw = (float)(newShape.width - new_un_pad[0]) / 2;
	auto dh = (float)(newShape.height - new_un_pad[1]) / 2;

	if (shape.width != new_un_pad[0] && shape.height != new_un_pad[1])
		cv::resize(image, outImage, cv::Size(new_un_pad[0], new_un_pad[1]));
	else
		outImage = image.clone();

	int top = int(std::round(dh - 0.1f));
	int bottom = int(std::round(dh + 0.1f));
	int left = int(std::round(dw - 0.1f));
	int right = int(std::round(dw + 0.1f));

	cv::Vec4d params;
	params[0] = ratio[0];
	params[1] = ratio[1];
	params[2] = left;
	params[3] = top;

	cv::copyMakeBorder(outImage, outImage, top, bottom, left, right, cv::BORDER_CONSTANT, color);
}

yolov5.h

#ifndef YOLOV5_H
#define YOLOV5_H

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <ctime>
#include <cuda_runtime.h>
#include <NvInfer.h>
#include <NvInferRuntime.h>
#include <opencv2/opencv.hpp>

#include "preprocess.h"
#include "postprocess.h"

//#define USE_CUDA

#ifdef USE_CUDA
    #include "preprocess.cuh"
    #include "decode.cuh"
#endif


class Yolov5
{
public:
    Yolov5();
    ~Yolov5();

    int load_model(const std::string& model_path);                        // 加载模型
    int infer(const cv::Mat &image, std::vector<Detection> &detections); // 推理运行模型

private:
    int pre_process(const cv::Mat &image);   // 图像预处理
    int post_process(const cv::Mat &image, std::vector<Detection>& detections); // 后处理

    const cv::Size input_size = cv::Size(640, 640);
    const int input_numel = 1 * 3 * input_size.width * input_size.height;
    const float confidence_threshold = 0.5;
    const float score_threshold = 0.25;
    const float nms_threshold = 0.45;
    const int class_num = 80;
    const int output_numprob = 5 + class_num;
    const int output_numbox = 3 * (input_size.width / 8 * input_size.height / 8 + input_size.width / 16 * input_size.height / 16 + input_size.width / 32 * input_size.height / 32);
    const int output_numel = 1 * output_numprob * output_numbox;

    nvinfer1::IRuntime* runtime = nullptr;
    nvinfer1::ICudaEngine* engine = nullptr;
    nvinfer1::IExecutionContext* execution_context = nullptr;
    cudaStream_t stream = nullptr;
    float* input_h = nullptr;
    float* output_h = nullptr;
    float* input_d = nullptr;          	
	float* output_d = nullptr;
    float* bindings[2]; 

#ifdef USE_CUDA
	uint8_t* input_host;
	float* d2s_host;
   	float* d2s_device; 
    float* s2d_host;
    float* s2d_device;
    float* output_box_host;
    float* output_box_device;
    const int max_box = 1024;
   	const int nubox_element = 7; 
    const int max_input_size = sizeof(float) * 3 * 1024 * 1024;
#endif
};

#endif // YOLOV5_H

yolov5.cpp

#include "yolov5.h"
#include "logger.h"


Yolov5::Yolov5()
{
	runtime = nvinfer1::createInferRuntime(logger);

	cudaMallocHost(&input_h, sizeof(float) * input_numel);
    cudaMallocHost(&output_h, sizeof(float) * output_numel);

	cudaMalloc(&input_d, sizeof(float) * input_numel);
	cudaMalloc(&output_d, sizeof(float) * output_numel);

	bindings[0] = input_d;
	bindings[1] = output_d;

#ifdef USE_CUDA
	cudaMallocHost(&input_host, max_input_size);
	cudaMallocHost(&d2s_host, sizeof(float) * 6);
	cudaMalloc(&d2s_device, sizeof(float) * 6);
	cudaMallocHost(&s2d_host, sizeof(float) * 6);
	cudaMalloc(&s2d_device, sizeof(float) * 6);
	cudaMallocHost(&output_box_host, sizeof(float) * (nubox_element * max_box + 1));
	cudaMalloc(&output_box_device, sizeof(float) * (nubox_element * max_box + 1));
#endif
}

Yolov5::~Yolov5()
{
	cudaStreamDestroy(stream);
	cudaFree(input_d);
    cudaFree(output_d);
	cudaFreeHost(input_h);
    cudaFreeHost(output_h);
}


int Yolov5::load_model(const std::string& model_path)
{
    std::ifstream in(model_path, std::ios::binary);
    if (!in.is_open()) 
		return -1;

    in.seekg(0, std::ios::end);
    size_t size = in.tellg();
    std::vector<unsigned char> engine_data(size);
    in.seekg(0, std::ios::beg);
    in.read((char*)engine_data.data(), size);
    in.close();

    engine = runtime->deserializeCudaEngine(engine_data.data(), engine_data.size());
	if(engine == nullptr) 
		return -1;

    execution_context = engine->createExecutionContext();
	if(execution_context == nullptr)
		return -1;

    cudaStreamCreate(&stream);
    return 0;
}


int Yolov5::pre_process(const cv::Mat &image)
{
#ifdef USE_CUDA
	cudaMemcpyAsync(input_host, image.data, sizeof(uint8_t) * 3 * image.cols * image.rows, cudaMemcpyHostToDevice, stream);
	preprocess_kernel_img(input_host, image.cols, image.rows, input_d, input_size.width, input_size.height, d2s_host, s2d_host, stream);
	cudaMemcpyAsync(d2s_device, d2s_host, sizeof(float) * 6, cudaMemcpyHostToDevice, stream);
	cudaMemcpyAsync(s2d_device, s2d_host, sizeof(float) * 6, cudaMemcpyHostToDevice, stream);
#else
	cv::Mat letterbox;
	LetterBox(image, letterbox, input_size);
	//cv::resize(image, letterbox, input_size);
	letterbox.convertTo(letterbox, CV_32FC3, 1.0f / 255.0f);

	int image_area = letterbox.cols * letterbox.rows;
	float* pimage = (float*)letterbox.data;
	float* phost_b = input_h + image_area * 0;
	float* phost_g = input_h + image_area * 1;
	float* phost_r = input_h + image_area * 2;
	for (int i = 0; i < image_area; ++i, pimage += 3)
	{
		*phost_r++ = pimage[0];
		*phost_g++ = pimage[1];
		*phost_b++ = pimage[2];
	}

	cudaMemcpyAsync(input_d, input_h, sizeof(float) * input_numel, cudaMemcpyHostToDevice, stream);
#endif
    return 0;
}


int Yolov5::infer(const cv::Mat& image, std::vector<Detection>& detections)
{
    pre_process(image);

	bool success = execution_context->executeV2((void**)bindings);
	if(!success)
	{
	    std::cerr << "Failed to run inference" << std::endl;
	    return -1;
	}

#ifndef USE_CUDA
	cudaMemcpyAsync(output_h, output_d, sizeof(float) * output_numel, cudaMemcpyDeviceToHost, stream);
    cudaStreamSynchronize(stream);
#endif

    post_process(image, detections);
    return 0;
}


int Yolov5::post_process(const cv::Mat &image,  std::vector<Detection>& detections)
{
    std::vector<cv::Rect> boxes;
	std::vector<float> scores;
	std::vector<int> class_ids;

#ifdef USE_CUDA
	cudaMemset(output_box_device, 0, sizeof(float) * (nubox_element * max_box + 1));	
	decode_kernel_invoker(output_d, output_numbox, class_num, score_threshold, d2s_device, output_box_device, max_box, nubox_element, stream);
	nms_kernel_invoker(output_box_device, nms_threshold, max_box, nubox_element, stream);
	cudaMemcpyAsync(output_box_host, output_box_device, sizeof(float) * (nubox_element * max_box + 1), cudaMemcpyDeviceToHost, stream);
	cudaStreamSynchronize(stream);

	for (size_t i = 0; i < max_box; i++)
	{
		if (output_box_host[7 * i + 7])
		{
			float x1 = output_box_host[7 * i + 1];
			float y1 = output_box_host[7 * i + 2];
			float x2 = output_box_host[7 * i + 3];
			float y2 = output_box_host[7 * i + 4];
			boxes.push_back(cv::Rect(x1, y1, x2-x1, y2-y1));
			scores.push_back(output_box_host[7 * i + 5]);
			class_ids.push_back(output_box_host[7 * i + 6]);
		}
	}

	detections.clear();
	detections.resize(boxes.size());
	for (int i = 0; i < boxes.size(); i++)
	{
		detections[i].bbox = boxes[i];
		detections[i].score = scores[i];
		detections[i].id = class_ids[i];
	}

#else
	// float x_ratio = float(image.cols) / input_size.width;
	// float y_ratio = float(image.rows) / input_size.height;
	for (int i = 0; i < output_numbox; ++i)
	{
		float* ptr = output_h + i * output_numprob;
		float obj_score = ptr[4];
		if (obj_score < confidence_threshold)
			continue;

		float* classes_scores = 5 + ptr;
		int class_id = std::max_element(classes_scores, classes_scores + class_num) - classes_scores;
		float score = classes_scores[class_id] * obj_score;
		if (score < score_threshold)
			continue;

		float x = ptr[0];
		float y = ptr[1];
		float w = ptr[2];
		float h = ptr[3];
		int left = int(x - 0.5 * w);
		int top = int(y - 0.5 * h);
		int width = int(w);
		int height = int(h);

		cv::Rect box = cv::Rect(left, top, width, height);
		scale_boxes(box, input_size, image.size());
		boxes.push_back(box);
		scores.push_back(score);
		class_ids.push_back(class_id);
	}

	std::vector<int> indices;
	nms(boxes, scores, score_threshold, nms_threshold, indices);
	
	detections.clear();
	detections.resize(indices.size());
	for (int i = 0; i < indices.size(); ++i)
	{
	    int idx = indices[i];
		detections[i].bbox = boxes[idx];
		detections[i].score = scores[idx];
		detections[i].id = class_ids[idx];
	}
#endif

    return 0;
}

单线程版本

test_yolov5.cpp

#include "yolov5.h"

int main()
{
    Yolov5* yolov5 = new Yolov5();
    yolov5->load_model("yolov5n_int8.engine");

    cv::Mat image = cv::imread("bus.jpg");
    std::vector<Detection> detections;
    yolov5->infer(image, detections);
    std::cout << "detections size: " << detections.size() << std::endl;
    yolov5->draw_detections(0, image, detections);
    cv::imwrite("result.jpg", image);
    
    cv::VideoCapture cap("bj_full.mp4");
    if (!cap.isOpened())
        return -1;

    int frame_count = 0;
    auto start_all = std::chrono::high_resolution_clock::now();
    while (true)
    {
        cap >> image;
        if (image.empty())
            break;
        yolov5->infer(image, detections);
        frame_count++;
        auto end_all = std::chrono::high_resolution_clock::now();
        auto elapsed_all = std::chrono::duration_cast<std::chrono::microseconds>(end_all - start_all).count() / 1000.f;
        if (elapsed_all >= 1000)
        {
            printf("FPS:%f \n", frame_count / (elapsed_all / 1000.0f));
            frame_count = 0;
            start_all = std::chrono::high_resolution_clock::now();
        }
    }
    cap.release();

    return 0;
}

运行./demo输出:

info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChane] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +15, now: CPU 0, GPU 17 (MiB)
detections size: 2
FPS:525.963196 
FPS:520.842163 
FPS:503.353210 
FPS:533.499023 
......

资源占用:(测试机器为RTX4090+24核cpu)
在这里插入图片描述
可以看到,gpu的utils在40%左右,有很大的优化空间。

线程池版本

yolov5_thread_pool.h

#ifndef YOLOV5_THREAD_POOL_H
#define YOLOV5_THREAD_POOL_H

#include "yolov5.h"

#include <iostream>
#include <vector>
#include <queue>
#include <map>
#include <thread>
#include <mutex>
#include <condition_variable>

class Yolov5ThreadPool
{
private:
    std::queue<std::pair<int, cv::Mat>> tasks;             // <id, img>用来存放任务
    std::vector<std::shared_ptr<Yolov5>> yolov5_instances; // 模型实例
    std::map<int, std::vector<Detection>> results;         // <id, objects>用来存放结果(检测框)
    std::map<int, cv::Mat> img_results;                    // <id, img>用来存放结果(图片)
    std::vector<std::thread> threads;                      // 线程池
    std::mutex mtx1;
    std::mutex mtx2;
    std::condition_variable cv_task, cv_result;
    bool stop;

    void worker(int id);

public:
    Yolov5ThreadPool();
    ~Yolov5ThreadPool();

    int setUp(const std::string &model_path, int num_threads = 12);     // 初始化
    int submitTask(const cv::Mat &img, int id);                         // 提交任务
    int getTargetResult(std::vector<Detection> &objects, int id);       // 获取结果
    int getTargetImgResult(cv::Mat &img, int id);                       // 获取结果(图片)
    void stopAll();                                                     // 停止所有线程
};

#endif //YOLOV5_THREAD_POOL_H

yolov5_thread_pool.cpp

#include "yolov5_thread_pool.h"

// 构造函数
Yolov5ThreadPool::Yolov5ThreadPool() { stop = false; }

// 析构函数
Yolov5ThreadPool::~Yolov5ThreadPool()
{
    // stop all threads
    stop = true;
    cv_task.notify_all();
    for (auto &thread : threads)
    {
        if (thread.joinable())
        {
            thread.join();
        }
    }
}

// 初始化:加载模型,创建线程,参数:模型路径,线程数量
int Yolov5ThreadPool::setUp(const std::string &model_path, int num_threads)
{
    // 遍历线程数量,创建模型实例,放入vector
    // 这些线程加载的模型是同一个
    for (size_t i = 0; i < num_threads; ++i)
    {
        std::shared_ptr<Yolov5> yolov5 = std::make_shared<Yolov5>();
        yolov5->load_model(model_path.c_str());
        yolov5_instances.push_back(yolov5);
    }
    // 遍历线程数量,创建线程
    for (size_t i = 0; i < num_threads; ++i)
    {
        threads.emplace_back(&Yolov5ThreadPool::worker, this, i);
    }
    return 0;
}


// 线程函数。参数:线程id
void Yolov5ThreadPool::worker(int id)
{
    while (!stop)
    {
        std::pair<int, cv::Mat> task;
        std::shared_ptr<Yolov5> instance = yolov5_instances[id]; // 获取模型实例
        {
            // 获取任务
            std::unique_lock<std::mutex> lock(mtx1);
            cv_task.wait(lock, [&] { return !tasks.empty() || stop; });
            if (stop)
                return;

            task = tasks.front();
            tasks.pop();
        }
        // 运行模型
        std::vector<Detection> detections;
        instance->infer(task.second, detections);

        {
            // 保存结果
            std::lock_guard<std::mutex> lock(mtx2);
            results.insert({task.first, detections});
            draw_detections(task.second, detections);
            img_results.insert({task.first, task.second});
            cv_result.notify_one();
        }
    }
}


// 提交任务,参数:图片,id(帧号)
int Yolov5ThreadPool::submitTask(const cv::Mat &img, int id)
{
    // 如果任务队列中的任务数量大于10,等待,避免内存占用过多
    while (tasks.size() > 1000)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }

    {
        // 保存任务
        std::lock_guard<std::mutex> lock(mtx1);
        tasks.push({id, img});
    }
    cv_task.notify_one();
    return 0;
}

// 获取结果,参数:检测框,id(帧号)
int Yolov5ThreadPool::getTargetResult(std::vector<Detection> &objects, int id)
{
    // 如果没有结果,等待
    while (results.find(id) == results.end())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
    std::lock_guard<std::mutex> lock(mtx2);
    objects = results[id];
    // remove from map
    results.erase(id);
    return 0;
}

// 获取结果(图片),参数:图片,id(帧号)
int Yolov5ThreadPool::getTargetImgResult(cv::Mat &img, int id)
{
    int loop_cnt = 0;
    // 如果没有结果,等待
    while (img_results.find(id) == img_results.end())
    {
        // 等待 
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        loop_cnt++;
        if (loop_cnt > 1000)
        {
            std::cerr << "getTargetImgResult timeout" << std::endl;
            return 0;
        }
    }
    std::lock_guard<std::mutex> lock(mtx2);
    img = img_results[id];
    // remove from map
    img_results.erase(id);

    return 0;
}

void Yolov5ThreadPool::stopAll()
{
    stop = true;
    cv_task.notify_all();
}

test_yolov5_thread_pool.cpp

#include "yolov5_thread_pool.h"
#include <chrono>


static int g_frame_start_id = 0; // 读取视频帧的索引
static int g_frame_end_id = 0;   // 模型处理完的索引
static Yolov5ThreadPool *g_pool = nullptr;
bool end = false;


void read_stream(const std::string& video_file)
{
    cv::VideoCapture cap(video_file);
    if (!cap.isOpened())
        return;

    cv::Mat img;
    while (true)
    {
        cap >> img;
        if (img.empty())
        {
            end = true;
            break;
        }
        g_pool->submitTask(img.clone(), g_frame_start_id++);
    }
    cap.release();
}


void get_results()
{
    auto start_all = std::chrono::high_resolution_clock::now();
    int frame_count = 0;

    //cv::VideoWriter writer = cv::VideoWriter("result.mp4", cv::VideoWriter::fourcc('m', 'p', '4', 'v'), 30, cv::Size(1280, 720));
    while (true)
    {
        cv::Mat img;
        auto ret = g_pool->getTargetImgResult(img, g_frame_end_id++);
        if (end)
        {
            g_pool->stopAll();
            break;
        }
        //cv::imwrite("output/" + std::to_string(g_frame_end_id) + ".jpg", img);
        //writer << img;

        frame_count++;
        auto end_all = std::chrono::high_resolution_clock::now();
        auto elapsed_all_2 = std::chrono::duration_cast<std::chrono::microseconds>(end_all - start_all).count() / 1000.f;
        if (elapsed_all_2 >= 1000)
        {
            printf("FPS:%f \n", frame_count / (elapsed_all_2 / 1000.0f));
            frame_count = 0;
            start_all = std::chrono::high_resolution_clock::now();
        }
    }
    g_pool->stopAll();
}


int main(int argc, char **argv)
{
    g_pool = new Yolov5ThreadPool();
    g_pool->setUp(argv[1], atoi(argv[2]));

    std::thread read_stream_thread(read_stream, "bj_full.mp4");
    std::thread result_thread(get_results);

    read_stream_thread.join();
    result_thread.join();

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(trt_inference LANGUAGES C CXX CUDA)
set(CMAKE_CXX_STANDARD 14)

find_package(CUDA REQUIRED)
include_directories(${CUDA_INCLUDE_DIRS})

set(OpenCV_DIR /home/tfy/document/HybrIK/cpp/opencv-4.12.0/lib/cmake/opencv4)
find_package(OpenCV REQUIRED)

set(TENSORRT_INCLUDE_DIRS /home/tfy/docker_share/TensorRT-10.6.0.26/include)
set(TENSORRT_LIBRARY_DIRS /home/tfy/docker_share/TensorRT-10.6.0.26/targets/x86_64-linux-gnu/lib)
include_directories(${TENSORRT_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS})
link_directories(${TENSORRT_LIBRARY_DIRS})

add_executable(demo test_yolov5.cpp preprocess.cpp postprocess.cpp yolov5.cpp)
add_executable(thread_pool test_yolov5_thread_pool.cpp preprocess.cpp postprocess.cpp yolov5.cpp yolov5_thread_pool.cpp)
target_link_libraries(demo PRIVATE ${OpenCV_LIBS} ${CUDA_LIBRARIES} ${TENSORRT_LIBRARY_DIRS}/libnvinfer.so)
target_link_libraries(thread_pool PRIVATE ${OpenCV_LIBS} ${CUDA_LIBRARIES} ${TENSORRT_LIBRARY_DIRS}/libnvinfer.so)

运行./thread_pool yolov5n_int8.engine 12测试性能,gpu的utils能在70%左右,cpu能跑到近2000%,继续增加线程数时推理帧率可能会下降:
在这里插入图片描述
打印输出如下内容,可以看到推理帧率偶尔能达到1000FPS:

info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +15, now: CPU 0, GPU 17 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +1, GPU +15, now: CPU 1, GPU 35 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +14, now: CPU 1, GPU 52 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +1, GPU +15, now: CPU 2, GPU 70 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +15, now: CPU 2, GPU 88 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +1, GPU +14, now: CPU 3, GPU 105 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +15, now: CPU 3, GPU 123 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +1, GPU +15, now: CPU 4, GPU 141 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +14, now: CPU 4, GPU 158 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +1, GPU +15, now: CPU 5, GPU 176 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +15, now: CPU 5, GPU 194 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +1, GPU +15, now: CPU 6, GPU 211 (MiB)
FPS:670.522827 
FPS:712.649414 
FPS:841.653320 
FPS:822.008606 
FPS:739.883850 
FPS:614.572327 
FPS:727.774353 
FPS:699.419434 
FPS:812.266663 
FPS:773.891296 
FPS:676.168945 
FPS:706.710266 
FPS:852.323303 
FPS:738.334290 
FPS:649.812866 
FPS:730.089355 
FPS:694.656860 
FPS:811.991943 
FPS:695.474304 
FPS:690.040283 
FPS:632.997681 
FPS:844.942078 
FPS:871.944153 
FPS:818.565552 
FPS:782.801514 
FPS:674.829834 
FPS:735.527466 
FPS:667.501587 
FPS:653.889221 
FPS:637.249756 
FPS:738.310974 
FPS:629.966736 
FPS:623.615601 
FPS:799.024475 
FPS:1048.638184 
FPS:1083.996826 
FPS:742.771179 
FPS:703.455505 
FPS:800.101257 
FPS:1058.399902

nvcodec硬件解码优化

安装ffnvcodec

sudo apt-get update
sudo apt-get install libnvidia-encode-<version> ffmpeg
sudo apt-get install nv-codec-headers

其中version通过下面命令确定:

nvidia-smi | grep "Driver Version"

若输出:

| NVIDIA-SMI 570.211.01             Driver Version: 570.211.01     CUDA Version: 12.8     |

则执行:

 sudo apt-get install libnvidia-encode-570 ffmpeg

安装支持CUDA的FFmpeg

LZ下载的是FFmpeg-n6.1.4,运行下面命令编译源码:

./configure --prefix=./install --enable-gpl --enable-nonfree --enable-cuda --enable-cuda-nvcc --enable-cuvid --enable-nvdec --enable-nvenc --disable-debug --enable-shared  --enable-static  --extra-cfl
make -j8
make install

添加文件nvdec_decode.h

#include <iostream>
#include <chrono>
#include <string>

// CUDA 头文件
#include <cuda.h>
#include <cuda_runtime.h>
// NPP 头文件
#include <npp.h>
// FFmpeg 头文件
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext.h>
#include <libavutil/hwcontext_cuda.h>
#include <libavutil/imgutils.h>
#include <libavutil/pixdesc.h>
}
// OpenCV 头文件
#include <opencv2/opencv.hpp>


/**
 * @brief 严格校验 NV12 格式的 CUDA 帧参数(仅需执行一次)
 */
int check_nv12_frame_once(AVFrame* hw_frame, int& w, int& h, int& y_stride, size_t& nv12_size, size_t& bgr_size) 
{
    if (hw_frame->format != AV_PIX_FMT_CUDA) 
    {
        std::cerr << "错误:hw_frame 不是 CUDA 格式!" << std::endl;
        return -1;
    }

    // 获取固定的宽高/行步长(同一视频所有帧一致)
    w = hw_frame->width;
    h = hw_frame->height;
    y_stride = hw_frame->linesize[0];
    // NV12 总大小 = Y(w*h) + UV(w*h/2)
    nv12_size = (size_t)w * h * 3 / 2;
    // BGR 总大小(固定)
    bgr_size = (size_t)w * h * 3;

    // 仅一次校验行步长
    if (y_stride < w) {
        std::cerr << "错误:Y 平面行步长异常!stride=" << y_stride << " width=" << w << std::endl;
        return -1;
    }

    std::cout << "NV12 帧参数校验完成(全局生效):" << std::endl;
    std::cout << "  宽=" << w << " 高=" << h << " Y行步长=" << y_stride << std::endl;
    std::cout << "  NV12总大小=" << nv12_size << " BGR总大小=" << bgr_size << std::endl;
    return 0;
}

/**
 * @brief GPU 端 NV12 → BGR 转换(复用显存/参数)
 */
int nv12_to_bgr_cuda(uint8_t* dev_nv12, int w, int h, int y_stride, uint8_t* dev_bgr) 
{
    // 1. 计算 NV12 各平面指针(考虑行步长对齐)
    Npp8u* pY = dev_nv12;
    Npp8u* pUV = dev_nv12 + (size_t)y_stride * h;
    
    // 2. 封装为 NPP 要求的指针数组
    const Npp8u* const ppSrc[] = {pY, pUV};
    
    // 3. 复用固定的行步长参数
    int nSrcStep = y_stride;       
    int nDstStep = w * 3;          
    NppiSize oSizeROI = {w, h};    

    // 4. 调用 NPP 转换函数
    NppStatus npp_ret = nppiNV12ToBGR_8u_P2C3R(
        ppSrc,       // 输入:Y+UV 指针数组
        nSrcStep,    // 输入行步长(复用全局参数)
        dev_bgr,     // 输出:复用的BGR显存指针
        nDstStep,    // 输出行步长(复用)
        oSizeROI     // 有效图像尺寸(复用)
    );

    return 0;
}

修改test_yolov5_thread_pool.cpp:

#include "yolov5_thread_pool.h"
#include "global_var.h"
#include <chrono>

#ifdef USE_NVCODEC
#include "nvdec_decode.h"
#endif

static int g_frame_start_id = 0; // 读取视频帧的索引
static int g_frame_end_id = 0;   // 模型处理完的索引
static Yolov5ThreadPool *g_pool = nullptr;
bool end = false;


#ifndef USE_NVCODEC
void read_stream(const std::string& video_file)
{
    cv::VideoCapture cap(video_file);
    if (!cap.isOpened())
        return;

    cv::Mat img;
    while (true)
    {
        cap >> img;
        if (img.empty())
        {
            end = true;
            break;
        }
        g_pool->submitImgTask(img.clone(), g_frame_start_id++);
    }
    cap.release();
}
#else
void read_stream_nvcodec(const std::string& video_file)
{
    AVFormatContext* fmt_ctx = nullptr;
    AVCodecContext* codec_ctx = nullptr;
    AVBufferRef* hw_device_ctx = nullptr;
    int video_stream_idx = 0;
    int ret = 0;

    // 打开视频
    ret = avformat_open_input(&fmt_ctx, video_file.c_str(), nullptr, nullptr);
    if (ret < 0)
    {
        printf("avformat_open_input failed\n");
        return;
    }
    else{
        printf("avformat_open_input success\n");
    }

    // 查找流信息
    ret = avformat_find_stream_info(fmt_ctx, nullptr);
    if (ret < 0)
    {
        printf("avformat_find_stream_info failed\n");
        return;
    }
    else{    
        printf("avformat_find_stream_info success\n");
    }

    // 找视频流
    video_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);

    // 创建 CUDA 设备上下文
    ret = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0);
    if (ret < 0)
    {
        printf("av_hwdevice_ctx_create failed\n");
        return;
    }
    else{    
        printf("av_hwdevice_ctx_create success\n");
    }

    // 强制使用 NVDEC 解码器
    const AVCodec* codec = avcodec_find_decoder_by_name("h264_cuvid");
    if (!codec) {
        codec = avcodec_find_decoder_by_name("hevc_cuvid"); // 兼容 HEVC
    }
    if (!codec) {
        std::cerr << "错误:未找到 NVDEC 解码器(h264_cuvid/hevc_cuvid)" << std::endl;
        return;
    }
    else{
        printf("avcodec_find_decoder_by_name success\n");
    }

    // 初始化解码器上下文
    codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        std::cerr << "avcodec_alloc_context3 failed" << std::endl;
        return;
    }
    else{
        printf("avcodec_alloc_context3 success\n");
    }

    ret = avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_idx]->codecpar);
    if (ret < 0)
    {
        printf("avcodec_parameters_to_context failed\n");
        return;
    }
    else{     
        printf("avcodec_parameters_to_context success\n");
    }

    codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
    codec_ctx->thread_count = 1; // GPU 解码无需多线程

    ret = avcodec_open2(codec_ctx, codec, nullptr);
    if( ret < 0)
    {
        printf("avcodec_open2 failed\n");
        return;
    }
    else{     
        printf("avcodec_open2 success\n");
    }

    // ========== 2. 全局变量:复用帧参数和显存 ==========
    AVPacket pkt;
    av_init_packet(&pkt);
    pkt.data = nullptr;
    pkt.size = 0;

    AVFrame* hw_frame = av_frame_alloc();
    if (!hw_frame) {
        printf("av_frame_alloc failed\n");
        return;
    }
    else{
        printf("av_frame_alloc success\n");
    }

    uint8_t* dev_bgr = nullptr; // 全局复用的BGR显存
    bool is_param_init = false; // 参数是否已初始化
    int frame_count = 0;        // 帧计数器

    while (av_read_frame(fmt_ctx, &pkt) >= 0) {
        if (pkt.stream_index != video_stream_idx) {
            av_packet_unref(&pkt);
            continue;
        }

        // 发送数据包
        ret = avcodec_send_packet(codec_ctx, &pkt);
        if (ret < 0 && ret != AVERROR(EAGAIN)) {
            av_packet_unref(&pkt);
            continue;
        }
        if(ret == AVERROR_EOF)
            goto cleanup;

        // 接收解码帧
        while (ret >= 0) {
            ret = avcodec_receive_frame(codec_ctx, hw_frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) 
                break;

            frame_count++;

            // ========== 4. 仅第一帧执行:参数校验 + 显存分配 ==========
            if (!is_param_init) {
                // 一次性校验帧参数
                ret = check_nv12_frame_once(hw_frame, global_w, global_h, global_y_stride, global_nv12_size, global_bgr_size);
                if (ret < 0) {
                    av_frame_unref(hw_frame);
                    av_packet_unref(&pkt);
                    //goto cleanup;
                }

                // 一次性分配BGR显存(复用至所有帧)
                ret = cudaMalloc(&dev_bgr, global_bgr_size);

                is_param_init = true; // 标记参数已初始化
            }

            // ========== 5. 快速校验:防止极端情况(帧尺寸突变) ==========
            if (hw_frame->width != global_w || hw_frame->height != global_h || hw_frame->linesize[0] != global_y_stride) {
                std::cerr << "错误:帧参数突变!当前帧宽=" << hw_frame->width << " 全局宽=" << global_w << std::endl;
                av_frame_unref(hw_frame);
                continue;
            }

            // ========== 6. GPU 转换(复用显存/参数) ==========
            ret = nv12_to_bgr_cuda((uint8_t*)hw_frame->data[0], global_w, global_h, global_y_stride, dev_bgr);
            if (ret < 0) {
                av_frame_unref(hw_frame);
                continue;
            }

            g_pool->submitImgTask(dev_bgr, g_frame_start_id++);
            av_frame_unref(hw_frame);
        }

        av_packet_unref(&pkt);
    }

cleanup:
    av_frame_free(&hw_frame);
    avcodec_free_context(&codec_ctx);
    av_buffer_unref(&hw_device_ctx);
    avformat_close_input(&fmt_ctx);
    exit(0);
}
#endif

void get_results()
{
    auto start_all = std::chrono::high_resolution_clock::now();
    int frame_count = 0;

    //cv::VideoWriter writer = cv::VideoWriter("result.mp4", cv::VideoWriter::fourcc('m', 'p', '4', 'v'), 30, cv::Size(1280, 720));
    while (true)
    {
        cv::Mat img;
        std::vector<Detection> detections;
        auto ret = g_pool->getTargetResult(detections, g_frame_end_id++);
        //auto ret = g_pool->getTargetImgResult(img, g_frame_end_id++);
        if (end)
        {
            g_pool->stopAll();
            break;
        }

        //writer << img;
        //std::cout << "frame:" << g_frame_end_id<<" "<<detections.size() << std::endl;

        frame_count++;
        auto end_all = std::chrono::high_resolution_clock::now();
        auto elapsed_all_2 = std::chrono::duration_cast<std::chrono::microseconds>(end_all - start_all).count() / 1000.f;
        if (elapsed_all_2 >= 1000)
        {
            printf("FPS:%f \n", frame_count / (elapsed_all_2 / 1000.0f));
            frame_count = 0;
            start_all = std::chrono::high_resolution_clock::now();
        }
    }
    g_pool->stopAll();
}


int main(int argc, char **argv)
{
    if( argc < 3)
    {
        printf("Usage: %s <model_path> <video_path> <thread_num>\n", argv[0]);
        return -1;
    }

    g_pool = new Yolov5ThreadPool();
    g_pool->setUp(argv[1], atoi(argv[3]));

#ifndef USE_NVCODEC
    std::cout << "Using opencv" << std::endl;
    std::thread read_stream_thread(read_stream, argv[2]);
#else
    std::cout << "Using nvcodec" << std::endl;
    std::thread read_stream_thread(read_stream_nvcodec, argv[2]);
#endif

    std::thread result_thread(get_results);

    read_stream_thread.join();
    result_thread.join();

    return 0;
}

运行./thread_pool yolov5n_int8.engine bj_full.mp4 2
在这里插入图片描述
可以看到,采用nvcodec硬件解码后,gpu的utils能达到近100%,但经过测试增加线程数(>2)时,推理的帧率反而会下降。

打印输出如下内容,推理帧率可以稳定在1400FPS左右:

info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +15, now: CPU 0, GPU 17 (MiB)
info: Loaded engine size: 4 MiB
warning: Using an engine plan file across different models of devices is not recommended and is likely to affect performance or even cause errors.
info: [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +1, GPU +15, now: CPU 1, GPU 35 (MiB)
Using nvcodec
avformat_open_input success
avformat_find_stream_info success
av_hwdevice_ctx_create success
avcodec_find_decoder_by_name success
avcodec_alloc_context3 success
avcodec_parameters_to_context success
[h264_cuvid @ 0x7292ccd83dc0] Invalid pkt_timebase, passing timestamps as-is.
avcodec_open2 success
av_frame_alloc success
NV12 帧参数校验完成(全局生效):
  宽=1280=720 Y行步长=1536
  NV12总大小=1382400 BGR总大小=2764800
FPS:995.006958 
FPS:1402.666138 
FPS:1401.745361 
FPS:1397.949707 
FPS:1396.142700 
FPS:1400.215820 
FPS:1397.886841 
FPS:1395.608521 
FPS:1404.317505 
FPS:1399.883911 
FPS:1401.903687 
FPS:1399.732666 
FPS:1401.914551 
FPS:1401.559082 
FPS:1397.706421 
FPS:1399.470947 
FPS:1401.383423 
FPS:1397.094604 
FPS:1401.063965 
FPS:1400.266235 
FPS:1398.696411 
FPS:1395.996338 

视频推理截图:
在这里插入图片描述
完整工程下载链接见:https://download.csdn.net/download/taifyang/92609495

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐