简介这份资源面向希望掌握目标检测模型工程化落地的开发者聚焦如何用C与Python结合ONNXRuntime部署RT-DETR实时检测算法。RT-DETR在DETR基础上优化了推理速度而ONNXRuntime提供跨平台高性能推理能力二者结合可解决从PyTorch模型到实际部署的转换与加速问题适合具备一定深度学习与C基础的进阶学习者。资源包共12个文件约150.47MB包含onnx模型权重、cpp与py推理源码、CMakeLists构建脚本、标签文件、示例图片及说明文档覆盖模型加载、预处理、推理与后处理全流程。目前已有1413人学习。通过源码实践读者可掌握模型转ONNX、验证与编译、编写推理代码、NMS后处理及多线程内存优化等关键技巧深入理解Transformer检测器与ONNXRuntime的配合方式切实提升算法部署能力。1. 拿到 RT-DETR 部署包先别急着编译这份资源到底解决了什么很多做目标检测落地的朋友都遇到过这个尴尬PyTorch 训练出来的 RT-DETR 模型精度不错但一到生产环境就卡在部署上——Python 端推理慢、C 端没有现成的 ONNXRuntime 调用范例、预处理和后处理全靠自己摸索。这份「基于 C 和 Python 使用 ONNXRuntime 部署 RT-DETR 目标检测算法」的项目源码就是冲着这个痛点来的。它把 RT-DETR 的 ONNX 模型、C 推理代码、Python 推理脚本、CMakeLists 构建文件、标签文件和测试图片全部打包在一起拿到手就能跑通从图像输入到检测框输出的完整链路。适合两类人一是刚接触 ONNXRuntime 部署、想找一个能跑通的目标检测实战项目练手的新手二是已经在做 C 推理服务、需要一份 RT-DETR 后处理参考实现的熟手。下面我按实际拆包和复现的顺序把这份资源讲透。2. 拆开压缩包先看什么目录结构与 ONNX 模型验证2.1 文件清单与各模块职责解压后的目录结构大致如下我按功能分组说明路径类型作用cplusplus/CMakeLists.txt构建脚本C 端的 CMake 工程配置负责找 ONNXRuntime、OpenCV 并链接cplusplus/main.cpp源码C 推理主程序含预处理、推理、后处理全流程python/main.py源码Python 推理脚本逻辑与 C 端对齐weights/rtdetr_r50vd_6x_coco.onnx模型已导出的 RT-DETR R50VD 6x COCO 预训练 ONNX 模型labels.txt配置COCO 80 类标签后处理映射类别名用images/数据测试图片含 bus.jpg 和 example_img 系列bus_cpp_result.jpg/bus_python_result.jpg输出作者跑出的参考结果图用来对比你的输出是否一致README.md文档环境依赖和运行说明这个结构很干净没有多余的训练代码纯粹是推理部署。weights目录里的 ONNX 模型是核心资产省去了你自己从 PyTorch 导出的步骤——但前提是你得确认这个模型和你的 ONNXRuntime 版本兼容。2.2 用 Python 验证 ONNX 模型是否可用在动手编译 C 之前我习惯先用 Python 把 ONNX 模型过一遍确认输入输出形状和算子支持情况。这一步能帮你排除掉大部分「编译过了但推理报错」的玄学问题。import onnxruntime as ort import numpy as np # 加载 ONNX 模型指定 CPU 执行提供者 session ort.InferenceSession( weights/rtdetr_r50vd_6x_coco.onnx, providers[CPUExecutionProvider] ) # 打印输入输出信息确认形状和数据类型 for inp in session.get_inputs(): print(f输入名: {inp.name}, 形状: {inp.shape}, 类型: {inp.type}) for out in session.get_outputs(): print(f输出名: {out.name}, 形状: {out.shape}, 类型: {out.type}) # 构造一个符合输入形状的随机张量做一次前向 input_name session.get_inputs()[0].name dummy np.random.randn(1, 3, 640, 640).astype(np.float32) outputs session.run(None, {input_name: dummy}) print(f输出数量: {len(outputs)}) for i, o in enumerate(outputs): print(f输出{i} 形状: {o.shape})这段代码的逻辑说明InferenceSession是 ONNXRuntime 的核心入口providers参数决定用 CPU 还是 GPU 执行。RT-DETR 的 ONNX 导出通常有两个输出——一个是检测框坐标[1, 300, 4]一个是类别分数[1, 300, 80]300 是查询数量num_queries。如果你打印出来的形状和这个对不上说明模型导出时的配置和当前代码不匹配需要回头检查导出脚本。参数说明dummy输入的640x640是 RT-DETR 常用的输入分辨率实际推理时你需要把真实图片 resize 到这个尺寸并做归一化。np.random.randn只是用来验证模型能否跑通真实推理要换成预处理后的图像张量。提示如果session.get_inputs()[0].shape显示的是动态维度比如[batch, 3, height, width]说明模型导出时开了动态轴C 端和 Python 端都需要按实际输入尺寸传参不能写死。2.3 确认 ONNXRuntime 版本与算子兼容性ONNXRuntime 的版本差异会直接影响模型能否加载。常见做法是先用onnxruntime的 Python 包跑一遍如果 Python 端能跑通C 端大概率也没问题因为两者用的是同一套算子内核。我一般会固定一个版本区间比如 1.16 到 1.18 之间太老的版本可能不支持 RT-DETR 里的某些算子如MultiHeadAttention的融合实现太新的版本又可能和系统里的 CUDA 驱动不匹配。# 查看当前安装的 onnxruntime 版本 pip show onnxruntime # 如果版本不对指定版本重装 pip install onnxruntime1.17.0C 端链接的 ONNXRuntime 库版本必须和 Python 端一致否则会出现「Python 能跑、C 加载失败」的翻车现场。CMakeLists.txt 里通常会通过find_package(onnxruntime)或手动指定ONNXRUNTIME_ROOT来定位库文件你需要确保这个路径下的.so或.dll版本和 Python 端一致。3. Python 端推理全流程从图像预处理到 NMS 后处理3.1 预处理resize、归一化与维度变换RT-DETR 的预处理和 YOLO 系列不太一样它不需要 letterbox 填充而是直接 resize 到固定尺寸。但归一化的均值和标准差要按 ImageNet 的标准来这点如果搞错检测框会偏得离谱。import cv2 import numpy as np def preprocess(image_path, input_size640): RT-DETR 预处理BGR转RGB、resize、归一化、HWC转CHW img cv2.imread(image_path) original_h, original_w img.shape[:2] # BGR 转 RGBRT-DETR 训练时用的是 RGB img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 直接 resize 到目标尺寸不保持宽高比 img_resized cv2.resize(img_rgb, (input_size, input_size)) # 归一化像素值转到 [0,1]再按 ImageNet 均值方差标准化 img_float img_resized.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img_norm (img_float - mean) / std # HWC 转 CHW再增加 batch 维度 img_chw img_norm.transpose(2, 0, 1) input_tensor np.expand_dims(img_chw, axis0).astype(np.float32) return input_tensor, original_h, original_w逻辑说明cv2.imread读进来是 BGR 格式必须转成 RGB否则颜色通道错位会导致类别预测混乱。归一化用的均值和标准差是 ImageNet 的经典值RT-DETR 的预训练权重就是按这个标准训练的。transpose(2, 0, 1)把 HWC 转成 CHW因为 ONNX 模型期望的输入是 NCHW 格式。参数说明input_size默认 640这个值必须和 ONNX 模型导出时的输入尺寸一致。如果你拿到的模型是 512 或 800这里要改。original_h和original_w是为了后处理时把检测框映射回原图尺寸。3.2 推理与输出解析理解 RT-DETR 的输出格式RT-DETR 的输出和传统检测器不同它没有锚框直接输出归一化的框坐标和类别分数。通常有两个输出张量boxes形状[1, 300, 4]格式是cxcywh归一化坐标scores形状[1, 300, 80]是每个查询对应的类别概率。import onnxruntime as ort # 加载模型 session ort.InferenceSession( weights/rtdetr_r50vd_6x_coco.onnx, providers[CPUExecutionProvider] ) # 预处理 input_tensor, orig_h, orig_w preprocess(images/bus.jpg) # 推理 input_name session.get_inputs()[0].name outputs session.run(None, {input_name: input_tensor}) # 解析输出outputs[0] 是 boxesoutputs[1] 是 scores boxes outputs[0] # [1, 300, 4] cxcywh 归一化 scores outputs[1] # [1, 300, 80] # 取每个查询的最大类别分数和对应类别索引 class_ids np.argmax(scores[0], axis1) confidences np.max(scores[0], axis1) # 过滤低置信度检测 conf_threshold 0.5 keep confidences conf_threshold boxes boxes[0][keep] class_ids class_ids[keep] confidences confidences[keep] print(f过滤后剩余检测框数量: {len(boxes)})逻辑说明np.argmax沿类别维度取最大值得到每个查询最可能的类别。np.max取对应的置信度。conf_threshold是过滤阈值RT-DETR 通常用 0.5 左右就能得到干净的结果因为它的查询机制本身就会抑制重复检测。参数说明outputs的顺序取决于模型导出时的输出定义有的导出脚本会把 scores 放在前面。如果你发现outputs[0].shape是[1, 300, 80]那就说明顺序反了交换一下即可。3.3 后处理坐标转换与 NMS 的取舍RT-DETR 理论上不需要 NMS因为它的端到端设计就是为了消除这个步骤。但实际部署中我建议还是加一个轻量的 NMS尤其是当置信度阈值设得比较低的时候能进一步去掉重叠框。def cxcywh_to_xyxy(boxes, orig_w, orig_h): 把归一化的 cxcywh 转成原图尺度的 xyxy cx, cy, w, h boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] x1 (cx - w / 2) * orig_w y1 (cy - h / 2) * orig_h x2 (cx w / 2) * orig_w y2 (cy h / 2) * orig_h return np.stack([x1, y1, x2, y2], axis1) def nms(boxes, scores, iou_threshold0.5): 纯 NumPy 实现的 NMS避免额外依赖 x1, y1, x2, y2 boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] areas (x2 - x1) * (y2 - y1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1) iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou iou_threshold)[0] order order[inds 1] return keep # 坐标转换 NMS boxes_xyxy cxcywh_to_xyxy(boxes, orig_w, orig_h) keep_indices nms(boxes_xyxy, confidences, iou_threshold0.5) final_boxes boxes_xyxy[keep_indices] final_classes class_ids[keep_indices] final_scores confidences[keep_indices]逻辑说明cxcywh_to_xyxy先把中心点加宽高的格式转成左上右下坐标再乘以原图宽高还原尺度。nms是标准的非极大值抑制实现按置信度排序后逐个保留最高分框并抑制与其 IoU 超过阈值的框。参数说明iou_threshold设 0.5 是比较通用的值如果你发现同一目标被检测了两次可以降到 0.4如果发现相邻目标被误抑制可以升到 0.6。RT-DETR 本身查询数量固定为 300所以 NMS 的计算量很小不会成为性能瓶颈。4. C 端部署CMake 构建与 ONNXRuntime API 调用4.1 CMakeLists.txt 配置要点C 端的构建是整个部署里最容易卡住的地方核心问题是让 CMake 找到 ONNXRuntime 的头文件和库文件。项目里的 CMakeLists.txt 通常长这样cmake_minimum_required(VERSION 3.15) project(rtdetr_deploy) set(CMAKE_CXX_STANDARD 17) # 手动指定 ONNXRuntime 路径根据你的实际安装位置修改 set(ONNXRUNTIME_ROOT /path/to/onnxruntime CACHE PATH ONNXRuntime root) # 找 OpenCV find_package(OpenCV REQUIRED) # 头文件路径 include_directories( ${ONNXRUNTIME_ROOT}/include ${OpenCV_INCLUDE_DIRS} ) # 链接库路径 link_directories(${ONNXRUNTIME_ROOT}/lib) add_executable(rtdetr_demo main.cpp) # 链接 ONNXRuntime 和 OpenCV target_link_libraries(rtdetr_demo onnxruntime ${OpenCV_LIBS} )逻辑说明ONNXRUNTIME_ROOT是你解压 ONNXRuntime 预编译包后的根目录里面应该有include/和lib/两个子目录。find_package(OpenCV)负责定位 OpenCV如果你系统里没装 OpenCV需要先通过包管理器或源码编译安装。参数说明CMAKE_CXX_STANDARD 17是 ONNXRuntime 的 C API 要求的最低标准别用 C11。link_directories在较新的 CMake 里不推荐用但在这个简单工程里够用。如果你在 Windows 上编译链接的库名可能是onnxruntime.libLinux 下是libonnxruntime.so。4.2 main.cpp 核心推理逻辑C 端的推理流程和 Python 端一一对应只是 API 调用方式不同。核心是Ort::Session和Ort::Value这两个类。#include onnxruntime_cxx_api.h #include opencv2/opencv.hpp #include vector #include iostream int main() { // 初始化 ONNXRuntime 环境 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, rtdetr); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); // 设置线程数 session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); // 加载模型 Ort::Session session(env, weights/rtdetr_r50vd_6x_coco.onnx, session_options); // 获取输入输出名称 Ort::AllocatorWithDefaultOptions allocator; auto input_name session.GetInputNameAllocated(0, allocator); auto output_name_0 session.GetOutputNameAllocated(0, allocator); auto output_name_1 session.GetOutputNameAllocated(1, allocator); // 读取并预处理图像 cv::Mat img cv::imread(images/bus.jpg); int orig_w img.cols; int orig_h img.rows; cv::Mat img_rgb, img_resized; cv::cvtColor(img, img_rgb, cv::COLOR_BGR2RGB); cv::resize(img_rgb, img_resized, cv::Size(640, 640)); img_resized.convertTo(img_resized, CV_32FC3, 1.0 / 255.0); // 归一化 cv::Scalar mean(0.485, 0.456, 0.406); cv::Scalar std(0.229, 0.224, 0.225); cv::subtract(img_resized, mean, img_resized); cv::divide(img_resized, std, img_resized); // HWC 转 CHW cv::Mat blob cv::dnn::blobFromImage(img_resized); std::vectorint64_t input_shape {1, 3, 640, 640}; size_t input_tensor_size 1 * 3 * 640 * 640; // 创建输入张量 auto memory_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, (float*)blob.data, input_tensor_size, input_shape.data(), input_shape.size()); // 执行推理 const char* input_names[] {input_name.get()}; const char* output_names[] {output_name_0.get(), output_name_1.get()}; auto outputs session.Run(Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 2); // 解析输出 float* boxes_data outputs[0].GetTensorMutableDatafloat(); float* scores_data outputs[1].GetTensorMutableDatafloat(); // 后续后处理逻辑与 Python 端一致此处省略 std::cout 推理完成 std::endl; return 0; }逻辑说明Ort::Env是全局环境一个进程只需要一个。SessionOptions里设置线程数和图优化级别ORT_ENABLE_ALL会启用所有可用优化。GetInputNameAllocated返回的是智能指针需要.get()取原始字符串。blobFromImage是 OpenCV 自带的 HWC 转 CHW 工具省去手动 transpose。参数说明SetIntraOpNumThreads(4)控制算子内并行线程数根据 CPU 核心数调整一般设成物理核心数。input_shape必须和模型输入完全一致如果模型是动态轴这里要按实际输入填。outputs返回的Ort::Value数组顺序和output_names一一对应。4.3 编译与运行常见链接错误处理编译命令很简单cd cplusplus mkdir build cd build cmake .. make -j4 ./rtdetr_demo但实际跑的时候大概率会遇到链接错误。最常见的是undefined reference to Ort::...这说明 CMake 没找到 ONNXRuntime 库。解决方法是确认ONNXRUNTIME_ROOT路径正确并且lib目录下有libonnxruntime.so。另一个常见问题是 OpenCV 版本不匹配比如代码里用了cv::dnn::blobFromImage但你的 OpenCV 编译时没开 dnn 模块那就需要重新编译 OpenCV 或改用cv::split手动转 CHW。注意Linux 下运行时如果报error while loading shared libraries: libonnxruntime.so需要把 ONNXRuntime 的 lib 目录加到LD_LIBRARY_PATH里或者把.so文件复制到/usr/local/lib并执行ldconfig。5. 避坑与排查部署 RT-DETR 时最容易翻车的五个点5.1 检测框全部偏移或缩放错误现象推理能跑通但画出来的框位置完全不对要么集中在图像左上角要么尺寸明显偏小。原因预处理时 resize 没有保持宽高比但后处理时却按原图比例去映射坐标。RT-DETR 的 ONNX 导出通常假设输入是正方形 resize如果你用了 letterbox 填充后处理就必须先去掉填充再映射。解决确认预处理和后处理使用同一套坐标变换逻辑。如果预处理是直接 resize 到 640x640后处理就直接乘以orig_w/640和orig_h/640如果用了 letterbox后处理要先减去填充偏移再缩放。5.2 类别索引与标签文件对不上现象检测框位置正确但类别名显示错误比如把公交车标成了卡车。原因labels.txt里的类别顺序和模型训练时的类别顺序不一致。COCO 数据集的 80 类有标准顺序但有些导出脚本会调整顺序或过滤掉某些类。解决打印scores的维度确认是 80 类还是 91 类COCO 原始类别数含空类。然后对照labels.txt的行数确保一一对应。如果模型输出是 91 类需要按 COCO 的类别映射表过滤掉空类。5.3 ONNXRuntime 版本不匹配导致模型加载失败现象Python 端能加载模型C 端报Failed to load model或Unsupported operator。原因C 链接的 ONNXRuntime 库版本和 Python 端不一致或者 C 库是 CPU-only 版本而模型包含 CUDA 算子。解决用pip show onnxruntime确认 Python 端版本然后下载对应版本的 C 预编译包。如果模型是在 GPU 上导出的C 端也需要用 GPU 版本的 ONNXRuntime并确保 CUDA 和 cuDNN 版本匹配。5.4 内存泄漏与重复加载模型现象长时间运行后内存持续增长最终 OOM。原因每次推理都新建Ort::Session没有复用。Ort::Session的构造开销很大而且不释放会累积内存。解决把Ort::Session作为全局单例或类的成员变量只在初始化时创建一次。Ort::Env同理一个进程一个就够。推理时复用同一个 sessionRun方法是线程安全的但要注意输入输出张量的生命周期。5.5 多线程推理时的张量竞争现象单线程推理正常多线程时结果随机错误或程序崩溃。原因多个线程共享同一个Ort::Value输入张量或者共享输出缓冲区。解决每个线程创建独立的输入张量和输出缓冲区Ort::Session可以共享。如果用的是Ort::IoBinding每个线程需要独立的 binding 对象。简单做法是每个线程走完整的session.Run调用让 ONNXRuntime 内部管理内存。6. 进阶技巧用 IoBinding 减少内存拷贝与性能验证当你把基础流程跑通之后下一步就是优化推理性能。ONNXRuntime 提供了一个叫IoBinding的机制允许你直接把输入输出绑定到预分配的内存上避免每次Run时的数据拷贝。对于 RT-DETR 这种输入输出都比较大的模型这个优化能带来 10% 到 20% 的延迟下降。// 创建 IoBinding Ort::IoBinding binding(session); // 预分配输入内存 std::vectorfloat input_data(1 * 3 * 640 * 640); auto memory_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); // 绑定输入 binding.BindInput(input, input_tensor); // 绑定输出到预分配的内存 std::vectorfloat boxes_data(1 * 300 * 4); std::vectorfloat scores_data(1 * 300 * 80); std::vectorint64_t boxes_shape {1, 300, 4}; std::vectorint64_t scores_shape {1, 300, 80}; Ort::Value boxes_tensor Ort::Value::CreateTensorfloat( memory_info, boxes_data.data(), boxes_data.size(), boxes_shape.data(), boxes_shape.size()); Ort::Value scores_tensor Ort::Value::CreateTensorfloat( memory_info, scores_data.data(), scores_data.size(), scores_shape.data(), scores_shape.size()); binding.BindOutput(boxes, boxes_tensor); binding.BindOutput(scores, scores_tensor); // 执行推理数据直接写入预分配内存 session.Run(Ort::RunOptions{nullptr}, binding); // 此时 boxes_data 和 scores_data 里已经是推理结果逻辑说明BindInput和BindOutput把张量地址告诉 ONNXRuntimeRun时就不再需要额外的内存分配和拷贝。输入数据直接填到input_data里推理完直接读boxes_data和scores_data。参数说明input_shape、boxes_shape、scores_shape必须和模型定义完全一致。BindOutput绑定的内存大小要足够容纳输出否则会越界写。如果你的模型输出是动态形状需要先跑一次Run拿到实际形状再分配内存。验证性能提升的方法很简单在Run前后加std::chrono计时对比用 IoBinding 和不用 IoBinding 的耗时。我一般会跑 100 次取平均值排除第一次的预热开销。另外SetGraphOptimizationLevel设成ORT_ENABLE_ALL后第一次推理会做图优化耗时较长从第二次开始才是稳定性能。还有一个容易被忽略的点RT-DETR 的输入分辨率对速度影响很大。640x640 和 512x512 的推理耗时可能差一倍但精度下降通常只有一两个点。如果你的场景对实时性要求高可以尝试导出 512 输入的 ONNX 模型用同样的代码跑只需要改input_shape和预处理里的 resize 尺寸。从那以后我每次部署新模型都会先用 Python 端跑通并记录基准延迟再上 C 端做 IoBinding 优化最后用同一张测试图对比两端输出是否一致。这个习惯帮我省了很多来回排查的时间。希望帮到你。本文还有配套的精品资源点击获取