ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLOv3与Jetson Nano的局部感知小车:TensorRT加速与跟踪避障实践

基于YOLOv3与Jetson Nano的局部感知小车:TensorRT加速与跟踪避障实践 简介基于YOLOv3与Jetson Nano平台的局部感知小车项目核心功能包括自动目标跟踪与实时避障适合计算机视觉、嵌入式AI及机器人方向的学生、开发者也适用于毕业设计、课程设计或初期项目立项演示。资源共18个文件以12个Python源码文件为主同时提供YOLOv3网络配置文件、coco类别文件、说明文档和示例图片压缩包整体仅94KB代码轻量但完整。源码中集成了YOLOv3目标检测、SiamRPN跟踪、摄像头实时检测与跟踪等多个可直接运行的demo脚本并附带依赖清单与README说明便于快速搭建环境并验证功能。对于希望结合深度学习与边缘计算完成小车感知的学习者这套代码提供了清晰的模块划分和二次开发接口可用于扩展路径规划或更复杂的避障策略。目前已有284名学习者浏览关注项目来自个人毕业设计代码经过运行验证可直接用于学习与二次开发。1. 从一块板卡到一台会自己走路的小车jetson nano 上跑 YOLOv3 并不是什么新鲜事官方镜像装好之后跑个 demo 检测几张图片也很简单。但这个项目真正棘手的地方在于检测只是一个环节小车要实时完成“看到目标 → 算出位置 → 控制电机 → 跟踪或绕开”的完整闭环。检测延迟 200ms 可能只是界面卡顿放在小车上就是冲出桌角。我见过太多人把时间花在调 YOLO 精度上结果装到小车上根本跑不动或者跑得动但电机响应跟不上。这篇文章围绕“基于 YOLOv3 和 jetson nano 的局部感知小车”来讲一套可落地的方案包含硬件选型、TensorRT 加速推理、Python 实现的跟踪和避障逻辑以及现场调试时最常踩的坑。所谓“局部感知”是指小车不依赖全局地图或 SLAM仅靠车载摄像头和测距传感器实时感知局部环境并做出决策。适合打算做嵌入式 AI 项目、毕业设计或者想在 jetson nano 上跑通视觉控制闭环的开发者。我会尽量让每个步骤都能直接复现代码片段也可以直接抄进你的工程里。2. 局部感知小车的架构设计与硬件选型2.1 为什么是 jetson nano 而不是树莓派或普通 PC做移动机器人控制板的选择决定了整个项目的性能上限和开发方式。树莓派 4B 的 CPU 不错但它的 GPU 不是为 CUDA 设计的跑 YOLOv3 全靠 CPU 推理320×320 输入下勉强 2–3 FPS根本没法做连续跟踪。普通 PC 性能没问题但体积、功耗和供电都不适合小车。jetson nano 刚好在中间128 核 Maxwell GPU 4GB 内存 5W/10W 可调功耗并且原生支持 CUDA、cuDNN、TensorRT这是它作为边缘 AI 设备的根本优势。我一般建议选jetson nano 4GB 版本B01 或 2GB 都可以但 4GB 更稳。原因很简单YOLOv3 的权重和中间特征图都需要显存TensorRT 转换后的引擎文件大约 60–240MB2GB 内存会比较紧张而且同时跑摄像头采集、Python 解释器和电机控制会产生内存抖动直接导致帧率不稳定。项目对实时性要求高多花一点钱买 4GB 版本值得。2.2 感知方案对比单目视觉 超声波是性价比解局部感知小车的传感器方案有几种常见选择我在不同项目里都试过列个对比表方案优势劣势适用场景单目摄像头 YOLOv3目标识别能力强、可跟踪特定物体无深度信息、受光照影响本项目首选双目摄像头可计算深度、避障精度高标定复杂、jetson nano 算力开销大需要精确测距时单目 超声波超声波补足近距离测距、成本低只能测一个方向、探测范围窄本项目组合方案LiDAR360° 测距、建图效果好贵、对 nano 来说数据量偏大SLAM 类项目我在这个项目里选用单目 USB 摄像头 HC-SR04 超声波模块是一种很务实的组合。YOLOv3 负责“看”到目标并输出目标框超声波负责“感受”近距离障碍物。有人会问既然 YOLO 已经能识别障碍物比如椅子、墙壁为什么还要超声波因为单目没有深度信息一个 100 像素宽的目标可能是 0.5 米外的猫也可能是 5 米外的汽车YOLO 算不出真实距离。超声波在 0.02–2 米范围内能直接返回厘米级距离用来做急停和低速避障非常可靠。视觉和测距互补这才是“局部感知”的完整含义。2.3 硬件接线与系统环境准备jetson nano 的 40-pin GPIO 引脚与树莓派兼容超声波模块的接线很直接我用 GPIO 12Board 编码作为 TrigGPIO 16 作为 Echo。注意 HC-SR04 是 5V 供电但 Echo 引脚返回也是 5Vjetson nano 的 GPIO 是 3.3V 逻辑必须加分压电阻两个 1kΩ 和 2kΩ 分压否则可能烧坏 GPIO。我见过有人直接接上去冒烟的。电机驱动用 L298N 或 TB6612FNG我的建议是 TB6612FNG它比 L298N 发热小、体积小适合小车。接线表大致如下模块引脚jetson nano GPIO (Board)超声波 Trig输入GPIO 12超声波 Echo输出GPIO 16经分压电机驱动 PWMA左轮速度GPIO 32电机驱动 AIN1/AIN2左轮方向GPIO 18 / GPIO 22电机驱动 PWMB右轮速度GPIO 33电机驱动 BIN1/BIN2右轮方向GPIO 15 / GPIO 13系统环境方面jetson nano 官方推荐用JetPack 4.6它自带 Ubuntu 18.04、CUDA 10.2、cuDNN 8.2 和 TensorRT 8.2。你不需要手动装 CUDA烧录完系统之后用jetson-zone或nvidia-smi确认环境。PyTorch 用官方预编译的 wheel 包安装Python 建议直接用系统自带的 3.6.9——不要折腾升级到 3.8很多 jetson 的编译包只针对 3.6。开发时我在 PC 上用 VSCode 远程连到 nano代码同步方便。# 检查系统环境 sudo apt update sudo apt install python3-pip python3 --version # 确认 Python 3.6.9 nvcc --version # 确认 CUDA 10.2 ls /usr/lib/aarch64-linux-gnu/libnvinfer.so.8 # 确认 TensorRT 8这些命令会从上到下确认环境完整。大部分时候你烧录完 ImageCUDA 和 TensorRT 就在了。真正要装的是 Python 的numpy、opencv-python、Pillow以及后续会用到的onnx。3. 在 jetson nano 上部署 YOLOv3 并完成 TensorRT 加速3.1 为什么必须做 TensorRT 转换而不用原始 DarknetYOLOv3 原生 Darknet 框架在 jetson nano 上的推理速度大约是 5–8 FPS416×416 输入、FP32这样的帧率用来做静态检测还可以但小车运动时目标在画面中连续移动低于 10 FPS 会导致跟踪丢失和电机控制抖动。TensorRT 是 NVIDIA 的推理优化引擎能把训练好的模型转换成针对具体 GPU 优化的计算图融合层、裁剪精度、选择最优 kernel。实测下来 YOLOv3 的 FP16 引擎在 nano 上能跑到 15–20 FPSINT8 引擎能到 25–30 FPS提升非常明显。转换流程的常见做法是“Darknet 权重 → ONNX → TensorRT 引擎”。不直接从 Darknet 转 TensorRT是因为 Darknet 的权重格式不是 TensorRT 原生支持的而 ONNX 是中间桥梁。jetson nano 上安装onnx很简单pip3 install onnx1.9.0版本要匹配TensorRT 8.2 对应的 ONNX 解析器支持到 opset 11 左右太新的 ONNX 版本会报不兼容错误。我在项目里反复踩过这个坑最后锁定了 1.9.0稳妥。3.2 从 YOLOv3 权重到 ONNX 的转换脚本网上有很多 YOLOv3 转 ONNX 的脚本但不少是给 PC 写的对 nano 上的环境不一定兼容。我一般自己写一个最小转换脚本思路是加载 Darknet 网络结构然后逐层输出为 ONNX 格式。下面是一个简化但可运行的版本# convert_yolov3_onnx.py # 依赖: pip3 install onnx1.9.0 numpy import torch import onnx # 这里借助 PyTorch 的 Darknet 实现来加载权重 # 如果你的环境里没有 PyTorch可以换用 opencv dnn 方式导出 from models import Darknet def main(): # cfg 文件指定网络结构weights 是 darknet 预训练权重 model Darknet(cfg/yolov3.cfg) model.load_weights(weights/yolov3.weights) model.eval() # 构造一个 416x416 的虚拟输入导出 ONNX dummy_input torch.randn(1, 3, 416, 416) torch.onnx.export( model, dummy_input, yolov3.onnx, opset_version11, input_names[input], output_names[boxes, confs, probs] ) print(ONNX 转换完成) if __name__ __main__: main()这段代码最关键的是两个参数opset_version11对应 TensorRT 8.2 能接受的 ONNX 算子集上限output_names需要与 YOLOv3 的检测头输出结构对应YOLOv3 有三个检测头分别负责小、中、大目标输出是三个尺度的特征图。注意我在 Nano 上通常只用 416×416 输入比 608×608 少了近一半的计算量检测精度损失在 1–2% 以内但帧率提升 30% 以上。如果你发现转换后的 ONNX 里有不支持的算子比如某些 PyTorch 版本导出的aten::index需要用 ONNX-Simplifier 化简一下pip3 install onnx-simplifier python3 -m onnxsim yolov3.onnx yolov3_sim.onnxonnx-simplifier会把计算图中的冗余节点消除掉这在 nano 上是一个很实用的预处理步骤。3.3 用 TensorRT 构建 FP16 推理引擎拿到 ONNX 文件后下一步是用 TensorRT 的 Python API 把它构建成.engine文件。TensorRT 的构建过程可以在 nano 上直接跑也可以在 PC 上跑完拷贝过去。我通常直接在 nano 上构建因为构建过程和运行环境完全一致避免 PC 上 GPU 架构不同而导致的兼容问题。以下代码会生成一个 FP16 引擎# build_engine.py # 依赖: tensorrt (JetPack 自带的 python3-libnvinfer) import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit TRT_LOGGER trt.Logger(trt.Logger.WARNING) def build_engine(onnx_path, engine_path): builder trt.Builder(TRT_LOGGER) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, TRT_LOGGER) with open(onnx_path, rb) as f: assert parser.parse(f.read()), ONNX 解析失败 config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) # 1GB 工作空间 config.set_flag(trt.BuilderFlag.FP16) # 动态 batch 需要设置 profile这里固定 batch1省去复杂配置 engine builder.build_serialized_network(network, config) with open(engine_path, wb) as f: f.write(engine) print(f引擎已保存至: {engine_path}) build_engine(yolov3_sim.onnx, yolov3_fp16.engine)工作空间大小注意一下130是 1GBjetson nano 共享内存只有 4GB太大可能 OOM太小转换会失败。FP16 标志开启后TensorRT 会自动把支持的层转换成半精度计算其他层保持 FP32这个混合精度策略是速度提升的主要来源。如果你的模型转换后检测效果变差比如目标框偏移可能是某些层对精度敏感可以改回 FP32 建一个对比引擎或者对个别层禁用 FP16。3.4 Python 侧的 TensorRT 推理封装引擎文件构建好之后需要一个 Python 类来加载引擎并执行推理。我提供一个常用的封装它处理了输入预处理、输出后处理和 NMS# trt_yolov3.py import numpy as np import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit class TRTYOLOv3: def __init__(self, engine_path, input_size416, conf_thres0.5, iou_thres0.4): self.input_size input_size self.conf_thres conf_thres self.iou_thres iou_thres runtime trt.Runtime(trt.Logger(trt.Logger.WARNING)) with open(engine_path, rb) as f: self.engine runtime.deserialize_cuda_engine(f.read()) self.context self.engine.create_execution_context() self.stream cuda.Stream() self._allocate_buffers() def _allocate_buffers(self): # 绑定输入输出缓冲区TensorRT 需要固定的 GPU 显存地址 self.h_input cuda.pagelocked_empty(1 * 3 * self.input_size * self.input_size, dtypenp.float32) self.h_outputs [cuda.pagelocked_empty(size, dtypenp.float32) for size in self.engine.max_batch_size] self.d_input, self.d_outputs cuda.mem_alloc(self.h_input.nbytes), [cuda.mem_alloc(h.nbytes) for h in self.h_outputs] def __call__(self, img): # img 是已经 resize 到 416x416 的 RGB ndarray0-1 范围 self.h_input[:] img.ravel() cuda.memcpy_htod_async(self.d_input, self.h_input, self.stream) self.context.execute_async_v2(bindings[int(self.d_input)] [int(d) for d in self.d_outputs], stream_handleself.stream.handle) cuda.memcpy_dtoh_async(self.h_outputs[0], self.d_outputs[0], self.stream) self.stream.synchronize() # 后续对输出做 sigmoid、解码和 NMS return self._postprocess(self.h_outputs[0])这里需要注意YOLOv3 的原始输出不是直接可用的框坐标需要做 sigmoid 激活、坐标解码、按 confidence 过滤、然后用 Non-Maximum Suppression 去除重叠框。我在_postprocess里用纯 NumPy 实现这些操作因为 nano 上 NumPy 的效率比 Python 循环高很多而且不需要额外依赖。让我给你一张精度和速度对比表方便选择用哪个引擎引擎模式权重文件大小推理帧率 (416×416)检测精度 (mAP 相对损失)适用场景原始 Darknet (FP32)236MB6 FPS基准不推荐仅验证TensorRT FP16118MB18 FPS约 1-2%推荐日常使用TensorRT INT859MB28 FPS约 4-6%需要高帧率、光照稳定时边框数量多、遮挡严重的场景用 FP16光照变化小、高速移动需要的场景用 INT8。我在现场调试时通常先 FP16确认逻辑没问题再切 INT8 看效果。4. 自动跟踪和避障的 Python 实现4.1 控制闭环的整体逻辑检测-决策-驱动YOLOv3 推理只是“看”的过程真正的难点在于把视觉结果转成电机控制信号。我设计了一个四步循环每个循环周期约 50ms20 FPS 的情况下第一步 采集从摄像头读取一帧图像resize 到 416×416 送入 TensorRT第二步 定位拿到目标类别、置信度和边界框计算目标的中心点坐标归一化的 u, v第三步 决策根据目标位置计算偏差 e偏差输入 PID 控制器得到左右轮速差第四步 驱动PWM 信号驱动 TB6612FNG 控制电机同时读取超声波距离小于阈值时强制减速或停车这个闭环最核心的思想是“目标偏离画面中心 → 计算出航向偏差 → 调整两侧轮速 → 目标回到中心”。避障则是一个并行逻辑超声波检测到前方障碍物时打断跟踪执行一个预设的绕行动作比如后退左转然后恢复搜索。4.2 目标跟踪基于质心的 PID 控制跟踪的输入是 YOLOv3 输出的检测框。我挑选置信度最高的那个框作为当前跟踪目标计算框中心在画面中的水平坐标然后和画面中心416/2 208比较得到误差 e。这个 e 被送到一个 PID 控制器# pid_controller.py class PID: def __init__(self, kp0.6, ki0.01, kd0.05, integral_limit100): self.kp kp self.ki ki self.kd kd self.integral 0.0 self.prev_error 0.0 self.integral_limit integral_limit def reset(self): self.integral 0.0 self.prev_error 0.0 def update(self, error, dt0.05): # dt 是控制周期按实际帧间隔传入秒 self.integral error * dt # 积分限幅防止长时间偏差累积导致输出饱和 self.integral max(-self.integral_limit, min(self.integral_limit, self.integral)) derivative (error - self.prev_error) / dt if dt 0 else 0.0 self.prev_error error return self.kp * error self.ki * self.integral self.kd * derivative # 回到主控制循环 pid PID(kp0.6, ki0.01, kd0.05) # 假设检测到目标框 center(cx, cy) error (cx - 208) / 208.0 # 归一化到 -1 到 1 steer pid.update(error) # steer 范围约 -1 到 1 # 左轮 base_speed - steer * max_turn, 右轮 base_speed steer * max_turn left_speed int(45 - steer * 30) right_speed int(45 steer * 30)PID 参数的选取和现场调法我一般先用纯比例kp0.5ki0kd0如果小车左右摇摆就降kp如果跟踪跟不上就升kp。然后加入积分消除稳态误差最后加入微分抑制超调。上面代码里把误差归一化到 -1~1这样大小和速度变化范围解耦换电池或者改基础速度后不用重新调 PID。基线轮速45是 0–100 的 PWM 占空比映射值实际要根据电机和地面摩擦调整过小走不动过大响应来不及避障。4.3 避障逻辑距离阈值与状态机避障逻辑不该是简单的 if-else 开关。我把它实现成一个三状态状态机更合理些TRACKING跟踪超声波距离 40cm正常跟踪目标AVOID避障超声波距离 ≤ 40cm停车并右转绕行持续 500msSEARCH搜索目标丢失超过 1.5 秒原地左转扫描寻找目标# avoid_logic.py import time class AvoidState: TRACKING 0 AVOIDING 1 SEARCHING 2 class AvoidController: def __init__(self, motor, ultrasonic, threshold40.0): self.motor motor self.ultrasonic ultrasonic self.threshold threshold self.state AvoidState.TRACKING self.state_since time.time() def update(self, target_visible): dist self.ultrasonic.read_cm() if self.state AvoidState.TRACKING: if dist self.threshold: self.state AvoidState.AVOIDING self.state_since time.time() self.motor.brake() elif not target_visible: self.state AvoidState.SEARCHING self.state_since time.time() elif self.state AvoidState.AVOIDING: # 绕行 0.5 秒后回到跟踪 if time.time() - self.state_since 0.5: self.state AvoidState.TRACKING self.state_since time.time() else: # 右转绕行 self.motor.set_speed(left55, right-55) elif self.state AvoidState.SEARCHING: self.motor.set_speed(left-35, right35) # 左转寻找 if time.time() - self.state_since 1.5: self.state AvoidState.TRACKING self.state_since time.time() return self.state这个状态机的好处是避免“目标丢失后直接原地乱转”或者“障碍物过了还一直绕”的问题。阈值 40cm 是怎么定的跟小车的最高速度和刹车距离有关。小车最大速度约 0.5m/s制动时间约 0.3 秒加上超声波采样周期 20ms安全距离的下限是 0.5×0.3×100 15cm。我留出 2 倍余量取 40cm这样即使在普通家居地面轮胎打滑也来得及刹停。如果场地比较空旷且速度更快把这个值加到 50–60cm 更安全。4.4 超声波测距的 Python 驱动超声波在 jetson nano 上按 GPIO 时序读取很容易写一个简单的测距函数。但注意它的采样周期较长约 20ms在主循环里同步调用会阻塞视觉处理所以我把它放到一个独立线程里用带锁的共享变量保存最新距离# ultrasonic.py import Jetson.GPIO as GPIO import time import threading class Ultrasonic: def __init__(self, trig, echo): self.trig trig self.echo echo self._distance 999.0 GPIO.setmode(GPIO.BOARD) GPIO.setup(self.trig, GPIO.OUT) GPIO.setup(self.echo, GPIO.IN) self._running True threading.Thread(targetself._measure_loop, daemonTrue).start() def _measure_loop(self): while self._running: GPIO.output(self.trig, True) time.sleep(0.00001) # 10us 高电平触发 GPIO.output(self.trig, False) while GPIO.input(self.echo) 0: pulse_start time.time() while GPIO.input(self.echo) 1: pulse_end time.time() duration pulse_end - pulse_start dist duration * 34300 / 2 self._distance dist time.sleep(0.02) # 50Hz 采样频率 def read_cm(self): return self._distance def stop(self): self._running False GPIO.cleanup()这里注意超声波模块返回的是声波往返时间所以除以 2 才是单程距离再乘以声速 34300 cm/s。实际使用中HC-SR04 在 10cm 以内测距不稳定在 2 米以上精度下降所以它的有效范围恰好覆盖了避障需求。用独立线程读取的另一个好处是即使超声波卡住Echo 一直为高也不会阻塞主循环的跟踪逻辑。4.5 主循环整合让三件事并行工作最后把摄像头读取、YOLOv3 推理、PID 控制三件事组装到主循环里注意维护一个稳定的循环周期# main.py import cv2 import numpy as np from trt_yolov3 import TRTYOLOv3 from pid_controller import PID from avoid_logic import AvoidController, AvoidState from ultrasonic import Ultrasonic engine TRTYOLOv3(yolov3_fp16.engine) pid PID(kp0.6, ki0.01, kd0.05) us Ultrasonic(trig12, echo16) motor Motor() # 电机驱动封装按你的接线实现 avoid AvoidController(motor, us) cap cv2.VideoCapture(0) # 默认 USB 摄像头 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame cap.read() if not ret: break # 预处理BGR-RGB、resize、归一化、CHW img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img cv2.resize(img, (416, 416)) img img.astype(np.float32) / 255.0 img np.transpose(img, (2, 0, 1)) img np.expand_dims(img, axis0) # YOLOv3 推理 dets engine(img) # dets 是 filter NMS 后的结果: [x1,y1,x2,y2,conf,class_id] target_visible False if len(dets) 0: best dets[0] # 按 conf 排序后取最高 x1, y1, x2, y2 best[:4] cx (x1 x2) / 2 error (cx - 208) / 208.0 steer pid.update(error) * max(0.0, best[4] - 0.5) # 置信度低时减弱控制幅度 motor.set_speed(left45 - steer*30, right45 steer*30) target_visible True else: pid.reset() # 避障状态机内部会判断距离阈值 avoid.update(target_visible) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break主循环里有个细节值得你注意steer pid.update(error) * max(0.0, best[4] - 0.5)这个乘数的作用是当置信度低接近 0.5 阈值时控制幅度自动变小避免误检导致小车猛打方向。当置信度等于 0.5 时系数为 0等于 1 时系数为 0.5这是一种“软制动”。我调试中发现单纯用硬阈值过滤会有很多边缘情况软系数让小车在“不确定”时跑得慢、转向轻比突然转向稳定得多。5. TensorRT 引擎的现场调参与复现验证5.1 先用离线视频复现别直接上车跑小车开着跑很容易出事故而且一旦失控你很难判断是检测的问题还是控制的问题。我在调试任何视觉小车时都会先让程序消费一段预先录好的视频文件把 YOLOv3 的检测框和 PID 输出画在回放画面上# 用本地视频文件代替相机输入来复现推理 gst_str filesrc locationtest.h264 ! h264parse ! omxh264dec ! nvvidconv ! video/x-raw, formatBGRx ! videoconvert ! video/x-raw, formatBGR ! appsink cap cv2.VideoCapture(gst_str, cv2.CAP_GSTREAMER)这段代码用 GStreamer 管道读取 H.264 视频文件omxh264dec是 jetson nano 的硬件解码器解码性能远好于 CPU。录制这段测试视频时你可以手持摄像头模拟小车视角走动或靠近障碍物然后在 PC 上离线分析每一帧的输出。如果发现检测框抖动先排查是否为单帧误检如果是 PID 输出振荡就把目标中心画成轨迹点直观看到振荡周期和幅度然后按比例降低kp。5.2 帧率瓶颈的三个排查层级实测中帧率不达标是常事我用一套固定排查顺序从最常用到最冷门层级检查项定位方法常用修复1输入分辨率过大416→320 对比帧率降低到 320 或 2882NMS 解码耗时对_postprocess打点计时改用向量化 NumPy 操作3GPU 显存不足sudo tegrastats观察切换 FP16 引擎、降低 batchtegrastats是 jetson 上实时显示 CPU/GPU 占用、内存/CV 温度的工具训练或调试时我基本一直开着它。YOLOv3 的 NMS 部分在 Python 里如果写成多重循环遍历每个类别每个框极其耗时必须用 NumPy 矩阵操作向量化。下面是一个相对高效的版本def nms_fast(boxes, scores, iou_thres0.4): x1 boxes[:, 0]; y1 boxes[:, 1]; x2 boxes[:, 2]; y2 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:]]) w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) inter w * h iou inter / (areas[i] areas[order[1:]] - inter) order order[1:][iou iou_thres] return keep这段代码是典型向量化写法每次选出分数最高的框一次性计算它与剩余所有框的 IOU用布尔掩码过滤。没有 for 循环嵌套在 nano 上 100 个候选框的处理时间在 1ms 以内。5.3 YOLOv3 的类别筛选只留你关心的目标YOLOv3 有 80 个 COCO 类别但如果小车的目标是人或者某个特定物体没必要在 NMS 时处理全部 80 类。一个简单而有效的优化是只保留你需要的类别 ID# 在主循环推理后加一行过滤 allowed_classes {0} # COCO 中 0 是 person dets dets[np.isin(dets[:, 5], list(allowed_classes))]在 80 类中只保留 personNMS 的计算量会直接下降一个数量级帧率也相应提升。但需要注意如果避障依赖 YOLO 识别障碍物比如椅子、沙发要把这些类别 ID 也加进集合里。我一般会把避障职责完全交给超声波YOLO 只负责跟踪目标这样两者的分工最清晰调参时互不干扰。5.4 检查 PID 控制方向的最后一道关小车跟踪效果不佳最常见的不是参数问题而是控制方向反了——目标在画面偏左小车却向右转。这种问题很隐蔽因为 YOLOv3 检测是正常的PID 参数也合理但小车越追越偏。我建议在代码里加一个显式的方向开关变量方便在现场快速翻转CONTROL_DIRECTION 1 # 设为 -1 可以立即反向 steer pid.update(error) * CONTROL_DIRECTION当发现小车往目标的反方向转时只需把CONTROL_DIRECTION改成-1不需要改动任何其他逻辑。为什么会出现方向反因为摄像头的安装方向面对车头还是车尾不同画面坐标和电机转向的映射关系就会反转。在 USB 摄像头没有固定安装规范的项目里这个开关几乎是必然要用的。我通常会把这个值放到一个config.py文件里和其他可调参数基础速度、PID 初值、避障阈值放在一起。测试时一次改一个参数不要同时动多个这是最基本的调参纪律。落地到这一步“基于 YOLOv3 和 jetson nano 的局部感知小车”的核心部分就全部打通了——从硬件接线、模型转换、Python 推理框架到跟踪控制、避障状态机和现场调参方法。把上面这些代码串起来配合一份正常的 JetPack 4.6 系统镜像你就能获得一辆具备目标识别和避障能力的小车原型。剩余的工作就是反复实跑、观察 PID 响应、调整阈值参数让它在你的场地上越跑越稳。本文还有配套的精品资源点击获取
返回列表