基于YOLOv3与Jetson Nano的局部感知小车:从检测到跟随避障

发布时间:2026/9/12 12:40:53
基于YOLOv3与Jetson Nano的局部感知小车:从检测到跟随避障 简介这是一份基于YOLOv3与Jetson Nano的局部感知小车项目完整提供Python源码与文档说明可实现自动跟踪与避障功能。资源定位在计算机视觉与嵌入式AI交叉领域面向人工智能、自动化、电子信息等专业的学生和开发者既适合毕业设计、课程设计也适合作为机器人入门项目进行二次开发。整个资源包内含18个文件以12个Python脚本为主体涵盖目标检测、实时跟踪、摄像头调用等核心逻辑同时附带YOLOv3网络配置、类别标签、说明文档和示例图片压缩包仅94KB结构精简、依赖清晰便于快速阅读和部署。已有284人学习/下载代码经测试运行成功作者为个人毕设项目答辩评审平均分达96分并支持下载后远程咨询。使用者可参考README与源码理解YOLOv3检测与DaSiamRPN跟踪的协同流程在此基础上扩展为行人跟随、避障导航等实际应用。1. 从“能跑”到“会跟人”YOLOv3 与 Jetson Nano 局部感知小车的组合逻辑在 PC 上跑 YOLOv3 没什么可兴奋的放到 Jetson Nano 上就是另一回事4GB 内存、四核 Cortex-A57、128 核 Maxwell GPU刚好卡在“能跑但必须会优化”的区间。基于这个组合做局部感知小车意味着既要压住检测模型的计算量又要用 Python 把检测结果快速变成转向和油门指令。这套方案到底解决什么说直白点就是让小车在局部范围里找到目标、持续跟随、遇障停下或绕开而不依赖 GPS、雷达这类昂贵传感器。对想入门机器人视觉、手里已有 Jetson Nano 的研发和爱好者来说这篇文章正好覆盖从模型选型、环境部署、跟踪避障逻辑到参数调优的完整链路。先把 YOLOv3 在 Nano 上跑通再谈怎么把它变成一辆会主动跟随的小车。2. 局部感知的控制闭环YOLOv3 检测、Jetson Nano 算力与转向映射局部感知小车的本质是一条高速闭环摄像头采集图像检测模型输出目标框控制逻辑把目标框的偏移量换算成转向角再转成电机 PWM。任何一个环节延迟过高车就会画龙或追丢。这一章先把三件事讲透为什么检测部分选 YOLOv3、Jetson Nano 的算力边界在哪、目标框到电机指令的最短映射怎么设计。2.1 为什么选 YOLOv3 而不是 YOLOv5 或轻量分类器YOLOv3 不是最新模型但在这个场景里反而是最稳的选择。Darknet53 的检测头在 416×416 输入下有 3 个尺度对远处小目标和近处大目标都有响应YOLOv3-tiny 进一步砍成两层检测头在 Jetson Nano 上能把推理压到每秒 20 帧上下。相比之下 YOLOv5 的部署链对刚接触 Nano 的人更折腾PyTorch 导出、动态尺寸、自定义后处理都容易踩坑。轻量分类器如 SSD-MobileNet 虽然更小但对遮挡和小目标召回明显弱于 YOLOv3 系。做小车这种“目标会左右走动、距离还会变化”的场景宁可牺牲一点帧率也要保住检测稳定性。模型参数量检测尺度Jetson Nano 参考帧率部署成本YOLOv3约 61.9M3 尺度3-8 FPS中YOLOv3-tiny约 8.9M2 尺度15-25 FPS低YOLOv4-tiny约 6.1M2 尺度20-30 FPS中SSD-MobileNetV2约 3.4M单尺度10-15 FPS中上表的帧率是参考区间实际会受相机类型、输入分辨率和是否开启 TensorRT 影响。我一般建议从 YOLOv3-tiny 入手先跑通整个小车闭环再根据实测帧率决定是否升级到完整版 YOLOv3。跟踪目标的类别也值得提前想清楚用 COCO 预训练权重可以直接跟踪 person 类其他被检出的类别当作静态障碍如果想跟踪自定义物体比如特定颜色的球就要准备自己的数据集做微调这是另一套工作。2.2 Jetson Nano 的算力边界FP16、TensorRT 与检测频率Jetson Nano 的 GPU 支持 FP16峰值算力约 472 GFLOPS这决定了它跑 CNN 时“能用但不是用来穷算”的定位。直接把 YOLOv3 的 Darknet 权重喂给 OpenCV 的 dnn 模块看推理延迟会高得很难受常见做法是先转 ONNX再用 TensorRT 生成 FP16 engine把推理时间砍到原来的三分之一甚至更低。TensorRT 的核心是层融合和 kernel 自动调优模型越大收益越明显YOLOv3 完整版在 Nano 上尤其依赖这一步。内存是另一个硬约束。4GB 内存在加载模型、图像预处理和多个 Python 进程之间需要精打细算。检测分辨率用 320×320 时模型激活占用更小推理更快用 416×416 时精度更高但显存压力大。电源模式也要提前切成 MAXN否则 GPU 频率会被限制到很低。先把sudo nvpmodel -m 0和sudo jetson_clocks这种最基础的性能释放手段用上再谈参数调优。注意Jetson Nano 预装的是 Python 3.6TensorRT 的 Python 绑定也是针对这个版本编译的。不要手贱把系统 Python 升到 3.8否则后面编译依赖会连环翻车。2.3 从边界框到 PWM局部感知小车最短控制路径目标检测输出的边界框[x, y, w, h]本身不包含距离但它的水平位置可以直接指导转向。把目标框中心横坐标和画面中心的偏差映射到[-1, 1]再乘一个比例系数就得到了转向指令。这个思路足够简单却能让小车从“检测到目标”跨到“跟随目标”。def compute_steering(bbox, frame_width, k0.8): # bbox: [x, y, w, h]单位是像素 obj_center_x bbox[0] bbox[2] / 2.0 # 归一化误差正值表示目标在画面右侧 error (obj_center_x - frame_width / 2.0) / (frame_width / 2.0) # 比例控制输出范围限制在 [-1, 1] steering k * error steering max(-1.0, min(1.0, steering)) return steering这段代码里的k是转向灵敏度取值 0.6 到 1.0 之间比较稳妥。k太小小车转向不够目标会慢慢跑出画面k太大车头会左右震荡。后面接电机驱动时把steering拆成左右轮速度即可比如left_speed base_speed - steering、right_speed base_speed steering。避障信息则来自独立通道超声波距离触发时直接覆盖转向输出优先级高于视觉跟踪。这样控制路径最短响应也最快。3. Jetson Nano 部署与 YOLOv3 最小推理工程很多人在这一节倒下的原因不是模型训练不出来而是环境装到一半就想砸电脑。Jetson Nano 不是普通 x86 机器很多 pip 包没有预编译 wheel需要现场编译。这一章按我的习惯顺序给出最小可跑工程刷官方镜像、换源装依赖、拿到 YOLOv3 模型文件、调通推理代码。3.1 从官方镜像到 Python 环境第一次刷机就避开依赖坑使用 NVIDIA Jetson Nano Developer Kit SD Card Image 作为底包烧到 32GB 以上的 TF 卡。烧录工具用 balenaEtcher 比较省心命令行环境可以直接用 dd 写入sudo dd ifjetson-nano-jp46.img of/dev/sdX bs32M statusprogress sync这里of/dev/sdX强烈建议先用lsblk确认设备名写错盘符会把其他移动硬盘清空。刷完开机完成初始化后第一件事不是装自己的依赖而是把系统自带包更新一遍并把 pip 源换成国内镜像。Jetson Nano 的 apt 源和 pip 源都建议同时换不然在默认源下装 opencv-python 可能要等很久。sudo apt update sudo apt install -y python3-pip libopenblas-dev libjpeg-dev libfreetype6-dev pip3 install --user numpy1.19.5 opencv-python4.5.3.56这里的 numpy 版本不是随手选的。Jetson Nano 的系统盘里预装的 TensorRT 和 CUDA 库基于 Python 3.6numpy 版本太新可能导致二进制不兼容。opencv-python 4.5.3.56 也是我常用的一个比较稳的旧版本新版本在 aarch64 下有时缺依赖。装完跑一句python3 -c import cv2; print(cv2.__version__)验证出现版本号就说明基础环境通了。换源时注意只改 pip 和 apt 的软件源不要动 CUDA 相关的 .so 软链接。经常有同学把 libcuda.so 指到错误版本结果 TensorRT 启动时直接报找不到库。这类问题排查起来比装环境还费时间。3.2 Darknet 权重转 ONNX 再转 TensorRT模型文件从哪来YOLOv3 的官方权重文件通常是 Darknet 格式也就是.cfg加.weights。Jetson Nano 上有多种方式使用它纯 Python 可以用 OpenCV dnn 加载但性能一般更常见的生产路径是先把 Darknet 转成 ONNX再转成 TensorRT engine。整个转换可以不在 Nano 上完成PC 上转好再把 engine 文件拷过去Nano 只负责加载推理。python3 yolo_to_onnx.py --config yolov3-tiny.cfg --weights yolov3-tiny.weights --output yolov3-tiny.onnx /usr/src/tensorrt/bin/trtexec --onnxyolov3-tiny.onnx --saveEngineyolov3-tiny_fp16.trt --fp16trtexec是 TensorRT 自带的命令行工具--fp16开启半精度--saveEngine把优化后的引擎存成文件避免每次启动都重新做层融合和 kernel 调优。如果遇到 ONNX 中某些算子不被 TensorRT 支持通常需要把 upsample 层替换成 resize 层这也是 YOLO 模型迁移最常见的坑之一。转换结果文件会包含 TensorRT 格式的权重不要指望它能在普通 x86 机器上跑。提示在 Nano 上执行 trtexec 会比 PC 慢很多因为 kernel 自动调优要遍历算法组合。耐心等两次后面每次启动就能秒级加载。3.3 最小推理代码OpenCV 调试版和 TensorRT 部署版调试阶段我建议先用 OpenCV 的 dnn 模块把整个流程跑通因为不用管 TensorRT 的输入输出 blob 细节代码最直观。下面这段就是标准的 YOLOv3-tiny 推理骨架import cv2 import numpy as np net cv2.dnn.readNetFromDarknet(yolov3-tiny.cfg, yolov3-tiny.weights) layer_names net.getLayerNames() out_layers [layer_names[i - 1] for i in net.getUnconnectedOutLayers()] with open(coco.names, rt) as f: classes f.read().rstrip(\n).split(\n) def detect(frame): h, w frame.shape[:2] blob cv2.dnn.blobFromImage( frame, 1/255.0, (320, 320), (0, 0, 0), swapRBTrue, cropFalse ) net.setInput(blob) outs net.forward(out_layers) boxes, confs, class_ids [], [], [] for out in outs: for det in out: scores det[5:] cls_id int(np.argmax(scores)) conf float(scores[cls_id]) if conf 0.5: continue cx, cy, bw, bh det[:4] * np.array([w, h, w, h]) boxes.append([int(cx - bw/2), int(cy - bh/2), int(bw), int(bh)]) confs.append(conf) class_ids.append(cls_id) idxs cv2.dnn.NMSBoxes(boxes, confs, 0.5, 0.4) result [] for i in idxs.flatten(): result.append((classes[class_ids[i]], confs[i], boxes[i])) return result这段代码的关键点有三个。blobFromImage做了 resize 和归一化输入尺寸固定为 320×320这是速度优先的选择改成 416×416 会提升小目标召回但帧率会掉。for out in outs里对每个检测框提取类别分数时det[:4]是中心点和宽高注意它们相对于检测输入尺寸的坐标需要乘回原图尺寸。最后的NMSBoxes做非极大值抑制去掉同一目标上的重复框。OpenCV 版本的好处是能快速验证模型、类别和人眼效果但它没有用到 TensorRT 优化。正式小车运行我一般会用 TensorRT 的 Python 接口加载之前生成的 engine 文件输入输出是 GPU 内存推理完成后把检测结果拷回 CPU。如果不想手写 TensorRT 的 binding也可以直接用 jetson-inference 库的detectNet加载 ONNX 模型它内部已经封好了预处理和后处理只是对 YOLO 的适配偶尔需要改配置。两条路都能走前提是先确认 OpenCV 调试版输出的框是准的。4. 自动跟踪与避障状态机从帧间匹配到转向控制检测只是眼睛要完成自动跟踪和避障还需要大脑。单帧检测结果是不连续的这一帧检测到人下一帧可能因为遮挡漏检或者同时出现多个目标。跟踪逻辑要决定“现在跟谁”避障逻辑要决定“什么时候放弃跟随”。这一章把这两件事合并成一个状态机直接可写到小车主循环里。4.1 帧间目标匹配用 IoU 把检测结果变成稳定跟踪最简单的跟踪方法是把上一帧的目标框和当前帧所有检测框做 IoUIoU 最大的那一对认为是同一个目标。阈值通常取 0.3 到 0.5太低会把不同目标混在一起太高会在目标快速移动时跟丢。def iou(a, b): ax1, ay1, aw, ah a bx1, by1, bw, bh b x1 max(ax1, bx1) y1 max(ay1, by1) x2 min(ax1 aw, bx1 bw) y2 min(ay1 ah, by1 bh) inter max(0, x2 - x1) * max(0, y2 - y1) area_a aw * ah area_b bw * bh return inter / (area_a area_b - inter) def match_tracks(detections, prev_tracks, iou_threshold0.3): matched [] for track in prev_tracks: best_det None best_iou iou_threshold for det in detections: cur iou(track.bbox, det.bbox) if cur best_iou: best_iou cur best_det det if best_det is not None: matched.append((track, best_det)) return matched这一段逻辑跑在 Jetson Nano 上开销很低几十个框算起来远远不到 1 毫秒。用 age 字段记录目标连续未命中的帧数超过比如 10 帧就丢弃该目标同时要求目标连续命中 3 帧才认定“锁定”避免误检导致小车去追一块阴影。如果对跟踪稳定性要求更高可以引入 OpenCV 的 KalmanFilter 预测目标下一帧位置再在预测位置附近搜索候选框代价是 CPU 占用增加。4.2 转向控制PID 输出与“原地转向”边界帧间匹配确认了目标身份后要输出转向指令。只用上一章的比例控制compute_steering在目标静止时通常没问题目标一旦频繁加减速会出现转向滞后。低速小车可以接受这一点但如果你希望车头稳定对准目标中心建议上 PID。下面这个 PID 类把积分和微分都做了抗饱和处理class PID: def __init__(self, kp, ki, kd, windup10.0): self.kp kp self.ki ki self.kd kd self.integral 0.0 self.last_error 0.0 self.windup windup def update(self, error, dt): self.integral error * dt self.integral max(-self.windup, min(self.windup, self.integral)) derivative (error - self.last_error) / dt if dt 0 else 0.0 self.last_error error return self.kp * error self.ki * self.integral self.kd * derivativekp控制响应速度ki用于消除目标持续偏移造成的稳态误差kd抑制转向过度。小车底盘常见问题是左右电机转速不一致导致直线跟随时车头往一边偏这时ki就能派上用场。windup限制了积分累积上限避免目标短暂丢失后积分值异常放大恢复跟随时猛地甩头。 dt 是两次控制调用的时间间隔建议用单调时钟计算而不是循环里写死 0.02。4.3 避障状态机超声波优先于视觉跟踪局部感知小车不能只盯一个人忽略前方障碍物。常见布置是超声波测距模块装在小车正前方检测距离设 20 到 40 厘米触发避障。视觉检测这时反而要退到第二位因为超声波测距更直接也不受模型类别限制。STATE_SEARCH, STATE_FOLLOW, STATE_AVOID, STATE_STOP range(4) state STATE_SEARCH while True: distance read_ultrasonic_cm() detection detect_center_target(frame) if distance 20.0: state STATE_AVOID elif detection is None: state STATE_SEARCH elif distance 35.0: state STATE_FOLLOW if state STATE_FOLLOW: steering compute_steering(detection.bbox, frame.shape[1]) drive(steering, speed0.25) elif state STATE_AVOID: drive(steering-0.6, speed0.0) # 原地转向避开障碍 elif state STATE_SEARCH: drive(steering0.3, speed0.08) # 慢速原地搜索状态转移条件看起来简单但执行顺序决定了避让可靠度。距离小于 20 厘米时无条件进入 AVOID即使当前检测到了目标也不跟因为小车可能已经贴近障碍物继续往前会造成碰撞。只有距离恢复到 35 厘米以上才允许回到 FOLLOW。搜索状态不能给太大转向速度否则小车会原地画圈没有机会重新扫描到目标。状态机的动作可以用一个函数封装方便后续替换成串口或者 PCA9685 输出。下面这张表可以贴在看板上调车时对照状态判断现象状态进入条件动作SEARCH无目标且距离正常原地慢速旋转持续检测FOLLOW目标连续命中距离大于 35cm按 PID 转向保持车速AVOID距离小于 20cm原地转向或倒退忽略跟踪STOP距离极近或急停信号停车等待恢复4.4 几种失败模式的应急策略跟踪丢失是最常见的问题。目标转身、走到障碍物后面、光线突变都可能导致连续几帧无检测框。我一般不会立刻切到 SEARCH而是保留目标最后的运动趋势继续打转向让车头朝向预测位置最多维持一秒。如果一秒后仍然没检测到再进入搜索。这能避免目标只是短暂被挡时小车乱转。另一个值得注意的问题是目标突然从画面里跑到车头正下方检测框消失但人还在。超声波如果没触发避障小车可能会追着人脚边顶着走。解决方法是给小车设一个最小跟随距离当检测框底边接近画面底部且面积超过阈值时视为目标过近先停车而不是继续前进。这种“视觉距离 主动测距”的双保险比单一方案可靠很多。5. 帧率、阈值与稳定性YOLOv3 小车必调的参数和必须躲开的坑检测模型能出框、状态机能跑不代表小车能稳定运行。Jetson Nano 没有高性能台式机的余量必须在帧率、精度和功耗之间做取舍。这一章给出我常用的参数起点以及最常见的三类故障排查路径。5.1 帧率上不去的三个瓶颈输入分辨率、后处理、锁频模式帧率低先不要怀疑模型太大按这个顺序查第一检测输入分辨率是不是超过了 416×416第二OpenCV 后处理里有没有做全量for循环、把每个框都画成带圆角或中文标注第三电源模式是否还停留在省电档。Nano 默认可能跑在 5W 模式GPU 频率被限制换到 MAXN 模式就能立刻缓解。sudo nvpmodel -m 0 sudo jetson_clocks sudo tegrastatsnvpmodel -m 0将设备切到性能模式jetson_clocks固定 CPU/GPU 频率避免频率波动导致检测延迟忽高忽低。tegrastats持续输出 CPU、GPU 和内存占用用来观察哪一项接近饱和。如果 RAM 占满或 GPU 使用率接近 99%就说明当前模型和分辨率确实超出 Nano 的能力了。5.2 阈值参数表置信度、NMS、检测分辨率与距离映射参数值不是一个固定数而是和场景强相关。小车跟随目标时建议置信度设 0.5太高容易漏检太低会出现大量误检。NMS IoU 设 0.4 以上能减少同一目标的重复框但如果两个目标挨得很近过高的 NMS 会合并成一个大框。下面是我在室内地面场景的起点参数推荐值说明检测输入分辨率320 × 320追求帧率目标较大时可用 416置信度阈值0.5调高到 0.7 可过滤远距离误检NMS IoU 阈值0.4两个目标贴近时降到 0.3检测调用频率10 Hz控制频率 50 Hz检测不必每帧跑跟随目标类别person 或自定义单类多类别会增加后处理开销避障距离阈值20 cm视小车减速能力和电机响应调整检测调用频率和控制频率分离是很多人忽略的手段。YOLOv3-tiny 在 Nano 上能跑 20 FPS但小车轮速控制和超声波采样不需要这么高的视觉帧率。让检测线程以 10 Hz 输出结果控制线程以 50 Hz 读取最近一次检测框并更新 PWM这样即使某帧推理超时控制循环也不会卡顿。5.3 线程化调用检测 10Hz、控制 50Hz 的最小骨架多线程在 Python 里要注意 GIL 的影响但 IO 和模型推理通常会释放 GIL控制线程的实时性可以接受。在这里给出一个轻量实现检测线程负责采集图像和推理主线程负责控制和状态机。import threading import time class DetectorThread(threading.Thread): def __init__(self, detect_fn): super().__init__(daemonTrue) self.detect_fn detect_fn self.lock threading.Lock() self.result ([], 0.0) self.running True def run(self): while self.running: frame capture() detections self.detect_fn(frame) with self.lock: self.result (detections, time.monotonic()) def latest(self): with self.lock: return self.result detector DetectorThread(detect) detector.start() last_control 0.0 while True: now time.monotonic() if now - last_control 0.02: # 50Hz 控制 detections, ts detector.latest() state_machine.update(detections, read_ultrasonic_cm()) last_control now这段骨架的关键是lock因为检测结果会在两个线程间共享不加锁可能读到写了一半的列表。控制频率固定 0.02 秒一次超声波测距也放在控制循环里省去额外的中断处理。如果检测线程跑得慢latest()拿到的还是上一帧结果反应会慢半拍但不至于崩。5.4 过热、掉电与“不输出检测框”的排查Jetson Nano 满载时发热明显如果没有主动散热GPU 会降频到比省电模式还低表现为帧率越来越慢。用散热片加风扇是最直接的方案软件层可以加温度检查超过温度阈值就降低检测分辨率。掉电问题更隐蔽常见于使用普通 5V 电源适配器供电的情况。Nano 最大功耗约 10W电机驱动和舵机又抢占电流瞬间电压跌落直接重启。要解决这个问题常见做法是给逻辑板用独立 5V 电源电机用单独电池组共地但不共享电流。排查时先用dmesg | tail -50看有没有电压报警信息。模型不出检测框时先打印原始检测输出而不是直接看画框后的图。常见原因是置信度阈值太高、输入 blob 归一化参数不对、或者类别 ID 与训练标签对不上。YOLOv3 的 COCO 权重中 person 是第 0 类如果你的工程里写了 1就会一直搜到自行车上去。这类错误单独看代码很难发现用一个已知视频逐帧回放才最容易定位。6. 用日志和回放验证跟踪效果再往 TensorRT、Orin Nano 迁移小车能跟人跑了不能靠肉眼说“看起来不错”。把跟踪效果量化成可比较的指标才是调参和后续优化的依据。在 Jetson Nano 上加日志很简单关键是字段设计要够用。import csv from datetime import datetime def log_record(path, ts, state, track_id, cx, cy, w, h, distance): with open(path, a, newline) as f: writer csv.writer(f) writer.writerow([ts, state, track_id, cx, cy, w, h, distance])记录时间、状态、目标 ID、中心坐标、框尺寸和超声波距离。跑几次跟随实验后用 Python 读 CSV可以统计目标被连续跟踪的时长、跟踪中断次数、跟随时长占有效时长的比例。中断次数多意味着帧间匹配阈值或控制器参数有问题跟踪时长占比高说明锁定稳定。配合同步录制的视频把日志按时间戳对齐能逐帧定位是在哪个瞬间跟丢的。这一步是后面所有优化策略的参照基准。ffmpeg -i test.mp4 -vf fps10 frame_%04d.jpg将视频抽帧后与日志中的cx, cy比对确认框坐标是否确实覆盖目标。这里的抽帧频率设为 10Hz与检测频率一致便于一帧一帧核对。把 OpenCV 调试版换成 TensorRT engine 后同样的视频文件分别跑 FP16 和 INT8记录每一帧的延迟和检出数。对比表可以这样建立推理模式engine 大小平均单帧延迟检出目标数是否有误检FP16约 20MB约 40ms86无INT8约 11MB约 25ms792 次如果 INT8 掉检不严重就用 INT8掉检明显就留在 FP16。这个指标不一定追求绝对精度而是看小车跟随实验的“目标连续跟踪时间”是否下降。跟踪连续性比单帧 mAP 更能反映最终体验。之后的迁移方向有两条。一条是换更合适的小模型YOLOv4-tiny 在 Nano 上同样能跑转换工具链和 YOLOv3 基本一致另一条是换 Jetson Orin Nano模型转换流程、Python 控制代码和状态机都不用重写只需要对相机参数做重新标定再把 TensorRT engine 按新设备重新生成一次。调好的日志回放脚本也要保留换硬件后它能直接对比新旧平台的跟踪性能差异。最后把 jetson_clocks 的调用写进开机自启避免每次冷启动都反复设一次。本文还有配套的精品资源点击获取