1. 项目概述与整体方案选型
1.1 核心需求解析
最近手里有个活儿,要给宇树Go2四足机器人做一套基于视觉的目标识别能力,核心任务是在机载边缘设备上实时跑YOLOv5,识别前方障碍物和指定目标物。一开始我犹豫过方案,是用Go2自带的算力模组,还是外挂一台高性能边缘计算设备。后来综合对比下来,选了 Jetson Orin 系列作为外挂算力平台。原因很直接:Go2本体虽然有算力模组,但要同时跑运动控制、导航SDK和视觉推理,资源会很紧张;外挂一台Orin,图像推理走独立链路,机身控制照旧走宇树官方SDK,两边互不干扰,开发调试也清爽得多。
这个项目涉及的环节比较长:从硬件选型、JetPack系统烧录、Python/CUDA环境配置、YOLOv5代码拉取与依赖安装,到模型训练、权重转换、TensorRT加速,再到接Go2的SDK做数据流打通,每一环都有坑。我算是把这些坑基本都趟了一遍,写出来给后面做同类项目的朋友省点时间。
1.2 为什么选 Jetson Orin 而非其他平台
边缘部署这事,可选平台其实不少:树莓派5、Jetson Nano、Jetson Orin NX / AGX Orin、Intel NUC配神经网络加速棒,甚至直接上RK3588之类的国产板子。我做选型时主要卡了三个硬指标:
- 算力要能跑得动YOLOv5s以上的模型,而且不能只是勉强能跑,最好留出余量做后续模型升级。
- 视频输入链路要友好,毕竟Go2自带的相机是USB输出,需要平台对USB摄像头和GStreamer管道有完善的驱动支持。
- 功耗和体积要适合装到Go2机身载板上,不能背个巨型供电模块。
树莓派5性价比不错,但算力用在YOLOv5s上跑到实时(30FPS以上)很吃力,实测大概只能跑到10~15FPS,而且TensorRT这种加速生态在树莓派上基本用不了。Jetson Nano属于上一代产品,显存和算力都偏低。Orin系列里面,Orin NX 8GB/16GB和AGX Orin都很有竞争力,最后我选了Orin NX 16GB版本——功耗在10W~25W可调,峰值算力能到100 TOPS,板子尺寸比一张名片大不了多少,非常适合做机器人机载算力。
1.3 整体技术链路设计
整套系统的架构我画在脑子里是这样的:Go2机身上的USB摄像头 → Jetson Orin的USB口 → GStreamer取流 → Python程序逐帧处理 → YOLOv5推理 → 识别结果(类别、坐标、置信度) → 通过UDP串口或网络协议回传给Go2的运控SDK → 机器人根据识别结果执行动作。
在这个链路里,YOLOv5只是中间一环,但它决定了整个系统的感知能力上限。所以模型训练和转换环节花的时间最多,也是后面踩坑最密集的区域。整个项目跑通之后,单帧延迟从最初的120ms以上优化到了35ms左右,基本满足Go2室外巡检任务的需求。
2. 环境搭建与系统烧录实战
2.1 JetPack系统刷机与启动黑屏排查
Jetson Orin装系统,常规路径是先用另一台Ubuntu主机,下载NVIDIA SDK Manager或使用命令行工具烧录JetPack。我这边用的是SDK Manager图形界面,烧录的是JetPack 5.1.2,自带CUDA 11.4、cuDNN 8.6、TensorRT 8.5,这套版本组合在后续装PyTorch和YOLOv5时比较省心。
刷机本身没什么技术含量,按向导操作就行。真正的坑出现在第一次开机——这也是热搜词里“jetson orin nano启动后黑屏”出现频率很高的原因。我遇到的情况是:供电用的是一块12V 5A的DC电源,理论上足够,但开机后HDMI输出没有画面,风扇转一下停一下,像是进入了反复重启的死循环。
排查了一下午,最后发现是供电接触不良和系统引导分区的问题。先说供电:Orin NX载板的电源接口看着是标准的DC圆头,但不同载板对电源规格要求差异挺大,我手里这块载板要求12V 5A,换了一根粗线径的电源线后稳定多了。再说系统引导,我重新烧录时选择了完整擦除(Full Flash),而不是仅更新分区(Update),彻底清空后再烧,黑屏问题就消失了。如果你们也遇到启动黑屏,优先检查电源线径和载板供电规格,其次再考虑重新刷机。
2.2 Python与CUDA环境的“最佳实践”配置
Orin刷完系统后,系统自带的是Python 3.8,但NVIDIA的很多工具链依赖特定的Python小版本,建议直接用系统Python,不要手动升级到3.10/3.11,否则后面编译一些依赖包会非常痛苦。
我的环境配置顺序是这样的:
# 1. 更新系统 sudo apt update && sudo apt upgrade -y # 2. 安装基础依赖 sudo apt install -y python3-pip libopenblas-dev libopenmpi-dev # 3. 安装PyTorch(关键:必须用NVIDIA预编译的wheel,不能直接用pip install torch) # 到NVIDIA官网下载对应JetPack 5.1.2的torch 2.0.0 wheel pip3 install torch-2.0.0-cp38-cp38-linux_aarch64.whl # 4. 安装对应版本的torchvision(同样用NVIDIA预编译版) pip3 install torchvision-0.15.0-cp38-cp38-linux_aarch64.whl # 5. 验证CUDA可用 python3 -c "import torch; print(torch.cuda.is_available())"这里必须强调:绝对不能直接从PyPI源pip install torch,因为那边默认拉取的是x86_64架构的包,在ARM64的Orin上要么报错要么装上一个纯CPU版本,GPU完全用不上。NVIDIA为Jetson平台维护了一套预编译的PyTorch wheel包,只能在NVIDIA官网的论坛或官方文档里找到下载入口。
2.3 YOLOv5代码拉取与依赖安装
YOLOv5的官方代码仓库在GitHub上,直接git clone就好。但要留意一件事:YOLOv5的master分支更新很频繁,依赖版本也跟着变,如果直接用最新代码,很容易出现某个依赖包版本不兼容的问题。我的做法是固定到一个稳定release版本。
git clone https://github.com/ultralytics/yolov5 cd yolov5 git checkout v7.0.0 pip3 install -r requirements.txtrequirements.txt里列的依赖比较多,其中容易出问题的几个是:opencv-python、matplotlib、numpy。在Jetson平台上,opencv不建议从pip安装,因为NVIDIA的系统里已经预装了一版带CUDA加速的OpenCV,直接用系统的版本反而性能更好。如果pip把系统的OpenCV覆盖了,后续用GStreamer取流时可能会遇到一堆莫名其妙的报错。
我的做法是:先装YOLOv5的其他依赖,然后手动卸载pip安装的opencv-python-headless,保留系统自带的OpenCV。
# 安装YOLOv5依赖,但不装opencv相关 sed -i '/^opencv/d' requirements.txt pip3 install -r requirements.txt # 验证系统OpenCV python3 -c "import cv2; print(cv2.__version__)"这一步能省下后面大量排错时间,尤其是当你需要同时用cv2.VideoCapture和GStreamer管道时,系统自带OpenCV的兼容性好得多。
3. 模型训练、超参数调节与权重转换
3.1 训练自己的数据集:从标注到格式整理
YOLOv5的模型训练是整个项目里最花时间的一环。如果你只是用官方预训练权重做一次验证,那很简单,但实际项目里几乎都要微调自己的数据集。我在这个项目里用了一个安全帽佩戴检测的数据集,后来也扩展了部分自定义目标类别。
数据集准备有几个关键点:
- 图片数量:每个类别至少准备200~500张图片,类别越少,需要的图片相对越少,但背景尽量丰富,避免模型过拟合。
- 标注格式:YOLOv5使用txt格式的标注文件,每行格式是“class x_center y_center width height”,坐标值都是归一化到0~1之间的浮点数。
- 数据集目录结构要严格按照YOLOv5的要求排列:
datasets/ custom/ images/ train/ val/ labels/ train/ val/我踩过的一个坑是:用LabelImg标注完导出的格式是Pascal VOC的XML格式,需要先转换成YOLO格式。很多新手直接用某个转换脚本一跑了之,结果类别编号对不上,训练时loss爆表,或者干脆不收敛。我的建议是转换完成后抽几张图,用YOLOv5自带的验证脚本可视化一下标注框,确认框位置正确再开始训练。
3.2 YOLOv5超参数的本质理解与调整策略
说到超参数,很多人直接套用官方默认的hyp.scratch-low.yaml,但实际项目里还是要根据自己的数据集和场景做调整。YOLOv5的超参数主要分成几类:优化器参数(lr0、momentum、weight_decay)、数据增强参数(hsv_h、hsv_s、hsv_v、degrees、translate、scale、shear、perspective、flipud、fliplr)、损失函数权重(box、cls、cls_pw、obj、obj_pw)等。
这里我举几个实际调过的参数:
- lr0(初始学习率):默认0.01,小数据集、类别少的场景可以降到0.005,避免前期震荡太厉害。
- mosaic(马赛克增强):默认是1.0,也就是每张训练图都由4张图拼接。这个增强对小目标检测非常有效,但在某些特定场景(比如检测目标本身很小、背景单一)反而会引入太多噪声,可以降到0.5试试。
- degrees(旋转增强):安全帽检测场景里,帽子在图像中的旋转角度不会太大,设成5~10就够了,设太大反而让模型学到不真实的姿态。
- fliplr(水平翻转):默认0.5,这个一般不用改,几乎所有目标检测任务都受益于水平翻转。
超参数调整不要贪多,一次只调一两个,观察验证集的mAP变化。我通常是先跑5~10个epoch看趋势,再决定要不要继续调。
3.3 训练命令与TensorRT导出实战
模型结构我选了YOLOv5s,输入分辨率640x640,在Orin NX上这个分辨率的性价比最高。训练命令:
python3 train.py \ --data custom.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 16 \ --epochs 100 \ --hyp hyp.scratch-low.yaml \ --device 0batch-size在Orin上可以稍微大胆一点,16GB显存跑YOLOv5s,batch-size 32也没问题,但建议还是16起步,先看显存占用再加。
训练完成后,模型导出TensorRT engine是这个项目提速的关键一步。YOLOv5官方代码自带export.py,可以直接导出engine格式,但用起来有几个坑。
python3 export.py \ --weights runs/train/exp/weights/best.pt \ --include engine \ --device 0 \ --half这行命令会自动调用TensorRT的Python API完成模型转换。但实际执行时经常报错,比如“TensorRT: export failure: can't parse the ONNX file”。这个问题的根源是PyTorch导出的ONNX模型里有一些TensorRT不兼容的算子,常见的是Focus层——YOLOv5 v6.0之前的版本用Focus做下采样,v6.0之后改成了普通的Conv层,但老权重转换时还是会遇到。
解决方法是升级到v7.0以上的YOLOv5代码,或者在导出时把opset版本调高一些:
python3 export.py \ --weights runs/train/exp/weights/best.pt \ --include engine \ --device 0 \ --half \ --opset 17我用的是v7.0 + opset 17的组合,一次就过了。导出成功后,在推理脚本里直接用torch.hub加载engine文件,就能享受到TensorRT加速。
4. 与宇树Go2通信串联与实时推理优化
4.1 宇树Go2的SDK接入方式回顾
宇树Go2提供了丰富的SDK接口,常用的有UDP控制接口和网络API。在边缘部署场景下,我采用的方案是:Jetson Orin作为独立的感知节点,推理结果通过UDP协议发给Go2的运控模块。Go2的SDK里有一个UnitreeGo2的Python库,可以接收外部指令并控制机器人运动。
具体的数据流设计是这样的:
- Jetson Orin端:摄像头取流 → YOLOv5推理 → 得到目标类别和坐标 → 计算出目标相对机器人的偏移角度和距离 → 打包成JSON或二进制格式 → 通过UDP发送到Go2的IP地址和端口。
- Go2端:SDK监听UDP端口 → 解析指令 → 控制云台或底盘运动,让目标保持在机器人视野中央。
这个方案的好处是:宇树Go2原有的运动规划能力不需要大改,我们只是给它加了一个“视觉大脑”。而且UDP通信的调试成本低,两边用Python就能快速联调。
4.2 相机取流与GStreamer管道配置
Go2机身上的摄像头一般通过USB接到Orin上,直接用OpenCV的cv2.VideoCapture(0)就能读取,但这种方式在需要高性能时会受限。为了提高实时性,我改用GStreamer管道取流,配合Orin的硬件编解码单元,能显著降低CPU占用。
gst-launch-1.0 v4l2src device=/dev/video0 ! \ video/x-raw, width=1280, height=720, framerate=30/1 ! \ videoconvert ! \ video/x-raw, format=BGR ! \ appsink在代码里用OpenCV调用GStreamer管道的方式:
import cv2 pipeline = ( "v4l2src device=/dev/video0 ! " "video/x-raw, width=1280, height=720, framerate=30/1 ! " "videoconvert ! video/x-raw, format=BGR ! appsink" ) cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER)这里有个细节:Go2的相机如果直接插在Orin上,设备节点可能是video0,也可能是video1,取决于有没有其他USB摄像头。建议先用ls /dev/video*确认设备号,再调整管道参数。
4.3 推理性能优化:从120ms到35ms的调优路径
YOLOv5在Orin上跑起来后,第一版性能其实不理想。我用的是直接加载PyTorch权重的方式推理,单帧延迟在120ms左右,大概8FPS,这对机器人实时控制来说太慢了。优化路径主要做了三步:
第一步是TensorRT引擎替换。把PyTorch权重换成engine格式后,推理延迟直接降到了60ms左右,约16FPS。这步收益最大,基本是白捡的加速。
第二步是输入分辨率调整。从1280x720直接缩放成640x640,并调整了摄像头取流分辨率到1280x720,推理分辨率保持640x640不变。这一步看似没变,实际上是把图像缩放过程从Python的OpenCV操作挪到了TensorRT的预处理阶段,省掉了部分拷贝开销。
第三步是启用FP16半精度推理。Jetson Orin的Tensor Core对FP16有专门加速,启用后延迟压到了35ms左右,大约28FPS。如果再激进一点用INT8量化,能到20ms以内,但INT8量化需要校准数据集,而且精度会有轻微下降,我没有在安全帽识别这种对精度要求较高的场景使用。
优化后的代码骨架大致是:
import cv2 import torch # 加载TensorRT engine model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.engine', device='cuda:0') # 视频取流 cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER) while True: ret, frame = cap.read() if not ret: continue # 推理 results = model(frame, size=640) # 解析结果 detections = results.xyxy[0].cpu().numpy() for det in detections: x1, y1, x2, y2, conf, cls = det label = model.names[int(cls)] # 计算目标中心和偏移 cx = (x1 + x2) / 2 cy = (y1 + y2) / 2 # 这里把cx, cy和置信度打包发送给Go2有一点要提醒:torch.hub第一次加载模型时会联网下载一些配置文件,如果运行环境是离线的,需要提前把依赖文件准备好。我在调试时遇到过联网超时导致加载失败的情况,后来直接把YOLOv5仓库放在本地,用custom路径加载,彻底断网也能跑。
5. 实战踩坑实录与性能调优全记录
5.1 启动黑屏与SSH远程排查技巧
回到前面提到的“Jetson Orin启动后黑屏”问题,我再多说几句。黑屏不等于系统挂了,很多时候系统其实正常启动了,只是显示输出没有工作。遇到这种情况,最有效的办法是直接用网线连接Orin和笔记本,通过SSH登录排查。
# 在笔记本上扫描Orin的IP地址 nmap -sn 192.168.1.0/24 # 或者直接在路由器后台找新接入的设备 ssh username@<orin-ip>登录系统后,先看系统日志:
dmesg | grep -i error journalctl -xe我当时就是从日志里发现显卡驱动初始化失败,然后又定位到是电源供电不稳导致GPU降频重启。更换电源后,再用sudo systemctl restart gdm3重启图形界面就正常了。
5.2 YOLOv5训练与推理中的典型报错
整个项目过程中,我记录了几类高频错误,这里整理成速查表:
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
| ModuleNotFoundError: No module named 'torchvision' | 环境变量或pip安装路径不对 | 确认用pip3而不是pip;检查sys.path |
| CUDA out of memory | batch-size设置过大 | 减小batch-size,或启用梯度累积 |
| 训练loss不为0但mAP始终为0 | 数据集标注类别编号错乱 | 可视化标签,检查类别索引与names配置 |
| TensorRT export failure | ONNX算子不兼容 | 升级到v7.0,设置--opset 17 |
| 推理时检测框偏移严重 | 输入图像缩放方式与训练时不一致 | 确保推理时letterbox填充一致,use YOLOv5自带预处理 |
| GStreamer管道报设备忙碌 | 摄像头被其他进程占用 | 关闭OpenCV的默认videocapture,用fuser查看占用进程 |
这里重点说一下“推理时检测框偏移严重”这个问题。YOLOv5在训练时会把图像统一缩放到640x640,缩放方式不是简单拉伸,而是保持宽高比后用灰色填充剩余区域,即letterbox操作。如果推理时直接用cv2.resize把图像拉成640x640,检测框的位置和大小就会偏移。所以推理端必须要复用YOLOv5自带的letterbox预处理逻辑,我用的是model(frame, size=640)这行自动处理,不会踩坑。
5.3 通信延迟与实时性冲突的折中方案
Go2的运控指令要求频率一般在50Hz以上,而YOLOv5的推理帧率在28FPS左右,这之间天然存在节奏差异。我的做法是:视觉推理线程跑独立循环,推理结果不断刷新到一个共享变量里;发送线程按25Hz的频率把最新结果打包发给Go2,即使视觉某帧延迟,发送线程依然保持稳定节奏,不会让Go2的运控因为收包不均匀而抖动。
实际测试下来,端到端延迟(从画面出现目标到Go2执行动作)大约在80ms左右,对于巡检、跟随这类任务完全够用。如果你做的是高速动态抓取这类要求极低延迟的任务,那就需要把模型换成更轻量的版本,或者用INT8量化,甚至可以裁剪摄像头ROI只推理画面中间区域,牺牲一点视野换速度。
5.4 散热与功耗管理的现实考量
Jetson Orin NX的性能让人满意,但满载时的发热和功耗必须认真对待。我一开始用的是一个不带风扇的被动散热外壳,测试时发现长时间跑推理后芯片温度飙到85度,然后系统自动降频,帧率从28FPS掉到18FPS,非常影响体验。
后来换成主动散热方案:一个5V的PWM风扇对着散热鳍片吹,加上一个简单的温控脚本,当温度超过70度时提高风扇转速。功耗方面,Orin NX我设置了20W的功耗模式:
sudo nvpmodel -m 0 # 0表示MAXN模式(25W),1表示15W模式,2表示10W模式室外巡检场景我建议用20W模式,兼顾性能和续航。如果Go2本身的电池供电能力有限,可以考虑用一块独立的USB-PD电池给Orin供电,避免大电流负载影响Go2主控的稳定性。
6. 方案复盘与后续可扩展方向
6.1 目前的实际检测效果
整个项目跑通后,我做了几轮场景测试。在普通室内光照条件下,YOLOv5s模型对安全帽目标的mAP@0.5达到了0.93左右,推理帧率稳定在28FPS,漏检率在可接受范围内。室外强光环境下,相机的自动曝光会对检测效果产生一定影响,主要表现为逆光时目标偏暗、置信度下降,这时可以适当调低置信度阈值(从默认的0.25调整到0.2),或开启相机的HDR模式。
从部署成本角度看,整个方案单套硬件成本主要是Orin NX模组和载板的费用,软件成本基本为零(核心组件都是开源或官方SDK)。对比原来租用云端GPU做推理的方案,边缘部署的长期成本明显更低,而且完全摆脱了网络带宽和延迟的束缚。
6.2 可改进的空间在哪里
虽然项目已经达到预期目标,但我自己也清楚还有几个可改进的方向。
第一个方向是模型轻量化。YOLOv5s在Orin NX上跑到28FPS,如果换成YOLOv5n或YOLOv8n,理论上可以突破45FPS,但精度会下降2~4个百分点。对于纯避障任务,可以牺牲精度换速度;对于识别任务,还是YOLOv5s更稳妥。
第二个方向是融合深度信息。当前方案只有2D检测框,缺少目标距离信息。后续如果接入深度相机(如RealSense D435),把深度图的对应像素点与检测框中心对齐,就能输出目标的3D坐标,Go2的机械臂或者底盘避障就更精确了。
第三个方向是多传感器融合。Go2本身就带激光雷达和多个视觉传感器,如果把YOLOv5的2D检测结果与激光雷达的3D点云做融合,再接入宇树SDK的地图模块,整个系统的环境感知能力会上一个大台阶。这是后续想花时间深入的方向。
6.3 对同样在做边缘部署的人说几句
最后分享一点个人的小经验。做边缘部署项目,最忌讳的就是“重算法、轻工程”,很多人把精力都花在调模型精度上,忽略了环境搭建、推理加速、通信链路这些工程环节。实际上在Jetson这类边缘平台上,成熟的模型算法反而不是稀缺资源,真正决定项目成败的是能不能把模型稳定、高效地跑起来,并让它融入整个机器人系统。
我在调试过程中最大的体会就是:排错一定要有方法论。遇到报错别慌,先看日志,再复现最小场景,最后才动代码。很多时候一个看似复杂的问题,根因只是某个依赖包版本不对,或者某个USB设备的权限没配好。把基础环境弄扎实,后续的迭代速度会快很多。
如果这篇文章对正在做宇树Go2开发、Jetson部署或YOLOv5边缘推理的朋友有一点帮助,那就值得了。这套流程里的每一个环节,都可以单独拿出来深入优化,踩坑的路才刚刚开始。