在这篇文章里,我们来聊聊一个完整的 C++ 工程实例:如何用 TensorRT 10 + CUDA + OpenCV CUDA 写一个推理引擎类 TrtEngine,实现从 图像输入 → GPU 预处理 → TensorRT 推理 → 输出解析 的完整流程。

如果你刚接触 TensorRT,可能会觉得 API 很复杂,名字也容易混乱。没关系,我们从实际工程代码入手,逐块来讲清楚每一步。

1. 整体思路

这个工程的目标是:

  • 给定一张 OpenCV 的 cv::Mat 图像(BGR 格式,CPU 内存)。

  • 我们把它传到 GPU 上,在 GPU 上完成 resize、颜色通道转换、归一化等预处理。

  • 然后调用 TensorRT 引擎跑推理,得到检测结果。

  • 最后把检测框解析出来,返回给上层代码。

整个过程需要解决几个关键问题:

  1. 如何加载 TensorRT 引擎.engine 文件)

  2. 如何准备输入输出内存(device buffer + pinned host buffer)

  3. 如何高效地在 GPU 上做预处理(用 OpenCV CUDA)

  4. 如何触发推理enqueueV3

  5. 如何解析输出(从 GPU → Host → 解析检测框)

2. 类的结构设计

我们封装了一个 TrtEngine 类,核心接口是:

std::vector<Detection> infer(const cv::Mat& bgr, float confThres=0.25f);

调用时你只需要传入一张 OpenCV 图像,它会返回一组检测结果(矩形框 + 类别 + 置信度)。

Detection 结构体定义很简单:

struct Detection { 
    cv::Rect box;  // 框位置
    int cls;       // 类别ID
    float score;   // 置信度
};

3. 引擎加载与资源准备

在构造函数里,我们做了几件事:

1 反序列化引擎

runtime_ = createInferRuntime(gLogger);
engine_  = runtime_->deserializeCudaEngine(buf.data(), sz);
ctx_     = engine_->createExecutionContext();
  • runtime_:推理运行时环境

  • engine_:具体的模型

  • ctx_:执行上下文(真正负责跑网络)

2 找输入输出 Tensor 的名字

for (int i = 0; i < engine_->getNbIOTensors(); ++i) {
    const char* name = engine_->getIOTensorName(i);
    if (engine_->getTensorIOMode(name) == TensorIOMode::kINPUT) inputName_ = name;
    else outputName_ = name;
}

TensorRT 10 不再推荐用 binding 索引,而是直接用名字来绑定。

3 计算输入输出大小

inputBytes_  = dimSize(inDims)  * sizeof(float);
outputBytes_ = dimSize(outDims) * sizeof(float);

分配内存

  • 输入 / 输出缓冲在 GPU 显存cudaMalloc

  • 输出还要一份 pinned host 内存cudaHostAlloc,这样从 GPU 拷回更快

5 创建 CUDA stream

  • 我们需要一个 cudaStream_t,然后用 OpenCV 的 cv::cuda::StreamAccessor::wrapStream() 把它封装成 OpenCV CUDA 的 stream,方便 OpenCV 算子和 TensorRT 在同一个 stream 上协同工作。

4. GPU 预处理

这部分在 preprocessGPU() 里实现,步骤如下:

  1. 上传图像到 GPU

    
      

    d_bgr_.upload(bgr, cvStream_);

  2. resize 到模型输入大小

    cv::cuda::resize(d_bgr_, d_resized_, cv::Size(W_, H_), ...);

  3. 颜色通道转换(BGR → RGB)

    cv::cuda::cvtColor(d_resized_, d_rgb_, cv::COLOR_BGR2RGB, 0, cvStream_);

  4. 转成 float32 并归一化到 [0,1]

    d_rgb_.convertTo(d_rgb_f32_, CV_32F, 1.0/255.0, 0.0, cvStream_);

  5. 通道拆分

    cv::cuda::split(d_rgb_f32_, d_ch_, cvStream_);

  6. 把通道数据拷贝成 CHW 格式
    这里用的是 cudaMemcpy2DAsync,因为 GpuMat 每行可能有对齐填充,不能直接当连续数组拷。我们要保证输入张量在内存里是紧凑的 CHW 格式。

    for (int c = 0; c < 3; ++c) { cudaMemcpy2DAsync(dst, dstPitch, src, srcPitch, widthBytes, H_, cudaMemcpyDeviceToDevice, stream_); }

这样,模型的输入数据就准备好了,直接放在了 dInput_ 这块 GPU 内存里。


5. 触发推理

核心的一行:

ctx_->enqueueV3(stream_);

  • 这行代码的本质就是:把输入张量送进网络,在 GPU 上跑一遍,把结果写到输出张量里。

  • 它不会阻塞 CPU,而是异步提交到 CUDA stream。

  • 我们前面已经绑定好输入/输出地址,所以这里直接执行。


6. 拷回输出并解析

输出张量写到了 dOutput_(GPU 显存),我们需要拷到 Host 端解析。

  1. 拷回

    cudaMemcpyAsync(hOutputPinned_, dOutput_, outputBytes_, cudaMemcpyDeviceToHost, stream_); cudaStreamSynchronize(stream_);

  2. 解析

    • 输出格式是 [N, 6],每一行 [x1,y1,x2,y2,score,cls]

    • 我们遍历这些行,把 score > 0 的框取出来。

    • 再按原图尺寸缩放,得到正确的坐标。


7. 上层调用

对上层来说,使用起来非常简单:

TrtEngine engine("best_fp16.engine", 640, 640); cv::Mat frame = cv::imread("test.jpg"); auto results = engine.infer(frame, 0.25f); for (auto& d : results) { cv::rectangle(frame, d.box, {0,255,0}, 2); printf("cls=%d, score=%.2f\n", d.cls, d.score); }


8. 工程实践中的亮点

  • 全 GPU 预处理:避免 CPU hotspot 和大数据拷贝,速度大幅提升。

  • Pinned host 输出:D2H 更快,特别适合实时场景。

  • 名字绑定 IO:对齐 TensorRT 10 的新接口,更加稳健。

  • Stream 协同:OpenCV CUDA 和 TensorRT 在同一个 stream 上执行,减少同步。


9. 可以继续优化的地方

  • 如果视频解码也用 OpenCV cudacodec::VideoReader,可以避免 upload

  • 解析输出时,可以用异步事件替代 cudaStreamSynchronize,和下一帧流水化。

  • 如果能接受精度下降,可以量化成 INT8,进一步提速。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐