
简介面向计算机视觉与高性能计算方向的在校生和开发者这份基于TensorRT的C推理库源码包可支持RT-DETR、YOLOv5/v7/v8、YOLOX、OSTrack、LightTrack等主流检测与跟踪模型的高效部署。项目针对边缘端与服务器场景使用CUDA核函数实现预处理与后处理结合生产者消费者模型让预处理与推理并行执行并采用RAII思想封装Tensor、Infer实现内存复用与自动拷贝兼顾性能与安全。包内共672个文件压缩后约14.09MB主要包含cpp/hpp/cu/cuh源码与头文件、md使用说明、py量化脚本以及大量jpg/png测试图片和avi演示视频便于对照运行效果。代码按apps、trt_common、quant-tools、workspace分层组织各算法demo相互独立若只需yolopv2可删除其他算法而不影响编译自行部署新算法时模仿现有代码调用trt_infer/trt_tensor即可。workspace中还附有编译好的可执行文件与engine示例便于跳过繁琐构建快速跑通。整体逻辑清晰、二次开发空间充足可直接作为毕业设计、课程设计或期末大作业的工程基础。目前已有525人学习下载适合具备C与深度学习基础、希望快速掌握TensorRT部署流程的读者。1. 从Python到CTensorRT推理库为什么值得折腾做过YOLO部署的人都有体会Python版TensorRT推理在demo里跑得飞快一上生产就露怯——GIL锁、显存碎片、Python侧预处理耗时、异常时CUDA context泄漏任何一个都够运维喝一壶。这个标题把TensorRT、C、RT-DETR和YOLOv5/v7/v8放在一起本质上是想要一套「引擎构建一次、推理接口稳定、后处理对得上模型」的C推理库。它的核心价值不是把Python代码翻译成C而是把预处理、推理、后处理全部压进GPU流水线减少CPU-GPU往返同时提供可嵌入现有服务框架的C API。适合谁来读这篇手上已有训练好的YOLO或RT-DETR权重想在Jetson Orin、Xavier或服务器GPU上做C部署的工程师以及被TensorRT版本兼容、动态shape、NMS实现细节反复折磨的人。下面按「原理 → 引擎构建 → 预处理与推理 → 后处理差异 → 工程化技巧」逐层拆开所有代码片段都按可直接落地的思路写。2. TensorRT加速原理与C推理库的整体架构2.1 TensorRT到底优化了什么TensorRT不是简单地把ONNX模型跑一遍而是对计算图做三件事层融合、精度校准、kernel自动调优。层融合把ConvBiasReLU合并成一个kernel减少显存读写精度校准把FP32权重压缩到FP16或INT8kernel调优则在运行时枚举不同tile尺寸和向量化方式挑出延迟最低的组合。这三件事在C API里体现为IBuilder、INetworkDefinition和IBuilderConfig三个核心对象。推理库的架构通常分成四层engine管理、runtime上下文、预处理、后处理。engine层负责从.engine文件反序列化或从ONNX构建runtime层管理IExecutionContext处理动态shape的输入输出绑定预处理层把cv::Mat转成NCHW float数据并归一化放进GPU显存后处理层解析模型输出做阈值过滤、NMS和坐标映射。四层之间用类封装对外只暴露一个infer(cv::Mat, vectorDetectResult)接口这是C推理库和Python脚本最大的区别——调用方不关心显卡内存和TensorRT细节。2.2 推理库的目录结构与模块划分一个可维护的推理库源码目录通常长这样inference_lib/ ├── CMakeLists.txt ├── include/ │ ├── trt_engine.h // 引擎构建与加载 │ ├── preprocess.h // 仿射变换、归一化 │ ├── postprocess.h // YOLO/RT-DETR输出解析 │ └── infer.h // 对外推理接口 ├── src/ │ ├── trt_engine.cpp │ ├── preprocess.cu // CUDA预处理核函数 │ ├── postprocess.cu // CUDA NMS可选 │ └── infer.cpp └── models/ └── yolov8s.engine // 序列化后的引擎文件这里有个容易忽略的点.engine文件依赖TensorRT版本、GPU架构和CUDA版本换机器必须重新构建。所以推理库一定要保留onnx - engine的构建入口不能只发engine文件。使用说明里需要写明./build --onnx yolov8s.onnx --engine yolov8s.engine --precision fp16这类命令的对应实现。2.3 为什么C比Python更适合做推理服务Python版TensorRT常见的性能损耗来自三处预处理在CPU上做resize和归一化耗时30-50ms每次推理调用Python API的overhead在几毫秒量级后处理用NumPy做NMS在目标数量多时成为瓶颈。C推理库把预处理写进CUDA kernel推理用enqueueV3异步提交后处理用CUDA NMS或高效的topK实现单帧总延迟可以压到Python版的1/3到1/2。更重要的是显存控制。Python的CUDA context和显存申请往往不可控长时间运行后碎片化严重C里可以显式管理cudaMalloc的缓存池避免每帧重新分配。在Jetson Orin这类显存有限的设备上这个差异直接决定能不能长期稳定跑服务。3. 基于TensorRT C API的引擎构建与加载3.1 从ONNX构建engine的C实现核心代码是用nvonnxparser把ONNX转成TensorRT网络再设置精度和显存配置。下面这段是构建函数的关键部分bool TrtEngine::buildFromOnnx(const std::string onnxPath, const std::string enginePath, Precision precision) { auto builder std::unique_ptrnvinfer1::IBuilder( nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger())); const auto explicitBatch 1U static_castuint32_t( nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH); auto network std::unique_ptrnvinfer1::INetworkDefinition( builder-createNetworkV2(explicitBatch)); auto parser std::unique_ptrnvonnxparser::IParser( nvonnxparser::createParser(*network, sample::gLogger.getTRTLogger())); if (!parser-parseFromFile(onnxPath.c_str(), static_castint(nvinfer1::ILogger::Severity::kWARNING))) { return false; } auto config std::unique_ptrnvinfer1::IBuilderConfig( builder-createBuilderConfig()); config-setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, (size_t)1 30); // 1GB workspace if (precision Precision::FP16) { config-setFlag(nvinfer1::BuilderFlag::kFP16); } else if (precision Precision::INT8) { config-setFlag(nvinfer1::BuilderFlag::kINT8); } auto engine std::unique_ptrnvinfer1::ICudaEngine( builder-buildSerializedNetwork(*network, *config)); // 序列化保存 std::ofstream file(enginePath, std::ios::binary); file.write(static_castconst char*(engine-data()), engine-size()); return true; }逻辑说明kEXPLICIT_BATCH必须设置否则动态batch会被当成隐式维度后面绑定输入输出时会报错setMemoryPoolLimit(kWORKSPACE, 130)设置构建阶段可用的显存池大小数值越大kernel调优越充分但Jetson上建议512MB起步避免爆显存。FP16是YOLO系列最稳的精度档位INT8需要量化校准数据集直接用kINT8但不提供校准器会报错。注意一个坑buildSerializedNetwork返回的是序列化后的IHostMemory在不同GPU架构间不通用。Ampere架构生成的engine在Orin也是Ampere上能跑但换到Turing或Ada架构基本都会加载失败。所以使用说明里一定要写engine文件与GPU架构强绑定分发程序时建议携带ONNX并在目标设备首次启动时构建。3.2 反序列化engine与dynamic shape绑定加载engine和创建执行上下文是推理前的最后一步准备。动态shape支持是RT-DETR和YOLO部署都要处理的问题——batch大小和输入尺寸可能在运行时变化bool TrtEngine::loadEngine(const std::string enginePath) { std::ifstream file(enginePath, std::ios::binary); std::vectorchar data((std::istreambuf_iteratorchar(file)), std::istreambuf_iteratorchar()); m_runtime.reset(nvinfer1::createInferRuntime(sample::gLogger.getTRTLogger())); m_engine.reset(m_runtime-deserializeCudaEngine(data.data(), data.size())); m_context.reset(m_engine-createExecutionContext()); // 检查输入维度是否为动态 auto inputName m_engine-getIOTensorName(0); auto inputDims m_engine-getTensorShape(inputName); m_dynamicBatch inputDims.d[0] -1 || inputDims.d[2] -1; return m_context ! nullptr; } bool TrtEngine::setInputShape(int batchSize, int height, int width) { if (!m_dynamicBatch) return true; nvinfer1::Dims4 dims{batchSize, 3, height, width}; return m_context-setInputShape(m_engine-getIOTensorName(0), dims); }这里的关键是setInputShape必须在enqueueV3之前调用否则输出tensor的尺寸还是旧值。动态shape的开销在于TensorRT会为每种shape组合重新做部分kernel选择如果生产环境固定输入尺寸比如统一resize到640x640建议构建engine时把维度写成固定的性能更稳。如果要支持多尺寸设置setOptimizationProfile并指定min/opt/max三档opt维度用最常见的推理尺寸否则第一次跑新shape会有明显卡顿。3.3 输入输出tensor绑定与显存管理推理时要为输入输出tensor分配GPU显存使用cudaMalloc还是cudaMallocAsync根据CUDA版本而定。常见做法是初始化时一次性分配用成员变量持有指针避免每帧malloc/freebool TrtEngine::allocateBuffers() { auto inputName m_engine-getIOTensorName(0); auto outputName m_engine-getIOTensorName(1); auto inputDims m_engine-getTensorShape(inputName); auto outputDims m_engine-getTensorShape(outputName); m_inputSize std::accumulate(inputDims.d, inputDims.d inputDims.nbDims, 1, std::multipliesint64_t()); m_outputSize std::accumulate(outputDims.d, outputDims.d outputDims.nbDims, 1, std::multipliesint64_t()); // 使用cudaMallocAsync需要CUDA 11.2退货到cudaMalloc更通用 cudaMalloc(m_inputBuf, m_inputSize * sizeof(float)); cudaMalloc(m_outputBuf, m_outputSize * sizeof(float)); m_context-setTensorAddress(inputName, m_inputBuf); m_context-setTensorAddress(outputName, m_outputBuf); return true; }注意输出tensor的形状。YOLOv8的ONNX导出通常输出[1, 84, 8400]含义是box(4) class(80)共84维8400是三个尺度特征图的总anchor数RT-DETR输出是[1, 300, 6]300个query每个是cx, cy, w, h, label, score。分配显存前先打印getTensorShape确认维度直接写死shape是后面很多诡异bug的来源。output buffer的大小还和动态shape联动。如果设置过setInputShape为[1, 3, 1280, 1280]特征图anchor数会变成8400的四倍输出buffer不够就会越界写入——TensorRT不检查buffer越界表现是下一帧推理结果随机错乱。稳妥做法是初始化时用opt shape分配最大可能输出或者每次改shape后重新分配。4. 预处理、推理与后处理YOLO和RT-DETR的流水线实现4.1 CUDA预处理resize、归一化与letterboxYOLO系列的输入通常要做letterbox——保持宽高比缩放到640x640剩余部分用114填充。Python里用cv2做这套操作需要把图像数据拷贝到GPUC推理库可以直接写CUDA kernel__global__ void letterbox_kernel(const uint8_t* src, int srcW, int srcH, float* dst, int dstW, int dstH) { int x blockIdx.x * blockDim.x threadIdx.x; int y blockIdx.y * blockDim.y threadIdx.y; if (x dstW || y dstH) return; float scale min((float)dstW / srcW, (float)dstH / srcH); float padX (dstW - srcW * scale) * 0.5f; float padY (dstH - srcH * scale) * 0.5f; int srcX (int)((x - padX) / scale); int srcY (int)((y - padY) / scale); if (srcX 0 || srcX srcW || srcY 0 || srcY srcH) { dst[y * dstW x] 114.0f / 255.0f; return; } // NHWC - NCHW 并归一化到[0,1] dst[0 * dstW * dstH y * dstW x] src[srcY * srcW srcX] / 255.0f; dst[1 * dstW * dstH y * dstW x] src[srcY * srcW srcX srcH * srcW] / 255.0f; dst[2 * dstW * dstH y * dstW x] src[srcY * srcW srcX 2 * srcH * srcW] / 255.0f; }这段kernel的效果等效于OpenCV的resize copyMakeBorder但省掉了两次CPU-GPU拷贝。实际工程里不会用这种最朴素的写法——相邻线程读取的src像素存在bank冲突换成__ldg只读缓存或纹理内存能提升20%-30%吞吐。关键参数是scale和padX/padY后处理坐标映射需要用到同样的值。调试时最容易错的是NHWC到NCHW的索引计算先在小图如64x64上逐像素验证再上全尺寸。归一化方式也要对齐训练时的配置。YOLOv8训练用的是[0,1]归一化或ImageNet mean/std如果模型是RGB顺序而OpenCV默认读BGR推理结果的类别会明显错乱。常见做法是把通道顺序转换放进CUDA kernel或者在cudaMemcpy到GPU前用cv::cvtColor处理——前者更快但代码要小心后者更稳妥。4.2 异步推理与stream管理预处理完成后调用enqueueV3提交推理典型实现如下bool TrtEngine::infer(cudaStream_t stream, float* input, float* output, int batchSize) { // 确保输入shape正确 setInputShape(batchSize, m_inputH, m_inputW); // 在指定stream上执行推理异步 m_context-enqueueV3(stream); // input数据已在上一步cudaMemcpyAsync到位 // output需要同步或等待事件后再读回CPU cudaStreamSynchronize(stream); return true; }enqueueV3是TensorRT 8.5之后推荐的接口替代旧的enqueueV2。差异在于V3用setTensorAddress绑定bufferV2用setBindingDimensions和setBindingPointerV3的tensor语义更清晰在多context场景下不容易搞混绑定关系。每个线程或每个推理请求建议用独立的cudaStream_t这样多个IExecutionContext可以在不同stream上并发执行——这正是C推理库承载多路视频流的基础。有一个容易踩的坑cudaStreamSynchronize会阻塞CPU直到当前stream所有任务完成同步点加得太早会让GPU流水线空等。正确的做法是用CUDA事件cudaEvent_t event; cudaEventCreateWithFlags(event, cudaEventDisableTiming); cudaEventRecord(event, stream); // 推理完成后记录事件 cudaStreamWaitEvent(cpuStream, event, 0); // 读回数据的操作在cpuStream上等待或者让后处理也跑在GPU上全程不把输出tensor拷回CPU。下一节会展开说后处理怎么往GPU上搬。4.3 后处理差异YOLOv5/v7/v8与RT-DETR的输出结构这是整个推理库最容易写错的模块。三个YOLO版本和RT-DETR的head输出结构完全不同模型输出tensor每个anchor/query的维度含义是否需要NMSYOLOv5[1, 25200, 85]cx,cy,w,h obj_score 80类分数需要YOLOv7[1, 25200, 85]或多头输出类似v5但可能存在辅助头输出需要YOLOv8[1, 84, 8400]cx,cy,w,h 80类分数无obj需要RT-DETR[1, 300, 6]cx,cy,w,h label score不需要需要注意YOLOv8的shape是[84, 8400]转置排列8400在最后一维。处理时可以先做一次transpose在GPU上用kernel或cudaMemcpy2D再按行解析如果直接在CPU上嵌套循环访问cache miss会非常严重8400x84次访问可能拖慢整体5-10ms。RT-DETR没有NMS300个query按score阈值过滤即可这是DETR类模型的结构优势。后处理的第一步是候选框过滤std::vectorDetection decodeOutput(const float* data, int numAnchors, int numClasses, float confThresh) { std::vectorDetection detections; for (int i 0; i numAnchors; i) { const float* row data i * (numClasses 4); float cx row[0], cy row[1], w row[2], h row[3]; float maxScore 0; int label -1; for (int c 4; c numClasses 4; c) { if (row[c] maxScore) { maxScore row[c]; label c - 4; } } if (maxScore confThresh) { Detection det; det.box {cx - w / 2, cy - h / 2, w, h}; det.label label; det.score maxScore; detections.push_back(det); } } return detections; }这段代码直接解析模型输出没有做坐标映射。映射要在NMS之后做——把letterbox坐标系下的坐标还原到原图坐标系公式是x_orig (x - padX) / scale同时裁剪到图像边界。如果先映射再做NMS逻辑上没错但数值精度损失稍大且多处重复运算。4.4 NMS的实现选择CPU排序、GPU加速还是直接砍掉NMS是后处理里的性能瓶颈常见三种实现方式各有适用场景CPU式NMS先按score排序再遍历候选框计算IoU。适用于单路视频流、目标数量少于500的场景纯C实现大约耗时1-3ms。实现简单但要先cudaMemcpy把输出拷回CPU加上拷贝时间整体约5ms。GPU式NMS用CUDA kernel并行计算IoU矩阵再用topK筛选。TensorRT 8.2自带EfficientNMS插件但需要ONNX导出时带插件节点如果ONNX导出时没有包含NMS层运行时加载engine会报Unknown plugin错误。自写CUDA NMS的复杂度较高适合目标数量1000的场景在单帧推理本身只有3-5ms时收益更明显。DETR式跳过NMSRT-DETR因为query设计本身抑制了重复检测官方后处理不含NMS。实际测试中RT-DETR在密集场景下确有少量重复框可以做一个简化版NMS——只对同类别内IoU0.7的框做抑制阈值放宽了计算量也小。我的建议是推理库默认提供CPU NMS作为兜底同时预留GPU NMS的实现接口。工程上的bug排查顺序先确认NMS是否正确——单独写一个test输入一对重叠度0.9的检测框看输出是否只保留高分的那个。4.5 坐标还原与置信度阈值的协同调参坐标还原的代码放在NMS之后void scaleCoords(const std::vectorDetection dets, std::vectorDetection out, float scale, float padX, float padY, int origW, int origH) { out.reserve(dets.size()); for (const auto d : dets) { Detection r d; r.box.x (d.box.x - padX) / scale; r.box.y (d.box.y - padY) / scale; r.box.w d.box.w / scale; r.box.h d.box.h / scale; // 越界裁剪 r.box.x std::max(0.0f, r.box.x); r.box.y std::max(0.0f, r.box.y); out.push_back(r); } }这里的scale、padX、padY必须是预处理kernel里用到的同一组变量。一个常见的bug是预处理用cv::resize的INTER_LINEAR而推理库假设了INTER_NEAREST坐标偏移一两个像素对目标检测影响不大但对分割或关键点任务是致命误差。所以使用说明里应该明确标注预处理插值方式必须与训练或验证时一致。置信度阈值建议做成可配置项暴露在推理接口YOLO系列常见取值0.25RT-DETR官方默认0.3实际业务场景里对漏检更敏感就降到0.15对误检更敏感就提到0.5。5. 多模型支持的设计RT-DETR与YOLOv5/v7/v8的适配层5.1 不同模型的输出解析策略一个推理库要同时支持RT-DETR和YOLOv5/v7/v8就不能把后处理逻辑硬编码进推理类。常见做法是定义抽象接口每个模型实现自己的解析器class IPostProcessor { public: virtual ~IPostProcessor() default; virtual std::vectorDetection parse(const float* gpuOutput, int batchSize, cudaStream_t stream) 0; virtual bool needsNMS() const { return true; } }; class Yolov8Processor : public IPostProcessor { public: std::vectorDetection parse(const float* gpuOutput, int batchSize, cudaStream_t stream) override { // 针对 [84, 8400] 的解析逻辑 } }; class RTDETRProcessor : public IPostProcessor { public: std::vectorDetection parse(const float* gpuOutput, int batchSize, cudaStream_t stream) override { // 直接过滤 [300, 6] 的query不需要NMS } };模型工厂根据engine名称或配置文件决定实例化哪个Processor。YOLOv5和YOLOv7的解析代码可以复用一份基类差别只在输出tensor的形状和是否存在objectness分数维度——v5/v7的前5维是cx,cy,w,h,objv8没有obj。这个差异可以用一个hasObjectness标志位控制。如果ONNX是直接从ultralytics导出的v8的输出可能是transpose后的[8400, 84]布局解析前检查一下维度排布并在README里写清楚。5.2 模型类别数与自定数据集的适配YOLO默认80类RT-DETR默认80类但业务场景常常用自训练权重换成自己的类别数。推理库里的numClasses不能写死要在engine加载时从输出维度推算int numClasses outputDims.d[1] - 4; // YOLOv8 [84, 8400] - 80类RT-DETR输出是[300, 6]类和score已经编码好不需要推测类别数。适配自定数据集时还有一个隐藏参数类别ID映射。模型的类别顺序取决于训练时的data.yaml推理结果里的label索引要和业务系统里的类别名对齐做法是加载一个labels.txt第i行就是第i个类别的名字。使用说明里要把这个文件的格式写清楚否则用户拿到自训练模型大概率在类别编号上栽跟头。5.3 INT8量化对多模型精度的影响如果推理库宣称支持INT8需要处理YOLO和RT-DETR不同的量化敏感度。YOLOv5/v7对INT8的耐受性好用几百张图片做校准就能把mAP掉点控制在1%以内YOLOv8的检测头对量化更敏感尤其是小目标RT-DETR的transformer结构量化掉点显著常见做法是只量化backbone保留head为FP16——这在TensorRT里可以通过设置per-layer precision实现// 对特定的层设置FP16其余保持INT8 for (int i 0; i network-getNbLayers(); i) { auto layer network-getLayer(i); std::string layerName layer-getName(); if (layerName.find(Detect) ! std::string::npos || layerName.find(MLP) ! std::string::npos) { layer-setPrecision(nvinfer1::DataType::kFLOAT); layer-setOutputType(0, nvinfer1::DataType::kFLOAT); } }INT8校准数据的选择也值得展开。常见做法是从验证集里均匀抽样500-1000张覆盖不同光照、目标尺度和类别分布校准集不能只用单一场景的图片否则量化后的模型在真实场景掉点明显。TensorRT提供IInt8Calibrator接口实现getBatchSize、getBatch和readCalibrationCache三个方法即可。如果推理库的源码里带了calibrator.h使用说明里应该补一句校准数据按calib/目录约定放置每张图片会被center-crop到640x640。6. 验证推理结果正确性的三个手段拿到一个C推理库第一件事不是调性能而是确认结果和Python推理一致。我的习惯是从三个维度做验证以下是具体到指令的实践。6.1 用固定输入验证数值一致性预处理环节最容易出错写一个test_preprocess.cpp输入一张纯色图片比如全灰128分别用OpenCV和推理库的预处理kernel处理对比输出tensor// 用OpenCV做参考实现 cv::Mat gray(640, 640, CV_8UC3, cv::Scalar(128, 128, 128)); cv::Mat floatImg; gray.convertTo(floatImg, CV_32FC3, 1.0 / 255.0); cv::Mat channels[3]; cv::split(floatImg, channels); // 对比推理库的preprocess输出 float* gpuOutput preprocess(gray.data, gray.cols, gray.rows); float* cpuRef channels[2].ptrfloat(); // BGR - R channel // 计算最大绝对误差应小于1e-6这个测试通过后再做端到端验证。用一张已知类别的图片分别跑Python版的model.predict(img)和C推理库对比检测框坐标和类别。坐标误差在letterbox后应小于2个像素——注意这里不能要求完全一致因为CUDA kernel的插值实现和OpenCV的INTER_LINEAR在高精度上可能有微小差异但IoU应该超过0.95。6.2 多输入与动态shape的压力测试动态shape的坑往往在切换尺寸时暴露。写一个循环测试连续推理640x640、960x960、1280x1280各50帧每帧检测输出缓冲区是否有越界写。观察指标包括帧间显存占用是否持续增长——如果增长说明setInputShape后buffer重分配没有复用切换尺寸后第一帧的延迟是否突然升高——这是TensorRT重新选择kernel的正常现象但要确认没有CUDA error输出tensor的shape是否与预期一致打印getTensorShape确认每一维6.3 用NMS单独验证后处理逻辑NMS是手工实现最容易出错的地方单独构造测试用例比调真实模型更快定位问题。构造三个框两个IoU0.8的同类别框一个完全不同位置的框验证NMS后低分的那个是否被抑制。再构造一个跨类别的重叠框验证不同类别之间不做NMS抑制两个框都应保留。这里用到的数据全部写死在测试代码里std::vectorDetection testBoxes { {Box(100, 100, 200, 200), 0, 0.9}, // 类别0高分 {Box(110, 110, 200, 200), 0, 0.8}, // 与前者IoU0.7应被抑制 {Box(100, 100, 200, 200), 1, 0.85}, // 类别1应保留 {Box(500, 500, 50, 50), 0, 0.7} // 无重叠应保留 };跑完这三个测试推理库的后处理正确性就有基础保证了。剩下的事情就是拿到自己的模型和业务数据把阈值调到你放心的误检率。一个推理库的「使用说明」写得再好也不如这个数字同一条视频流C版推理库的单帧延迟和Python版的差值——按我的部署经验在Jetson Orin上YOLOv8s从Python版的45ms降到C版的18ms是正常水平。达不到这个差距优先检查预处理有没有从CPU来回拷贝、NMS是不是还在用Python式逐层循环。本文还有配套的精品资源点击获取