
简介面向边缘计算与智能视频分析开发者这份项目包演示了在Jetson-Nano上借助TensorRT部署YOLOv8实现车辆和摩托车的实时检测、跟踪与计数。它直面边缘设备算力有限与实时性要求高的矛盾适合需要将深度学习模型落地到嵌入式平台的算法工程师、竞赛选手或研究人员。压缩包共35个文件、206.24MB以C源码h/cpp/hpp、Python脚本、Markdown/TXT文档、yaml配置及mp4/avi演示视频为主。源码工程覆盖训练、模型导出、推理与跟踪计数模块文档说明环境准备和部署步骤视频则方便先看效果再动手。资源按train、detect、track_count等模块组织从数据准备、模型训练到TensorRT转换和边缘端部署均有完整流程同时提供C与Python两种调用方式便于对照学习。目前已有390人学习下载对于想快速跑通YOLOv8边缘端部署全流程的开发者是一份实践性很强的参考。1. 在 Jetson-Nano 上推 YOLOv8 的真实瓶颈TensorRT 不是可选项把 YOLOv8 检测模型塞进 Jetson Nano 时第一个打击往往来自帧率。在 Nano 的 Maxwell GPU 上直接跑 PyTorch 推理一个 640x640 的 YOLOv8n 都很难稳定到 15 FPS更不用说在视频流里同时做车辆和摩托车的跟踪计数。TensorRT 在这里不是加速选项而是让项目能跑起来的必要条件通过层融合、精度校准和 kernel 自动调优把模型变成针对当前 GPU 高度特化的执行计划推理延迟能压到 PyTorch 的 1/3 甚至更低。这个项目正好落在边缘计算里最常见的场景用固定摄像头统计双向车流中的车辆和摩托车数量。适合两类人——一类是要在资源受限设备上交付算法的工程师想知道 ONNX 导出到 engine 构建之间有哪些坑另一类是刚接触 YOLOv8 部署的学生需要一份能直接复现的完整流程。下面从模型转换讲起最后落到 C 和 Python 两套代码怎么选。2. 模型准备从 YOLOv8 权重到 TensorRT 引擎的完整链路在接触 main.py 之前先把离线的模型转换跑通。这一步决定了后续推理的准确率和实时性也是项目里最容易返工的地方。整个链路是 best.pt → best.onnx → best.engine。2.1 环境版本匹配与依赖安装Jetson Nano 的 JetPack 镜像已经带了 CUDA、cuDNN 和 TensorRT。安装 Python 侧依赖时最大的原则是不要用 pip 任意升级系统 TensorRT。JetPack 4.6 系列自带 TensorRT 8.2对应的 PyTorch 需要从 NVIDIA 提供的预编译 wheel 安装而不是 PyPI 上的最新版。先确认基础环境python3 -c import tensorrt; print(tensorrt.__version__) nvcc --version如果 TensorRT Python 包没有装可以在 JetPack 的 apt 源里安装sudo apt install python3-libnvinfer-dev然后安装训练与导出的依赖。这里有一个容易踩的坑ultralytics 最新版对 torchvision 的版本有硬性要求在 Nano 的老 Python 环境里经常装不上。建议把 PyTorch 版本固定到 1.10 或 1.11再配合对应的 torchvision。项目里的requirements.txt如果直接写可以用如下命令安装pip3 install ultralytics8.0.0 onnx onnxruntimeonnxruntime不是导出的必需品但用来在 PC 上先验证 ONNX 的输入输出 shape能节省大量在 Jetson 上反复构建 engine 的时间。参数说明ultralytics负责加载权重、导出 ONNX、训练和验证固定大版本避免 API 变动导致export_onnx.py里的调用方式失效。我一般会在 PC 上把导出和验证做了再把 ONNX 拷到 Nano 上构建 engine。Nano 的 CPU 构建 engine 很慢一个 YOLOv8n 可能要十几分钟。2.2 导出 ONNXC2f 结构与动态轴的坑YOLOv8 的 backbone 使用了 C2f 结构它把输入分成两路经过多个 Bottleneck 后拼接再走一层卷积。在 PyTorch 里看起来是几个模块嵌套导出 ONNX 后会被展开成一串算子TensorRT 解析时看到的是多个卷积和拼接。这意味着导出的 ONNX 不会像网络结构图那样保留清晰的 C2f 边界调参时要接受这一点。项目里的export_onnx.py核心逻辑一般是这样from ultralytics import YOLO model YOLO(runs/train/exp/weights/best.pt) # 换成训练好的权重 model.export( formatonnx, imgsz640, dynamicFalse, simplifyTrue, opset12, halfFalse, )参数说明参数值说明imgsz640必须和训练尺寸一致Nano 上不建议 1280 输入dynamicFalseJetson 场景固定 batch1 最省心simplifyTrue清理多余 reshape 和 transpose减少 TRT 报错opset12TensorRT 8.x 兼容性最好的档位halfFalse半精度放到 TensorRT 构建阶段做导出后会得到 best.onnx。用 onnxruntime 检查一下输出 shapeimport onnxruntime as ort sess ort.InferenceSession(best.onnx) for inp in sess.get_inputs(): print(inp.name, inp.shape) for out in sess.get_outputs(): print(out.name, out.shape)车辆和摩托车两类时输出一般是[1, 6, 8400]其中 6 4 个坐标 2 个类别概率8400 是 640x640 下三个尺度特征图铺平后的 anchor 数量。如果输出是[1, 8400, 6]后处理逻辑要对应调整这是 YOLOv8 不同导出方式最常见的差异点。2.3 用 trtexec 或 Python API 构建 FP16 engine构建 engine 有两种方式项目文档prepare_jetson.md里通常会提供命令。推荐先用 trtexec 做一次构建它能直接输出每个算子的耗时方便确认瓶颈。cd /usr/src/tensorrt/bin sudo ./trtexec \ --onnx/home/nano/traffic_count/best.onnx \ --saveEngine/home/nano/traffic_count/best.engine \ --fp16 \ --workspace2048如果 TensorRT 版本新一点8.5 以后--workspace已经被--memPoolSize取代sudo ./trtexec \ --onnxbest.onnx \ --saveEnginebest.engine \ --fp16 \ --memPoolSizeworkspace:2048参数说明--fp16开启半精度Nano 的 GPU 对 FP16 有硬件加速这是延迟下降的主要来源--workspace或--memPoolSize限制构建时的显存申请Nano 只有 4GB 内存共享给 2GB 比较稳妥如果不想用命令行也可以在 Python 里用tensorrt.Builder构建源码的src目录里可能已经有封装。构建成功后用下面的代码验证 engine 能正常加载import tensorrt as trt logger trt.Logger(trt.Logger.WARNING) with open(best.engine, rb) as f: runtime trt.Runtime(logger) engine runtime.deserialize_cuda_engine(f.read()) print(engine loaded:, engine.name)这里值得留意的是engine 包含了网络结构和权重部署时不需要再带 ONNX 和训练权重。几个文件的大小差异很大best.pt 通常在几十 MBbest.engine 往往比 ONNX 稍大一点但拷贝到设备后只能用 TensorRT 加载不能用 PyTorch 加载。3. 车辆/摩托车跟踪计数的实现逻辑从检测框到过线统计模型推理只是拿到了一帧的检测框要完成计数必须把前后帧的框关联成轨迹再和一条预先画好的虚拟线比较位置。这套逻辑集中在track_count目录下是项目的核心。3.1 从 TensorRT 输出到检测框的后处理YOLOv8 的输出是每个 anchor 的原始预测值需要经过 sigmoid、解码、NMS 三步才能变成可用的矩形框。在 Nano 上 IOU 和置信度阈值设置很关键阈值太高容易漏掉远处的小目标阈值太低会产生大量碎片框给后面的跟踪器带来压力。import numpy as np def decode_outputs(pred, conf_thres0.4, num_classes2): # pred 形状可能是 (1, 6, 8400)先改为 (8400, 6) pred pred.transpose(0, 2, 1).squeeze(0) # (8400, 6) boxes pred[:, :4] scores pred[:, 4:] cls_scores scores.max(axis1) cls_ids scores.argmax(axis1) mask cls_scores conf_thres boxes boxes[mask] cls_ids cls_ids[mask] cls_scores cls_scores[mask] return boxes, cls_scores, cls_ids说明这里假设 YOLOv8 输出坐标是中心点加宽高shape 是(1, 6, 8400)所以先转置到(8400, 6)类别分数取 max 而不是直接索引是因为输出可能包含多个类别车辆和摩托车要分开统计conf_thres参数后处理阶段可以单独调整但最终要以跟踪计数结果为准建议先用 0.4 跑一遍测试视频。坐标解码部分如果导出的 ONNX 已经包含解码逻辑上面拿到的boxes就是原图坐标。如果拿到的还是归一化状态需要乘以输入图片尺寸。识别这一点的方法是随机取一个框打印坐标看是否大于 1。NMS 可以用torchvision.ops.nms在 Nano 上处理 8400 个候选框时有一定 CPU 开销。更快的办法是用 TensorRT 的 EfficientNMS 插件把 NMS 放进 engine但那样会把输出格式改了后处理也要跟着改这个放到性能调优部分再展开。3.2 跟踪器选择IOU/卡尔曼/ByteTrack 的取舍项目没有用复杂的特征重识别模型因为 Nano 的算力预算不允许。常见做法是轻量卡尔曼滤波加匈牙利匹配也就是 ByteTrack 的基本思路。检测框在前后帧的移动用卡尔曼预测匹配代价用 IoU 和中心点距离的加权值。class TrackState: def __init__(self, box, cls_id, track_id): self.bbox box # x,y,w,h self.cls_id cls_id self.track_id track_id self.hit_streak 0 def iou_cost(prev_box, cur_box): # 计算两个框的 IoU返回 1 - IoU 作为匹配代价 ...参数层面的选择IOU 匹配适合车辆这种刚体目标前后帧位移不大摩托车和车辆重叠时会互相遮挡可以放宽置信度阈值用低分框进行二次匹配这是 ByteTrack 的关键改进对帧率较低的视频低于 10FPSIOU 容易断轨迹需要把匹配半径调大。在 Nano 上我一般会把跟踪器设计成每帧最多跟踪 50 个目标超过就丢弃得分最低的避免在大量车辆进入画面时 CPU 占用过高。调整参数时可以参考下面这组初始值参数推荐值说明conf_thres0.4太高漏检太低碎片框多iou_thres0.45NMS 抑制重合框max_track50防止 CPU 过载match_thres0.6匈牙利匹配阈值越小越容易分裂轨迹3.3 虚拟线计数实现draw_line 与计数逻辑计数的核心是一根线。utils/draw_line.py的作用是让人用鼠标在视频流上点两个点然后把线段坐标保存到配置文件里。到 main 里判断目标是否穿越这条线。def cross_line(prev_pt, cur_pt, line_p1, line_p2): # 向量叉积符号变化表示跨线 x1, y1 line_p1 x2, y2 line_p2 prev_s (x2 - x1) * (prev_pt[1] - y1) - (y2 - y1) * (prev_pt[0] - x1) cur_s (x2 - x1) * (cur_pt[1] - y1) - (y2 - y1) * (cur_pt[0] - x1) return (prev_s 0 and cur_s 0) or (prev_s 0 and cur_s 0)说明叉积的符号代表点在直线的哪一侧前后两帧符号相反说明这条轨迹穿过了线要统计双向车流可以检查是prev_s 0到cur_s 0还是相反方向然后分别累加到up_count和down_count车头方向会先经过车身中心点中心点跨线即可计数不需要整个框越过。需要给同一个 track_id 只计一次数否则车辆在线附近来回抖动会导致多次计数。代码里一般维护一个counted_ids集合或者给每个轨迹打一个countedTrue的标记。对于摩托车和车辆分方向计数结构可以设计成counts { vehicle: {up: 0, down: 0}, motorcycle: {up: 0, down: 0}, }帧数足够多以后跟踪器偶尔会丢失 ID这会让同一种目标被重复计数。解决思路是检测到轨迹跨越时再取该轨迹最近 5 帧的类别众数作为最终类别而不是只看当前帧的检测结果这样能减少类别跳变带来的误计。4. 项目源码结构与双语言部署Python 快速验证 vs C 生产落地拿到项目压缩包后不要急着跑 main.py。先理清文件之间的关系不然会被src目录、track_count目录混合的 Python/C 代码绕晕。这个仓库其实包含了两套部署方案Python 用于快速验证C 用于正式运行。4.1 源码文件地图项目根目录解压后结构大致如下train/ # 训练相关 export_onnx.py # PyTorch 权重转 ONNX main.py # Python 推理 跟踪计数 data.yaml # 训练数据配置类别名 requirements.txt # Python 依赖 src/ # Python 后处理/跟踪工具 test.mp4 test_1.mp4 test_2.mp4 # 测试视频 video_presentation.avi # 效果展示视频 utils/ draw_line.py # 在视频上画虚拟线并导出坐标 docs/ prepare_jetson.md # Jetson 部署步骤 train.md # 训练步骤 detect/ # C 端模型加载与推理 track_count/ # C 跟踪计数主模块 include/ src/ CMakeLists.txt关键点detect和track_count是 C 工程的输入输出一般把前者封装成库后者链接它src是 Python 版的工具集video_presentation.avi用于给客户或验收演示不用来做性能测试data.yaml记录了nc类别数和类别名后处理里num_classes必须和它一致。main.py和track_count/src/main.cpp分别对应两套入口输入都是视频和 engine 文件输出是叠加检测线、计数结果标注后的视频。4.2 Python main.py 推理流程Python 版本适合先跑通效果。整体流程加载 engine → 读取视频 → 预处理 → 推理 → 后处理 → 跟踪 → 计数 → 画框 → 写视频。加载 engine 的代码可以直接参考项目src目录下的封装import tensorrt as trt import numpy as np import cv2 class TrtEngine: def __init__(self, engine_path): logger trt.Logger(trt.Logger.WARNING) runtime trt.Runtime(logger) with open(engine_path, rb) as f: self.engine runtime.deserialize_cuda_engine(f.read()) self.context self.engine.create_execution_context() self.bindings [] self.outputs [] def infer(self, input_blob): # 这里省略 buffer 申请和 copy 细节 ...推理时一个常见的坑是输入图片的预处理YOLOv8 要求 BGR 转 RGB、除以 255、再转成 NCHW float很多人在这一步直接把 OpenCV 的 BGR 图喂进去导致检测结果完全错乱。正确的做法是def preprocess(img, size640): img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (size, size)) img img.astype(np.float32) / 255.0 img np.transpose(img, (2, 0, 1)) # HWC - CHW return np.ascontiguousarray(img[None])这里的np.ascontiguousarray是很多初学 TensorRT 的人忽略的细节。PyTorch 和 numpy 默认可能不是连续内存而 CUDA 的 memcpy 要求输入输出 buffer 是连续的。4.3 C main.cpp CMakeLists 编译要点C 版本部署时main.cpp的结构是读 engine 文件 → 创建IRuntime和IExecutionContext→ 为输入输出分配 GPU 显存 → 循环读取帧。TensorRT 的 C API 使用起来比 Python 版本繁琐但显存控制更明确适合 24 小时运行的监控服务。项目的track_count/CMakeLists.txt需要同时找到 TensorRT、CUDA、OpenCVcmake_minimum_required(VERSION 3.10) project(track_count) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(CUDA REQUIRED) find_package(OpenCV REQUIRED) # Jetson 上 TensorRT 头文件和库位于系统路径 set(TENSORRT_INCLUDE_DIR /usr/include/aarch64-linux-gnu) set(TENSORRT_LIB_DIR /usr/lib/aarch64-linux-gnu) add_executable(track_count main.cpp) target_include_directories(track_count PRIVATE ${TENSORRT_INCLUDE_DIR} ${CUDA_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS} ) target_link_libraries(track_count PRIVATE nvinfer nvinfer_plugin cudart ${OpenCV_LIBS} ) set_target_properties(track_count PROPERTIES CUDA_STANDARD 14 CUDA_STANDARD_REQUIRED ON )说明TensorRT 的库名是nvinfer而不是tensorrt编译时拼错是最常见的 CMake 报错nvparsers和nvonnxparser用于解析模型如果只加载 engine 文件链接nvinfer就够了OpenCV 在 Jetson 上可能编译成libopencv_*系列find_package(OpenCV REQUIRED)一般能正确找到。编译命令mkdir build cd build cmake .. make -j4 ./track_count ../test.mp4 ../best.engine这里-j4是 Nano 上比较稳定的并行度j8容易因为内存不足导致编译器被杀。Python 版和 C 版的定位差别可以参考下面的对照对比项Python 版C 版启动速度慢加载 numpy/trt 较重快直接加载 engine内存占用高多一层 Python runtime低显存可控开发速度快适合调算法参数慢适合固定流程适用场景实验验证、演示长时间运行、轻量交付5. 性能调优与测试帧率、准确率与常见问题的定位完成了部署接下来要回答两个问题这套系统在 Nano 上到底能跑多快结果到底准不准。测试的意义不只是为了汇报数据更是为了在后续调整阈值时有个参照。5.1 用 test.mp4 做基准测试与结果解读项目自带的 test.mp4、test_1.mp4、test_2.mp4 是不同的监控场景。测试时要固定模型输入尺寸、FP16 开关、后处理阈值否则数据之间不可比。我一般用下面的命令跑一个固定 300 帧的循环time python3 main.py \ --video test.mp4 \ --engine best.engine \ --num-class 2 \ --conf 0.4 \ --iou 0.45 \ --count 300统计结果时关注preprocess、inference、postprocess三个阶段的耗时。在 Nano 上一个 YOLOv8n FP16 engine常见结果如下表模型/精度输入尺寸推理耗时全流程 FPSYOLOv8n FP16640x64025~35 ms20~28YOLOv8s FP16640x64055~75 ms10~15YOLOv8n FP32640x64060~80 ms8~12表里的 FP32 数据是用 trtexec 不加--fp16构建得到的实际项目里 FP32 意义不大因为 Nano 的 CUDA 核心本来不多FP16 能减少一半显存带宽压力。如果实测帧率远低于表中数据先看两件事sudo jetson_clocks有没有执行默认电源模式会锁在低频率视频解码有没有成为瓶颈test.mp4如果是 1080P/30FPS用 OpenCV 的VideoCapture解码会占掉一个 CPU 核心。用tegrastats可以同时观察 CPU/GPU 频率和内存占用sudo tegrastats重点关注EMC_FREQ和GR3D_FREQ两个指标都要接近最高频率才能说明硬件跑满了。5.2 优化手段动态 batch、多 stream 和 GPU NMS优化顺序是先保证检测是唯一瓶颈再考虑后端加速。动态 batch 适合多路摄像头场景。把多个画面拼成一个 batch 输入 engine推理耗时不会等比例增长但 Nano 的显存只有 4GBbatch4 时输入分辨率通常得降到 480。具体做法是在导出 ONNX 时设置dynamicTrue然后用 trtexec 指定--minShapes和--maxShapes./trtexec \ --onnxbest.onnx \ --saveEnginebest_b4.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:2x3x640x640 \ --maxShapesimages:4x3x640x640NMS 放在 CPU 上会限制整个流程的下限。把 NMS 换成 TensorRT 的 EfficientNMS 插件后后处理可以并入 engine减少一次 D2H 拷贝但输出格式会变成[1, 1, max_det, 7]第 4~6 列是类别、置信度、坐标后处理代码需要同步改写。5.3 常见故障定位部署阶段最容易遇到的问题按出现频率整理了一张表现象常见原因定位方式trtexec 构建失败ONNX 算子 opset 太高缩小 opset 到 12看日志里第一个 unsupported 算子推理输出全是 0/NaN输入预处理 BGR/RGB 混淆先打印输出统计再用单张图片对比 PyTorch帧率上不去没有开 jetson_clockstegrastats 看 GR3D_FREQ视频卡顿但 GPU 使用率低OpenCV 解码瓶颈换成硬件解码 v4l2src ! nvvidconv计数重复轨迹丢失后 ID 重置检查跟踪器删除未命中帧的策略main.py 闪退CUDA context 没有释放在进程退出前调用 del engine并显式释放 binding定位用单帧调试脚本最有效下一章我会说明怎么快速对比 PyTorch 和 TensorRT 的输出。6. 实用技巧用一张图片快速验证 TensorRT 引擎输出差异改后处理或换 TensorRT 版本后判断有没有破坏模型行为最直接的方法是把 PyTorch 模型和 TensorRT engine 放在同一个进程里对同一张图推理然后对比两类输出。这个脚本在项目调试阶段能省下大量看视频的时间。核心思路是相同的前处理、相同的置信度阈值把两者的检测框画在同一张图上并计算匹配框的 IoU。如果 IoU 平均大于 0.8说明 engine 转换没有破坏精度如果接近 0先检查输入预处理。import torch import cv2 import numpy as np from ultralytics import YOLO from src.engine import TrtEngine img cv2.imread(test.jpg) blob preprocess(img, size640) # 复用前面定义的函数 # PyTorch 推理 pt_model YOLO(best.pt) pt_out pt_model.predict(img, conf0.4, imgsz640, verboseFalse)[0] # TensorRT 推理 engine TrtEngine(best.engine) trt_raw engine.infer(blob) trt_boxes, trt_scores, trt_cls decode_outputs(trt_raw, conf_thres0.4) # 把 pt_out 的框转成相同格式计算 IoU # 这里省略 IoU 函数直接打印重叠情况 print(PT boxes:, len(pt_out.boxes), TRT boxes:, len(trt_boxes))这段脚本里有两个容易出问题的点。第一preprocess用cv2.resize会把原图直接拉伸成 640x640而不是 letterbox。严格来说 YOLOv8 推理时要在保持宽高比的基础上填充灰度边否则同一个目标在两种模型下的坐标天然对不上。理想的做法是让 PyTorch 的predict也走完全相同的 letterbox。第二TensorRT 输出的坐标如果对应输入分辨率需要按640 / img.shape[1]之类比例映射回原图才能和pt_out的坐标比较。如果确认坐标映射没问题再对比类别。车辆和摩托车在样本不平衡时容易混淆先看 NMS 之前的类别分数分布再决定是调conf_thres还是补充训练数据。这个验证脚本不要放到生产代码里只留在开发目录当test.mp4上的计数结果异常时优先用这个脚本排除模型转换层的问题而不是去怀疑跟踪器的方向判断。本文还有配套的精品资源点击获取