十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

树莓派智能小车:从避障到网球追踪的完整实现

树莓派智能小车:从避障到网球追踪的完整实现 简介基于树莓派的智能小车项目完整整合自动避障、实时图像传输、视觉车道循迹、目标检测与网球追踪五大功能适合作为毕设、课程设计、大作业或工程实训课题。项目按功能模块组织代码与文档配套齐全面向嵌入式视觉与机器人控制方向的小白及进阶学习者。资源共98个文件包含18个程序源码文件车控、摄像头、PID调速、目标检测、追踪等独立模块34个标注文件与35张训练图像用于网球检测数据集另有4个演示动画展示避障、循迹、目标检测效果附带说明文档与配置文件压缩包仅5.68MB轻量易下载。已有355人学习使用。从中可获取完整的树莓派小车实现方案超声波与红外避障逻辑、视频流电脑端实时接收、视觉车道线提取、目标检测与网球追踪算法均有对应代码同时项目保留旧版本备份便于对照学习适合在此基础上快速跑通并二次开发。1. 树莓派小车上同时跑避障、图像传输和网球追踪树莓派做智能小车最容易低估的是负载。底盘通电能转、避障能拐弯这些都花不了多少时间真正麻烦的是同一块板子还要边看摄像头、边跑目标检测、边把画面送给浏览器。帧率一低很多人第一反应是换更快的主控但树莓派4B的CPU和USB带宽放在那里真正的问题往往是任务之间互相抢资源而不是某个传感器不行。这篇文章按“树莓派4B/5 摄像头 超声波 双轮驱动”的常见方案把自动避障、实时图像传输、目标检测、网球追踪这四件事按依赖顺序串起来先解决底盘引脚和PWM波输出再解决测距决策接着解决视频流最后叠加模型推理和颜色追踪。方向是给工创赛智能小车、课程设计以及第一次做多传感器协同的人一条能直接落地的路子。2. 底盘与树莓派4B引脚先把PWM波输出调明白2.1 电机驱动选型L298N还是DRV8833市面上常见的树莓派小车底盘有两种驱动方案L298N 和 DRV8833。L298N 便宜、耐操6~12V 供电都能工作但内部三极管压降大在电池电压偏低时电机会明显变软DRV8833 体积小、压降低适合 7.2V 或 11.1V 锂电池而且带电流限制不会因为堵转直接烧驱动。如果你的小车还要后期加机械臂、电磁铁这类负载L298N 的扩展余量更大如果只做避障和摄像头追踪DRV8833 足够。接线上有一个原则电机电源和树莓派电源必须分开。电机启动瞬间电流可达 1~2A如果和树莓派共用一路 5V轻则电压跌落导致重启重则烧掉电源芯片。常见做法是电池正极进驱动板电机供电端另用降压模块单独给树莓派供电共地但不共电。树莓派 GPIO 只输出 PWM 和方向信号不要把电机的功率电流从 GPIO 引脚上走。2.2 用 gpiozero 输出 PWM 波调速树莓派的 PWM 波输出分为硬件 PWM 和软件 PWM 两类。4B 和 5 的引脚功能图里硬件 PWM 只有少数几个引脚可用BCM 12/18 等而 Python 的 gpiozero 库默认用软件 PWM几乎任意 GPIO 都能输出。软件 PWM 在低频率下会占用一点 CPU但对小车电机这种 1kHz 左右的控制信号完全够用。from gpiozero import PWMOutputDevice from gpiozero import DigitalOutputDevice import time # 左轮: 方向IN1/IN2, 速度PWM IN1 DigitalOutputDevice(17) IN2 DigitalOutputDevice(27) PWM_LEFT PWMOutputDevice(18, frequency1000) # 右轮: 方向IN3/IN4, 速度PWM IN3 DigitalOutputDevice(22) IN4 DigitalOutputDevice(23) PWM_RIGHT PWMOutputDevice(24, frequency1000) def set_motor(left_speed, right_speed): # speed 范围 -1.0 ~ 1.0负值反转 if left_speed 0: IN1.on(); IN2.off() PWM_LEFT.value left_speed else: IN1.off(); IN2.on() PWM_LEFT.value -left_speed if right_speed 0: IN3.on(); IN4.off() PWM_RIGHT.value right_speed else: IN3.off(); IN4.on() PWM_RIGHT.value -right_speed # 测试左轮半速前进右轮全速前进 车向左偏 set_motor(0.5, 1.0) time.sleep(2) set_motor(0, 0)PWMOutputDevice的frequency1000表示 1kHz PWM 频率这个值对直流减速电机来说听不到明显啸叫也不会让驱动板开关损耗过大。value直接写占空比1.0 就是 100% 占空比0.2 就是 20%。负速度用方向引脚取反实现而不是给 PWM 传负值因为 gpiozero 的 PWM 引脚只接受 0 到 1 的浮点数。调车时先空转测试每个电机的正方向把左右轮的 IN 接线记下来不然后续差速转向全是反的。电机驱动还有一个容易被忽略的参数PWM 频率过低会导致电机一顿一顿过高会让驱动板发热。下表是我常用的参数范围参数推荐值说明PWM 频率500Hz ~ 1000Hz频率过低会抖动过高增加开关损耗占空比最小值15% ~ 20%低于此值电机可能不转占空比最大值80% ~ 90%留出余量避免电池电压跌落时失控上电 GPIO 状态全部设为 LOW防止 GPIO 默认高电平导致小车上电猛冲2.3 差速转向中的占空比分配双轮差速是这类小车最常见的转向方式。左轮快右轮慢就右转左轮反转右轮正转就原地转圈。控制逻辑不复杂但有一个关键点避障、遥控、巡线都会给出“转向角度”或“转向方向”最后都要换算成左右轮速度。建议把电机控制封装成一个统一的set_motor(left_speed, right_speed)接口上层模块只负责算速度不直接碰 GPIO。这样后面接超声波避障、或者改成 PS2 手柄遥控都不需要重写底层驱动。原地旋转时占空比不能太大。两轮转速相反如果都在 80% 占空比车身会剧烈抖动而且树莓派 USB 供电会被拉低。实测中原地旋转用 40%~50% 占空比就够了速度快反而容易因为地面摩擦导致打滑航向偏差很难修正。3. 超声波自动避障先测准距离再做决策3.1 HC-SR04 测距函数从时序到距离换算HC-SR04 测距依赖声波往返时间。树莓派先给 Trig 引脚一个不低于 10μs 的高电平模块自动发出 8 个 40kHz 脉冲然后把 Echo 引脚拉高高电平持续时间就是声波往返的时间。距离等于时间乘以声速再除以 2。import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) TRIG 20 ECHO 21 GPIO.setup(TRIG, GPIO.OUT) GPIO.setup(ECHO, GPIO.IN) def measure_distance(): # 发送 10us 触发信号 GPIO.output(TRIG, True) time.sleep(0.00001) GPIO.output(TRIG, False) # 等待 Echo 变高记录上升沿时间 timeout time.time() 0.1 while GPIO.input(ECHO) 0: if time.time() timeout: return 999 pulse_start time.time() # 等待 Echo 变低记录下降沿时间 while GPIO.input(ECHO) 1: if time.time() timeout: return 999 pulse_end time.time() duration pulse_end - pulse_start # 声速 343m/s换算成 cm往返所以要除以 2 distance duration * 34300 / 2 return round(distance, 1) while True: d measure_distance() print(fdistance: {d} cm) time.sleep(0.1)这段代码的关键在两个while循环。代码里加了一个 0.1 秒的超时判断当检测超时返回 999这样不会卡死。注意 999 在避障逻辑里要按“无目标处理”不能直接拿去和阈值比较。HC-SR04 的测量范围是 2cm~400cm小于 2cm 时回波信号会被盲区吃掉所以车头距离障碍物已经非常近时超声波反馈可能突然跳到 999必须加前后两次测量取最小值或者加滤波。3.2 避障决策距离阈值和转向优先级自动避障不能只在前方有障碍时转向还要考虑车已经靠近障碍物的情况。常见的阈值设计是三级大于 30cm 正常前进20~30cm 减速并小幅转向小于 20cm 停车并大幅转向。测量距离决策左右轮速度d 30cm直线前进(0.5, 0.5)20cm d 30cm减速右偏(0.3, 0.15)10cm d 20cm停车右转(-0.2, 0.2)d 10cm 或 999停车原地旋转(-0.3, 0.3)为什么默认右转而不是左转既然后面要加摄像头和图像传输摄像头通常装在车头靠右或者居中位置右转时摄像头视野能扫到更多区域左转会被车头右侧盲区卡住。这个方向不是绝对的如果右侧有墙壁或车道线就把左右规则反一下。比较稳妥的做法是把转方向做成全局变量在遥控或上位机指令里可以随时改。3.3 用独立函数封装避障逻辑避免阻塞图像处理避障逻辑不要写在 GPIO 的回调函数里也不要写成一个死循环里面又做摄像头读取又做测距。把避障封装成avoidance_step()返回左右轮速度主循环只负责调用它并喂给电机驱动。def avoidance_step(): d measure_distance() if d 30: return (0.5, 0.5) elif d 20: return (0.3, 0.15) elif d 10: return (-0.2, 0.2) else: return (-0.3, 0.3) while True: left_v, right_v avoidance_step() set_motor(left_v, right_v) time.sleep(0.05)这个循环周期 50ms避障响应已经足够快。真正要注意的是超声波测距本身会阻塞 10~20ms如果在同一线程里先测距再读摄像头再跑目标检测整体帧率会掉得很厉害。所以避障决策可以单独放一个线程或者每 3~5 帧图像才执行一次测距把 CPU 时间优先让给图像链路。4. 实时图像传输MJPEG over HTTP 是最省事的方案4.1 为什么在局域网里用 MJPEG 而不是 RTMP 或 WebRTC如果只是把树莓派摄像头画面传到浏览器看常见的选择有三种RTMP、WebRTC、MJPEG over HTTP。RTMP 需要推流服务器还要装 nginx-rtmp 或者 SRS在树莓派上属于杀鸡用牛刀WebRTC 延迟最低但需要信令服务和 STUN/TURN 配置前端也要引入 WebRTC 库对小车这种随时可能断网重新连接的场景反而难调试。MJPEG over HTTP 的思路是把每一帧图像编码成 JPEG然后用 HTTP 的multipart/x-mixed-replace格式一个接一个发出去。浏览器直接访问http://树莓派IP:8000/stream就能看到视频。它的优点是实现仅需几十行 Python不依赖第三方流媒体服务器每一帧都是独立 JPEG单帧丢失不会导致视频流中断树莓派的 GPU 硬件编码器对 JPEG 编码有专门优化。缺点是没有音频传输、数据量相对 H.264 更大但在 640x480 分辨率下问题不大。4.2 用 OpenCV HTTPServer 实现实时图像传输服务树莓派安装 OpenCV 后可以直接把摄像头读到的帧传给浏览器。下面的代码是完整的 MJPEG HTTP 服务读帧、编码、响应都在同一个请求循环里完成跑通后再改成多线程。import cv2 from http.server import BaseHTTPRequestHandler, HTTPServer cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 固定曝光防止明暗跳动 class StreamHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path /stream: self.send_response(200) self.send_header(Content-Type, multipart/x-mixed-replace; boundaryframe) self.end_headers() while True: ok, frame cap.read() if not ok: break # 后续目标检测的绘制结果可以画在 frame 上 ok, jpeg cv2.imencode(.jpg, frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) data jpeg.tobytes() self.wfile.write(b--frame\r\n) self.wfile.write(bContent-Type: image/jpeg\r\n\r\n) self.wfile.write(data) self.wfile.write(b\r\n) else: self.send_response(404) self.end_headers() def log_message(self, format, *args): pass # 关闭访问日志减少串口刷屏 HTTPServer((0.0.0.0, 8000), StreamHandler).serve_forever()代码里固定了摄像头分辨率 640x480 和 JPEG 质量 80。imencode的作用是把 OpenCV 的 BGR 矩阵编码成 JPEG 字节流IMWRITE_JPEG_QUALITY是 0~100 的质量参数80 在局域网里画质足够且字节数比较小。multipart/x-mixed-replace是浏览器支持的一种流式响应格式boundaryframe定义了每帧之间的分隔标记浏览器读到新分隔符就会刷新本帧。最后在浏览器地址栏输入http://树莓派IP:8000/stream就能看到画面。需要注意cap.read()在树莓派 USB 摄像头上偶尔会返回空帧代码里直接break退出循环会导致整个服务挂掉实际用的时候要加一个continue跳过空帧。4.3 帧率、分辨率和带宽的取舍树莓派上的实时图像传输实际瓶颈往往不是编码而是摄像头读帧方式和 CPU 占用。下表是常用配置下的大致开销树莓派4B系列在同一时刻只能跑一个主要任务编码和检测同时进行时需要做资源分配。分辨率JPEG质量单帧大小估30fps 带宽适用场景320x240708~12KB~2.5Mbps避障辅助、低负载持续推流640x4808030~50KB~8Mbps目标检测与网球追踪1280x7208580~120KB~25Mbps只在调试时使用延迟升高如果帧率不稳定首先检查是不是用了 USB 摄像头而非 CSI 摄像头。CSI 摄像头比如 OV5647 模块走专门的 CSI 接口不占用 USB 总线稳定性和帧率远好于 USB 摄像头。如果只能使用 USB 摄像头选择支持 MJPEG 输出的型号可以大幅降低 CPU 负担因为树莓派直接读取的就是 JPEG 帧不需要额外做 JPEG 编码。4.4 图像传输和目标检测共用一份帧数据实时图像传输和后续目标检测不要各自开摄像头去cap.read()。OpenCV 的 VideoCapture 同时被两个线程读是不安全的常见做法是一个生产线程读帧并放进全局变量HTTP 发送线程和检测线程都只读这份最新帧frame_lock threading.Lock() latest_frame None def camera_loop(): global latest_frame while True: ok, frame cap.read() if ok: with frame_lock: latest_frame frame.copy()图像处理线程拿到的永远是摄像头最新的一帧。检测慢一点没关系画面可以卡顿但不会因为多线程抢摄像头导致读帧失败。5. YOLO 目标检测与网球追踪先检测到再追得上5.1 树莓派 CPU 上跑 YOLOv8n不需要 GPU目标检测在树莓派上的方案很多YOLO 是现在的主流选择完整的 YOLO 目标检测流程包括图像预处理、模型推理、置信度过滤、非极大值抑制。树莓派没有 NVIDIA GPU所以不能用 CUDA 加速。如果先跑yolov8s再降输入分辨率小目标基本检测不到反过来用yolov8nnano 版本配合 320x240 输入单帧推理在树莓派4B上通常能控制在 100ms 左右再配合跳帧处理可以把有效检测频率做到 5~10 FPS。这就是“全网都在讨论目标检测需要用到 GPU 吗”的答案的一部分用轻量模型时 CPU 也能跑只是帧率有限。import cv2 import numpy as np import onnxruntime as ort session ort.InferenceSession(yolov8n.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name output_name session.get_outputs()[0].name def detect(frame): h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 1/255.0, (320, 240), swapRBTrue, cropFalse) results session.run([output_name], {input_name: blob})[0] # 对 results 做置信度过滤和 NMS得到 boxes, scores, class_ids return boxes, scores, class_idsblobFromImage会完成缩放、归一化和通道转换一次性输出推理要的输入张量。模型输出维度在 YOLOv8 里是[1, 84, 8400]其中 84 表示 4 个框坐标加上 80 个 COCO 类别得分8400 是三个尺度下 anchor 的总数。后处理阶段要把这个矩阵拆成多个框然后用cv2.dnn.NMSBoxes去掉重叠框。如果你要追踪的网球不在 COCO 类别里用 YOLO 检测球是不可靠的这时候就要用颜色追踪。5.2 用 HSV 颜色空间提取网球候选区目标检测负责识别画面中的人、瓶子、遥控器等常规物体而网球这种既小又快的目标更适合交给 HSV 颜色分析处理。OpenCV 默认把图像存成 BGR但 BGR 对光照很不稳定同一种绿色在阴影里会变成深蓝绿色所以要先转换到 HSV 颜色空间再用固定的色相范围过滤。def find_tennis(frame): hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, (35, 80, 60), (80, 255, 255)) mask cv2.erode(mask, None, iterations1) mask cv2.dilate(mask, None, iterations2) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None # 选面积最大的轮廓过滤掉小噪点 largest max(contours, keycv2.contourArea) if cv2.contourArea(largest) 200: return None M cv2.moments(largest) if M[m00] 0: return None cx int(M[m10] / M[m00]) cy int(M[m01] / M[m00]) return (cx, cy)HSV 的过滤范围中H 是色相、S 是饱和度、V 是明度。网球的荧光绿大约在 H35~80 之间S 要高于 80 才不会被灰色地面干扰V 不能设得太高否则高光反射会让球的中心变成白色空洞。erode和dilate的作用是去掉噪点并填补球体上的高光空洞迭代次数太少会导致轮廓断成好几块。如果追踪的是橙色网球就把色相范围改成 (5, 25)。参数不是凭空猜的现场运行后调整最简单的方法是打印 HSV 值并记录球在不同位置的取值范围。5.3 用卡尔曼滤波预测位置减少运动模糊导致的漏检HSV 颜色追踪看着简单但帧与帧之间目标容易出现跳变球运动太快产生运动模糊轮廓被拉长或者球暂时被手臂遮挡掩膜直接丢失。这种情况下最实用的补点方案是卡尔曼滤波用上一时刻的位置和速度预测下一帧位置如果这一帧没有找到球就把预测值当作当前值输出。from filterpy.kalman import KalmanFilter kf KalmanFilter(dim_x4, dim_z2) kf.F np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]], dtypefloat) kf.H np.array([[1, 0, 0, 0], [0, 1, 0, 0]], dtypefloat) def track_ball(frame): pt find_tennis(frame) if pt is None: # 没检测到球直接用预测位置 pred kf.predict() return int(pred[0]), int(pred[1]) kf.update(np.array(pt, dtypefloat)) pred kf.predict() return int(pred[0]), int(pred[1])卡尔曼滤波的状态向量是(x, y, vx, vy)H 矩阵把状态映射到观测值(x, y)。检测到球时执行update让预测值和真实位置融合检测不到时只执行predict位置会按照上一帧速度惯性滑行。dim_x4和dim_z2是状态维度和观测维度一旦改错矩阵维度程序会直接报错。需要说明的是这种匀速模型只适合球速变化不大的场景如果球在空中有明显抛体运动加速度还是得手动加一个加速度状态。实际测试中卡尔曼滤波不会让追踪成功率提升到 100%但它能把 HSV 掩膜断裂时的小抖动压一压让轨迹更平滑。5.4 追踪稳定性技巧ROI 搜索配合固定曝光做网球追踪最容易犯的错误是全图每个像素都跑一遍 HSV 过滤。树莓派 CPU 资源有限720p 图像全图处理会占用几十毫秒。既然摄像头固定在小车上球的位置帧间变化就不会太大更好的做法是先裁剪以预测点为中心的区域做识别找不到再逐步扩大范围。这个 ROI 窗口就是卡尔曼滤波预测位置加一个固定偏移量比如上一帧球在 (300, 200)那就在 260~340 和 160~240 这个 80x80 窗口里找球。每帧先裁 ROI只有 ROI 里没找到才全图搜索整体计算量至少少一半。想进一步减少跳变还有两个容易忽略的细节。一是在 HSV 追踪前先关掉摄像头的自动曝光自动曝光会让网球表面颜色在明暗交替时漂移出过滤范围实际效果比调 HSV 阈值更立竿见影。二是用cv2.CALIB_CB_ASYMMETRIC_GRID这类标定方式先把摄像头畸变校准一遍网球在画面边缘时会被拉成椭圆让轮廓面积和重心位置产生偏差。校准完再用固定曝光参数球的轨迹就会从“跳点连成线”变成“连续曲线”。本文还有配套的精品资源点击获取
返回列表