ARTICLE DETAIL

资讯详情

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

树莓派图像识别循迹避障小车:从HSV标定到PWM控制的完整方案

树莓派图像识别循迹避障小车:从HSV标定到PWM控制的完整方案 简介基于树莓派图像识别的智能循迹避障小车项目覆盖电脑端与树莓派端两套程序源码适合正在完成毕业设计、课程设计或工程实训的入门及进阶学习者。项目以道路检测和标识牌识别为核心道路部分采用图像灰度中心位置差判断左右转弯标识牌部分借助开源视觉库训练分类模型整体思路简洁便于理解算法与硬件配合流程。压缩包共十四个文件包含四个程序源文件、三个识别模型配置文件、三份说明文档另附测试图片与许可证整体大小约208KB结构紧凑可按电脑端与树莓派端分开查看。目前已有164人学习下载。使用者能从中获得一套完整的车道检测、标识牌识别与小车控制参考方案配合说明文档能理清图像处理、串口通信和电机驱动的关系再根据实际环境调试报错并扩展避障、调速等能力是图像识别小车入门的实用样板。1. 树莓派图像识别循迹避障小车的方案分层为什么电脑端和树莓派端要分工拿到底盘套件时大家的第一反应是去找识别模型把“能识别”当成“能做小车”。实际上这辆树莓派图像识别循迹避障小车最难的不是算法先进而是摄像头跟着车跑起来之后还能保持稳定的颜色判断。曝光一变、白平衡一抖阈值立刻失效转向还没送出来车已经冲出赛道。所以这套项目从组织上先拆成电脑端和树莓派端。电脑端做离线标定、实时调参、监控与录制树莓派端只负责采集、算偏差、转 PWM两端用 socket 打通调一个 HSV 参数不用反复拔 SD 卡。适合正在做嵌入式视觉课设的学生也适合想用图像识别替代灰度传感器的玩家以及要把“视觉—控制”链路整体跑通的地面机器人开发者。2. 电脑端图像识别选型与 HSV 调参把“看赛道”这件事先在电脑上闭环2.1 循迹与避障优先选颜色识别不急着上深度学习小车在固定赛道、固定光照、单一地板材质下运行时赛道线、障碍物和背景在颜色上是可区分的所以先采用“颜色分割 几何特征”的方案。树莓派4B 上 CPU 处理 320x240 的 HSV 图像能稳定跑到 15-25fps这部分开销可控一旦换成轻量级目标检测模型帧率会掉到 5fps 上下视觉结果无法平滑驱动两周期的转向控制车身高频修正跟不上。避障也优先用颜色和面积比而不是深度相机。单目摄像头估算不了绝对距离但能通过图像中“近景 ROI 区域的占比”判断障碍物接近程度这在结构化赛道上已经够用。只有在障碍物形状多变、背景杂乱、需要识别“这是什么物体”时才值得把电脑端换成带标注数据训练的轻量模型。这一步在树莓派端只需要保留相同的 ROI 和后续控制接口不会推翻整个设计。2.2 电脑端 OpenCV HSV 标定脚本取色、调阈值、存参数一次完成无论赛道是“白色地板 黑色胶带”还是“绿色地毯 白色线”第一步都是把目标颜色从 BGR 转到 HSV 再设阈值。HSV 把亮度分量单独拿出来受车灯、窗户反光影响比 BGR 小得多。电脑端连同一颗摄像头或同一个视频文件用下面的脚本调参数# hsv_calib.py import cv2 import numpy as np import json frame None hsv_frame None def on_mouse(event, x, y, flags, param): # 鼠标点击画面时打印该点的 HSV 值方便确定 H/S/V 的上下限 if event cv2.EVENT_LBUTTONDOWN and hsv_frame is not None: h, s, v hsv_frame[y, x] print(fclicked HSV({h},{s},{v})) def on_trackbar(value): pass cv2.namedWindow(calib) cv2.setMouseCallback(calib, on_mouse) trackbars [H_min, H_max, S_min, S_max, V_min, V_max] limits [(0, 179), (0, 179), (0, 255), (0, 255), (0, 255), (0, 255)] for name, (lo, hi) in zip(trackbars, limits): cv2.createTrackbar(name, calib, lo, hi, on_trackbar) cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break hsv_frame cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) h_min cv2.getTrackbarPos(H_min, calib) h_max cv2.getTrackbarPos(H_max, calib) s_min cv2.getTrackbarPos(S_min, calib) s_max cv2.getTrackbarPos(S_max, calib) v_min cv2.getTrackbarPos(V_min, calib) v_max cv2.getTrackbarPos(V_max, calib) mask cv2.inRange(hsv_frame, (h_min, s_min, v_min), (h_max, s_max, v_max)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8)) cv2.imshow(mask, mask) key cv2.waitKey(1) 0xFF if key ord(s): config { h_min: h_min, h_max: h_max, s_min: s_min, s_max: s_max, v_min: v_min, v_max: v_max, } with open(hsv_config.json, w) as f: json.dump(config, f, indent2) print(saved hsv_config.json) elif key ord(q): break cap.release() cv2.destroyAllWindows()这段脚本的核心是把“调参”变成可视化过程。on_mouse里打印点击点的 HSV能快速确定 H 的大致范围滑动条实时修改cv2.inRange的上下限MORPH_CLOSE闭合运算弥补透视造成的断线。保存的 JSON 文件之后直接由树莓派端加载两端的 HSV 格式完全一致都是 OpenCV 的 0-179/0-255 取值。红色是特例。OpenCV 的色相环是 0-179红色同时跨在 0 附近和 179 附近单区间阈值会漏掉一半。标定红色障碍物时要么把掩膜按两个区间分别生成再按位或要么在脚本里把 H 小于 10 和大于 170 的部分合并。常见物体与赛道颜色的 HSV 参考范围如下实际要以点击取色结果为准识别目标H 范围S 范围V 范围说明黑胶带白底0-1790-600-90低饱和度优先亮度范围要收窄深蓝赛道100-124100-25580-255配合白色线反掩膜红色障碍物0-10 与 170-179120-255120-255需要两个区间合并绿色地毯35-8580-25560-255提取背景色做反掩膜更快2.3 从 mask 到控制偏差一次只算一条中线HSV 调好后识别层输出的不该是一整张图而是一个归一化的偏差值。取图像底部一段 ROI因为这部分离车头最近透视变形最小曲率也最接近当前需要执行的转向。def line_center(mask, roi_rows(240, 300)): roi mask[roi_rows[0]:roi_rows[1], :] moments cv2.moments(roi) if moments[m00] 50: return None cx int(moments[m10] / moments[m00]) return (cx - roi.shape[1] / 2) / (roi.shape[1] / 2)m00小于 50 时认为这条 ROI 里没有有效目标返回None控制层据此切换到搜索模式。偏差归一化到 -1 到 1 之后后续 PID 参数和转向系数的量纲就固定了dx 0表示线在画面正中央dx 0表示需要向右修正符号取决于摄像头安装朝向。电脑端标定的最后一步是把这段函数放到一段录像上逐帧跑一遍统计偏差序列是否连续。偏差连续跳变超过 0.4 的帧通常不是算法问题而是 HSV 范围过窄导致掩膜碎片化。3. 树莓派端摄像头采集、PWM 电机输出与循迹避障状态机3.1 树莓派4B 摄像头环境先锁曝光和白平衡再谈识别稳定树莓派端推荐 OV5647 CSI 摄像头固定好之后镜头不会像 USB 摄像头那样受震动影响。第一步先验证硬件通路sudo apt update sudo apt install -y python3-opencv python3-picamera2 libcamera-hello --output test.jpg没有接显示器时带--output参数抓一帧静态图能生成图片就说明 CSI 通路正常。OpenCV 里cv2.VideoCapture(0)在很多树莓派镜像上也能直接读但 Picamera2 对曝光、白平衡、增益的控制更完整。摄像头驱动起来以后立刻固定自动曝光和自动白平衡from picamera2 import Picamera2 picam2 Picamera2() config picam2.create_still_configuration( main{size: (640, 480)}, ) picam2.configure(config) picam2.set_controls({ AwbEnable: False, AeEnable: False, AnalogueGain: 2.0, ColourGains: (1.2, 1.4), })自动白平衡在室内灯光和窗外阳光之间跳变时同一块黑色的 HSV 值会漂移几十个单位这就是“昨天调好的阈值今天全失效”的主要原因。固定AwbEnable和AeEnable后颜色判断的前提才成立。ColourGains的值可以先在电脑端标定时用手动白平衡确定如果赛道路面偏黄就把红色增益调低。3.2 采集线程与识别线程分离用两个槽位的队列保住最新帧小车的控制循环里GPIO 输出、socket 发送和图像计算挤在同一个循环时一慢全慢。常见做法是拆成两个线程一个只往队列里放最新帧另一个取出帧做识别和控制。import threading import queue class FrameBuffer: def __init__(self, maxsize2): self.q queue.Queue(maxsizemaxsize) def put(self, frame): if self.q.full(): try: self.q.get_nowait() except queue.Empty: pass self.q.put_nowait(frame) def get(self): try: return self.q.get_nowait() except queue.Empty: return None最大槽位设成 2 而不是无限增长。树莓派端图像处理慢时旧的帧继续堆积只会让控制指令越来越滞后直接丢掉旧帧始终处理最新的一帧转向反应才会接近实时。线程入口里buffer.put(frame)永远不阻塞识别线程里buffer.get()拿不到帧就跳过本轮控制保持上一轮 PWM 输出。3.3 GPIO 与 TB6612 接线PWM 频率、死区和方向引脚电机驱动用 TB6612FNG 比 L298N 轻压降小低速线性度也更好。下面是本项目的参考接线树莓派端使用 BCM 编号树莓派 GPIOTB6612 引脚作用23AIN1左电机方向 124AIN2左电机方向 212PWMA左电机 PWMPWM0 通道17BIN1右电机方向 127BIN2右电机方向 213PWMB右电机 PWMPWM1 通道25STBY必须拉高否则电机不转外部 5VVM电机电源树莓派 5V 不够驱动电机注意电机电源和树莓派电源要分开但两个电源的 GND 必须接在一起否则 GPIO 信号没有参考电位现象是电流一大就随机重启。import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setup([23, 24, 17, 27, 25], GPIO.OUT) GPIO.output(25, GPIO.HIGH) pwm_a GPIO.PWM(12, 1000) pwm_b GPIO.PWM(13, 1000) pwm_a.start(0) pwm_b.start(0) DEAD_ZONE 12 def motor_run(pwm, pin1, pin2, speed): speed max(-100, min(100, speed)) if abs(speed) DEAD_ZONE: pwm.ChangeDutyCycle(0) return GPIO.output(pin1, speed 0) GPIO.output(pin2, speed 0) pwm.ChangeDutyCycle(abs(speed))GPIO.PWM(12, 1000)表示频率 1kHz对普通直流减速电机已经足够1kHz 以上不是必须。低速时电机会出现不转动、突然跳动的问题把abs(speed)小于DEAD_ZONE的区间直接置 0起步更干脆。真正需要更细腻低速控制时再把RPi.GPIO换成pigpio的硬件 PWM控制侧不需要改接口。3.4 循迹控制律与避障状态机从偏差到左右轮差速循迹控制用比例控制起步不需要一开始就上 PID。左右轮基准速度上叠加一个与偏差成比例的修正量base_speed 35 kp 28 def track_control(dx): if dx is None: return None delta kp * dx left int(base_speed delta) right int(base_speed - delta) return left, rightkp的值取决于赛道弯道半径和车速。先设 25-30在直道上观察偏差曲线如果车身左右摆动明显就降低kp如果入弯后转向不足就提高。这个公式没有积分项因为小车上偏差的稳态累积没有意义积分反而会造成过冲。避障部分用一个更简单的状态机正常循迹、避障中、搜索赛道。只在摄像头近景 ROI 里计算“非背景”像素占比占比超过阈值就认为障碍物已经逼近。MODE_TRACK, MODE_AVOID, MODE_SEARCH 0, 1, 2 mode MODE_TRACK avoid_ticks 0 def decide_action(dx, obstacle_ratio): global mode, avoid_ticks if obstacle_ratio 0.35: mode MODE_AVOID avoid_ticks 12 if mode MODE_AVOID: avoid_ticks - 1 if avoid_ticks 0: mode MODE_SEARCH return 0, -35 if mode MODE_SEARCH: return 20, -20 if dx is None: mode MODE_SEARCH return 15, -25 mode MODE_TRACK return track_control(dx)避障期间右转固定持续avoid_ticks周期靠时间而不是传感器判定转完之后进入搜索模式找线。搜索模式让小车原地旋转直到line_center重新返回非空值。这个状态机的顺序是避障优先级最高其次搜索最后才是循迹避免车已经撞上障碍物时还在追线。4. 电脑端与树莓派端通信联调Socket 传帧与指令4.1 协议选型TCP 比 UDP、MQTT 更适合双端联调树莓派和电脑在同一局域网内数据量是 640x480 的 JPEG 帧和几十字节的控制指令TCP 足够。UDP 丢一帧会导致画面卡顿但指令也可能丢对调试不友好MQTT 要额外跑 broker小车端资源紧张时不想多背一个进程。TCP 唯一的麻烦是粘包用固定长度头解决。方案帧传输指令可靠性额外依赖本场景结论TCP Socket可靠可靠无首选UDP低延迟不保证无预留扩展MQTT可靠可靠broker多机联动时再上HTTP逐帧请求开销大可靠Web 服务不适合控制闭环4.2 树莓派端 Socket 服务器JPEG 编码帧与 5 字节协议头树莓派端作为服务器监听端口电脑端作为客户端主动连接。协议头固定 5 字节第 1 字节是消息类型后 4 字节是大端序的负载长度。import socket import cv2 import numpy as np def send_msg(sock, msg_type, payload): head bytes([msg_type]) len(payload).to_bytes(4, big) sock.sendall(head payload) def recv_exact(sock, n): data b while len(data) n: chunk sock.recv(n - len(data)) if not chunk: return None data chunk return data def recv_msg(sock): head recv_exact(sock, 5) if head is None: return None msg_type head[0] length int.from_bytes(head[1:5], big) payload recv_exact(sock, length) return msg_type, payload服务器主循环里识别线程算出的当前帧通过send_msg(sock, 0x01, jpeg_bytes)发送jpeg_bytes由cv2.imencode(.jpg, frame, [cv2.IMWRITE_JPEG_QUALITY, 80])生成。质量设 80 而不是 95画面肉眼几乎看不出差异传输量能少一半。树莓派端同时用recv_msg接收电脑端发来的指令0x02表示修改 HSV 参数0x03表示启停控制。4.3 电脑端显示与控制键盘热调 HSV不用重启小车电脑端跟树莓派同一个局域网连接后一边显示视频帧一边把键盘事件转成指令发回去。核心循环如下import socket import cv2 import numpy as np client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((192.168.1.50, 9009)) while True: msg_type, payload recv_msg(client) if msg_type 0x01: frame cv2.imdecode(np.frombuffer(payload, np.uint8), cv2.IMREAD_COLOR) cv2.imshow(pi_feed, frame) key cv2.waitKey(1) 0xFF if key ord(h): send_msg(client, 0x02, bh_min-5) elif key ord(H): send_msg(client, 0x02, bh_min5) elif key ord(s): send_msg(client, 0x02, bsave) elif key ord(q): send_msg(client, 0x03, bstop) break电脑端不直接调本地参数而是把h_min-5这样的文本指令发给树莓派由树莓派端解析后修改运行中的 HSV 配置。这样在赛道上跑两圈按几下键就能把色域范围调准省去每次改文件、重启进程的往返。调好之后按s树莓派端把参数写回hsv_config.json下次开机直接加载。提示联调阶段电脑端一定要有急停键。小车失控时发送0x03的stop指令树莓派端收到后立即把两路 PWM 置 0比拔电池可靠。4.4 联调顺序从静止调色到上赛道按五步走第一步树莓派断开连接单独跑 GPIO 自检左右电机分别正转、反转、PWM 从 0 升到 100确认转向和提速方向与代码预期一致。第二步电脑端连上服务器对着地面静置的小车调 HSV直到掩膜中赛道线完整、背景不残留。第三步让小车离线跑一圈并录制视频用第 2 章的偏差函数离线看轨迹曲线。第四步把车架起来四轮悬空电脑端用按键调整kp和base_speed观察差速响应有没有方向反冲。第五步才真正放地上慢速试跑起始base_speed从 25 开始。5. 树莓派图像识别小车排错与帧率优化曝光、丢帧与模型替换5.1 三个最典型的误判场景第一个是曝光漂移。车从室内走廊开向窗边时自动曝光会让地面由亮变暗黑色胶带的 V 值瞬间升高掩膜变成一团噪点。固定AeEnable后为了适应不同时段的光照可以在电脑端按b发送“曝光档位 1/-1”指令而不是重新标定整套 HSV。第二个是反光与影子。地砖反光会让黑线中间出现高光断裂影子又会让白色地板变成灰蓝色干扰 S 通道。对这种情况减小 ROI 到车头前方 20-40cm 的区域并把MORPH_CLOSE的核加大到 7x7能明显减少误分割。不要在 V 通道上做全局二值化反光点处理不干净。第三个是电池电压下降。满电和低电时同一组 PWM 占空比下的实际车速可能差 20% 以上循迹的kp会从合适变成震荡。加一个电压检测点低于阈值时把base_speed整体下调 10比重新调 PID 更直接。5.2 帧率上不去的五个调整点帧率不足先看瓶颈在哪按下面的顺序逐项修改调整项修改方式预期效果分辨率640x480 降到 320x240色彩空间转换量减少约 75%ROI只处理底部 60 行掩膜计算量大幅下降JPEG 质量从 95 降到 80网络传输量减半跳帧每 2 帧识别 1 次控制频率不变识别负载减半PWM软件 PWM 换 pigpio 硬件 PWM减少 GPIO 中断对 CPU 的抢占320x240 分辨率下一条赛道线的宽度仍然占几十个像素不会影响中线计算精度。跳帧时注意控制线程必须每帧都跑只有识别线程可以跳。5.3 用录像回放做回归验证再谈替换模型的边界把一段赛道视频保存下来反复调整 HSV 参数时不需要把车放回赛道。用python hsv_calib.py --video test_lap.h264 --params hsv_config.json这类离线模式回放逐帧输出偏差值统计“偏差跳变大于 0.4 的帧数 / 总帧数”作为回归指标。改动参数后该指标不应变差。若后续要替换成目标检测模型录像文件本身就是数据集用它做轻量模型训练和边界验证替换时只需把line_center的输出换成模型的检测框中心。本文还有配套的精品资源点击获取
返回列表