基于树莓派与YOLOv8的智能交通系统:从数据采集到边缘部署全流程实践
1. 项目概述与核心思路
作为一名长期混迹于嵌入式开发和计算机视觉交叉领域的开发者,我始终对如何将前沿的AI算法落地到低成本、可触达的硬件上充满兴趣。这次,我将分享一个为期三周的快速原型项目:一个基于树莓派(Raspberry Pi)和YOLOv8的微型智能交通系统。这个项目的核心价值不在于构建一个工业级系统,而在于完整地走通“从数据到决策”的闭环,为AI初学者和嵌入式爱好者提供一个清晰、可复现的实践路径。它本质上是一个 目标检测模型在边缘计算设备上的集成与应用案例 ,通过统计玩具车流量来模拟真实路口的自适应信号灯控制。
你可能会问,为什么用玩具车和玩偶?答案很简单: 降低门槛,聚焦核心 。在真实道路上采集车辆数据、部署硬件涉及安全、隐私和成本等一系列复杂问题。而使用微型场景,我们可以用极低的成本(一个树莓派、一个USB摄像头、一些LED和玩具)快速验证整个技术栈的可行性。整个过程涵盖了计算机视觉项目最经典的几个环节:数据采集与标注、模型选择与训练、边缘设备部署、硬件交互控制。无论你是想了解YOLO模型如何从图片中“认出”物体,还是好奇树莓派的GPIO如何控制外部硬件,亦或是想学习如何让AI模型在资源受限的设备上跑起来,这个项目都能给你带来一次酣畅淋漓的动手体验。
2. 核心硬件选型与搭建逻辑
硬件是项目的骨架,选型的合理性直接决定了后续开发的顺畅度。我的核心思路是: 计算单元、感知单元、执行单元、显示单元 四部分清晰分离,并确保它们之间的连接尽可能简洁、可靠。
2.1 计算核心:为什么是Raspberry Pi 5?
我选择了树莓派5作为主控。相较于树莓派4,Pi 5的CPU和内存带宽有显著提升,这对于运行YOLOv8这类轻量级但依然有计算需求的模型至关重要。在实测中,使用 yolov8s 模型处理640x480分辨率的图像,Pi 5的推理速度(FPS)比Pi 4平均快30%以上,这意味着更流畅的实时处理体验。当然,如果你手头只有Pi 4,项目同样可以运行,只需在代码中适当调整图像输入尺寸或模型版本(例如使用更小的 yolov8n )来平衡性能。
注意 :务必为树莓派配备官方电源或能提供5V/5A稳定电流的电源适配器。Pi 5在高负载下功耗增加,供电不足会导致系统不稳定、频繁重启,这是新手最容易踩的坑。
2.2 感知单元:摄像头的部署与伪装
感知部分由一个普通的USB网络摄像头完成。为了融入场景,我将其伪装成了一个交通锥。这里有几个关键细节:
- 摄像头固定 :我使用了一个小型三脚架,将其主体藏于锥筒内部,仅让云台螺丝从锥筒顶部预先开好的小孔穿出。这样既能稳定固定摄像头,又能灵活调整俯仰角度。
- 视角与高度 :摄像头的安装高度和角度决定了检测范围。我将其置于“路口”正上方约30厘米处,镜头略微向下俯视,以确保能覆盖整个“道路”区域(90x200cm的游戏垫)。在真实项目中,这对应着选择合适的路灯或龙门架安装位置。
- 照明考虑 :尽管是室内项目,均匀的光照对检测精度影响巨大。应避免单一强光源造成的反光和阴影。我使用了两个台灯从两侧补光,模拟阴天均匀的自然光,这能有效提升模型在不同“天气”下的鲁棒性。
2.3 执行与显示单元:GPIO控制与状态反馈
执行单元是两组红黄绿LED,模拟交通信号灯。它们直接连接到树莓派的GPIO引脚。我使用了一块Freenove扩展板,这极大地简化了接线,避免了直接焊接或使用面包线容易松脱的问题。如果你没有扩展板,务必对照树莓派GPIO引脚图,正确连接每个LED的正极(通过一个220Ω的限流电阻)到指定的GPIO,负极连接到GND。
显示单元是一个1602字符型LCD屏,用于实时显示左右两个方向的车辆计数。它通过I2C接口与树莓派通信,仅需连接4根线(VCC, GND, SDA, SCL),编程简单。LCD屏提供了直观的系统状态反馈,在调试阶段尤其有用,你可以立刻知道检测算法是否在正常工作,计数逻辑是否正确。
硬件连接表示例(基于扩展板) :
| 组件 | 信号线 | 连接至树莓派GPIO (BCM编号) | 备注 |
|---|---|---|---|
| 左侧红灯 | 控制线 | GPIO 16 | 高电平点亮 |
| 左侧黄灯 | 控制线 | GPIO 20 | 高电平点亮 |
| 左侧绿灯 | 控制线 | GPIO 21 | 高电平点亮 |
| 右侧红灯 | 控制线 | GPIO 13 | 高电平点亮 |
| 右侧黄灯 | 控制线 | GPIO 6 | 高电平点亮 |
| 右侧绿灯 | 控制线 | GPIO 5 | 高电平点亮 |
| LCD显示屏 | SDA | GPIO 2 (SDA1) | I2C数据线 |
| SCL | GPIO 3 (SCL1) | I2C时钟线 | |
| VCC | 5V引脚 | ||
| GND | GND引脚 |
3. 数据集构建:从玩具车到可学习的特征
模型训练的上限由数据集质量决定。我们的目标是让YOLOv8学会识别各种姿态、光照下的玩具车。
3.1 数据采集的“心机”
我拍摄了约500张原始图片,但绝不是随意乱拍。每一张都蕴含了让模型泛化能力更强的设计:
- 多样性背景 :不仅在纯色桌面拍摄,更在游戏垫、木纹桌面、甚至带纹理的地毯上拍摄,让模型不依赖特定背景。
- 多角度与遮挡 :包含车辆的正面、侧面、45度角视图,并故意安排一些车辆部分被遮挡(如被另一个锥筒挡住一半),模拟真实交通中的遮挡情况。
- 光照变化 :开了灯、关了灯、用台灯制造侧光,甚至用手在车顶制造移动的阴影。这能强迫模型学习物体本身的特征,而非光照条件。
- 尺度与数量变化 :图片中包含1辆、3辆、5辆甚至更多车,车辆在画面中的大小也从占据1/4画面到仅占1/20画面不等。
3.2 高效标注与数据增强策略
标注是枯燥但关键的一步。我使用Roboflow平台,它提供的 自动预标注(Auto-Label) 功能是效率倍增器。我先手动标注了50张图片,然后用这50张图片训练一个临时的初始模型,让这个模型去预测剩下的图片,我只需要检查和修正错误即可。这比从头到尾手动画框快了至少3倍。
数据增强是“无中生有”的艺术。在Roboflow上,我应用了以下增强组合,将数据集扩增至1000张:
- 几何变换 :随机水平翻转(模拟对向车道)、小幅度的旋转(±15度)和剪切(模拟倾斜视角)。
- 色彩空间变换 :调整亮度、对比度、饱和度,并添加随机灰度化(模拟不同天气和摄像头色彩差异)。
- 噪声与模糊 :添加微量的高斯噪声和运动模糊(模拟摄像头快速移动或低光照下的噪点)。
这些增强操作都是在线的,即每次训练时随机应用,而不是永久修改原图,这能保证模型看到几乎无限多样的样本变体。
3.3 数据集划分的科学性
我采用了85%训练集、10%验证集、5%测试集的划分比例。这里有个小心得: 验证集和测试集绝对不能使用数据增强 。它们必须是原始的、具有代表性的真实场景图片,用于公正地评估模型在“没见过”的数据上的真实表现。如果验证集也做了增强,你会得到一个虚高的、误导性的精度指标。
4. YOLOv8模型训练与优化实战
训练是将数据“知识”注入模型的过程。我选择YOLOv8是因为它在精度和速度间取得了极佳的平衡,且Ultralytics库的API非常友好。
4.1 环境搭建与关键参数解析
首先在电脑(性能更强,用于训练)上创建Python 3.11环境,并安装 ultralytics 和 roboflow 库。训练代码的核心部分如下,我将逐行解释关键参数:
from ultralytics import YOLO
# 加载预训练模型骨架
model = YOLO('yolov8s.pt') # 使用‘s’(small)版本,兼顾精度与速度
# 开始训练
results = model.train(
data='path/to/your/data.yaml', # Roboflow导出的数据集配置文件
epochs=40, # 遍历整个训练集的次数
imgsz=640, # 输入图像缩放到的尺寸(长边)
batch=8, # 一次送入模型的图片数量,受显卡内存限制
patience=10, # 如果连续10个epoch验证指标没提升,则提前停止
device='0', # 使用第一块GPU,如果是CPU则设为‘cpu’
workers=4, # 数据加载的线程数,加快数据读取
optimizer='AdamW', # 优化器,AdamW通常比默认的SGD收敛更快
lr0=0.01, # 初始学习率
weight_decay=0.0005 # 权重衰减,防止过拟合
)
-
imgsz=640:YOLOv8会将图片统一缩放到这个尺寸。更大的尺寸(如1280)能检测更小的物体,但计算量呈平方增长。对于我们的玩具车场景,640足够。 -
batch=8:在RTX 3060 12GB显卡上,这个值比较安全。如果出现“CUDA out of memory”错误,需要降低batch或imgsz。 -
patience=10:这是防止过拟合、节省训练时间的“保险丝”。当验证集损失不再下降时,继续训练只会让模型记住训练集的特有噪声。
4.2 训练过程监控与调优
启动训练后,不要干等。Ultralytics会启动一个本地Web页面(默认 http://localhost:port ),展示损失曲线、精度(mAP)曲线等关键指标。
- 关注验证集损失(val/loss) :它应该随着训练持续下降并最终趋于平稳。如果训练集损失下降但验证集损失上升,这是典型的过拟合,需要增加数据增强强度、添加
weight_decay或使用更早的模型检查点。 - 关注mAP@0.5 :这是核心评估指标,表示在IoU(交并比)阈值为0.5时的平均精度。我们的玩具车检测项目,最终在测试集上达到了 98.2%的mAP@0.5 ,说明模型已经学得非常好了。
训练完成后,使用 model.export(format='onnx') 将模型导出为ONNX格式。ONNX是一种通用的模型格式,在树莓派上使用ONNX Runtime进行推理,通常比直接使用PyTorch原模型效率更高,内存占用更小。
5. 系统软件架构与核心代码实现
整个系统的软件部分分为两大模块:运行在电脑上的 图像采集与发送服务端 ,以及运行在树莓派上的 图像接收、推理、控制客户端 。两者通过Socket通信。
5.1 服务端(电脑):高效的图像抓取与流式传输
服务端的核心任务是稳定、低延迟地捕获摄像头画面,并通过网络发送给树莓派。这里没有使用复杂的视频流协议,而是采用 按需抓取+JPEG压缩 的方式,以降低带宽要求和编码复杂度。
# camera_server.py (简化版核心逻辑)
import socket
import cv2
import pickle
import struct
def start_server(host='0.0.0.0', port=5000):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen(1)
cap = cv2.VideoCapture(0) # 打开摄像头
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
while True:
client_socket, addr = server_socket.accept()
try:
while True:
ret, frame = cap.read()
if not ret:
break
# 编码为JPEG,大幅减少数据量
_, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
data = pickle.dumps(buffer)
# 先发送数据长度,再发送数据本身,这是Socket传输图像的常用技巧
message = struct.pack("Q", len(data)) + data
client_socket.sendall(message)
except Exception as e:
print(f"Client disconnected. {e}")
finally:
client_socket.close()
实操心得 :
cv2.imencode将图像压缩为JPEG格式,相比传输原始的BGR数组,数据量减少了90%以上,极大减轻了网络压力。设置JPEG_QUALITY=80在画质和压缩率间取得了很好的平衡。
5.2 客户端(树莓派):推理、计数与控制的融合
树莓派端的代码是系统的大脑,它需要连续完成接收图像、推理、计数、显示、控制信号灯这一系列动作。
# pi_client.py (核心逻辑节选)
import socket
import cv2
import pickle
import struct
from ultralytics import YOLO
import RPi.GPIO as GPIO
import time
# 1. 硬件初始化
GPIO.setmode(GPIO.BCM)
# ... 初始化LED引脚为输出模式,LCD屏幕 ...
# 2. 加载训练好的模型
model = YOLO('best.onnx', task='detect') # 使用导出的ONNX模型
# 3. 连接服务器
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('电脑的IP地址', 5000))
data = b""
payload_size = struct.calcsize("Q")
while True:
# 4. 接收图像数据
while len(data) < payload_size:
packet = client_socket.recv(4*1024)
if not packet: break
data += packet
packed_msg_size = data[:payload_size]
data = data[payload_size:]
msg_size = struct.unpack("Q", packed_msg_size)[0]
while len(data) < msg_size:
data += client_socket.recv(4*1024)
frame_data = data[:msg_size]
data = data[msg_size:]
# 5. 解码并推理
buffer = pickle.loads(frame_data)
frame = cv2.imdecode(buffer, cv2.IMREAD_COLOR)
height, width = frame.shape[:2]
midline = width // 2 # 计算图像中轴线,用于划分左右车道
results = model(frame, imgsz=640, verbose=False) # 执行推理
left_count, right_count = 0, 0
# 6. 遍历检测结果并计数
for box in results[0].boxes:
x1, y1, x2, y2 = box.xyxy[0].tolist()
cls = int(box.cls[0])
conf = float(box.conf[0])
if conf > 0.5: # 置信度阈值过滤
x_center = (x1 + x2) / 2
if x_center < midline:
left_count += 1
else:
right_count += 1
# 7. 更新LCD显示
lcd_string(f"L:{left_count:2d} R:{right_count:2d}", 0x80)
# 8. 根据计数控制信号灯(简易逻辑)
control_traffic_lights(left_count, right_count)
time.sleep(0.1) # 控制循环频率,避免CPU占用率100%
计数逻辑详解 :判断车辆属于左车道还是右车道,我采用了 检测框中心点 与 图像垂直中轴线 比较的方法。这是基于摄像头正对路口中央的假设。这种方法简单有效,避免了复杂的透视变换。在更复杂的场景中,可能需要预先标定一个透视变换矩阵,将图像坐标映射到真实的路面鸟瞰图坐标,再进行车道判断。
信号灯控制逻辑 :我实现了一个简单的 固定周期+需求感应 的混合逻辑。基础是一个固定的红绿灯切换周期(例如左绿30秒,右绿30秒)。但在每个周期内,系统会实时比较左右车道的车辆数量。如果某一侧排队车辆超过阈值(例如5辆),而当前是红灯,则会适当延长下一周期的绿灯时间,或插入一个全红相位后立即切换。这个逻辑在 control_traffic_lights 函数中实现,你可以根据自己的策略进行修改,比如实现完全自适应的感应控制。
6. 系统集成、调试与问题排查实录
将硬件和软件组装起来并让它们协同工作,是最考验耐心和细心的环节。
6.1 分模块调试法
不要试图一次性让所有功能跑通。务必采用分步调试:
- 硬件单体测试 :先写一个简单的Python脚本,单独测试每个LED是否能点亮、LCD是否能显示字符。确保所有物理连接正确。
- 网络通信测试 :在树莓派上运行一个简单的Socket客户端,尝试接收电脑发送的测试字符串,确保网络连通性和端口无误。
- 模型推理测试 :在树莓派上,用一张静态图片测试加载的ONNX模型能否正确推理并输出检测框。这一步可以排除模型格式或路径问题。
- 摄像头流测试 :单独运行服务端和客户端的图像传输部分,查看树莓派是否能稳定接收并显示视频流,检查是否有卡顿或延迟。
- 全流程集成 :最后将计数、显示、控制逻辑整合进主循环。
6.2 常见问题与解决方案速查表
在实际搭建中,我遇到了以下典型问题,并总结了排查思路:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 树莓派无法连接到电脑服务端 | 1. 防火墙阻止 2. IP地址错误 3. 服务端未启动 |
1. 在电脑上临时关闭防火墙或添加端口例外(如5000)。 2. 在电脑命令行输入 ipconfig (Windows)或 ifconfig (Linux/Mac)查看本地IP,确保树莓派连接的是此IP。 3. 检查服务端脚本是否已运行并无报错。 |
| 图像传输卡顿、延迟高 | 1. 网络带宽不足(WiFi干扰) 2. 图像分辨率或质量过高 3. 树莓派处理不过来 |
1. 尽量使用有线网络连接。若用WiFi,让设备靠近路由器。 2. 降低服务端摄像头采集分辨率(如320x240)和JPEG压缩质量(如70)。 3. 在客户端代码中增加 time.sleep(0.05) ,主动降低处理帧率。 |
| YOLO推理速度非常慢(<1 FPS) | 1. 使用了过大的模型(如yolov8x) 2. 未使用ONNX Runtime或TensorRT加速 3. 树莓派散热不佳,CPU降频 |
1. 换用更小的模型,如 yolov8n 或专门为边缘设备优化的 YOLOv8nano 。 2. 确保使用 model = YOLO('best.onnx') 加载ONNX模型,它默认会调用ONNX Runtime。 3. 为树莓派加装散热风扇或散热片,使用 vcgencmd measure_temp 监控温度。 |
| 车辆漏检或误检严重 | 1. 训练数据不足或缺乏多样性 2. 推理置信度阈值设置不当 3. 现场光照与训练数据差异大 |
1. 回顾数据集,补充在类似当前光照、角度下的图片重新训练。 2. 调整推理代码中的 conf 阈值(如从0.5调到0.6或0.4),在精度和召回率间权衡。 3. 改善现场照明条件,或增加训练数据中的光照增强幅度。 |
| GPIO控制LED不亮 | 1. 引脚编号模式错误 2. LED正负极接反 3. 未设置引脚为输出模式 4. 缺少限流电阻 |
1. 确认代码中 GPIO.setmode(GPIO.BCM) 与接线使用的BCM编号一致。 2. 用万用表或电池检查LED极性。 3. 确保在控制前执行了 GPIO.setup(pin, GPIO.OUT) 。 4. 务必串联一个220Ω电阻,否则可能烧毁LED或GPIO口。 |
| LCD屏幕无显示或乱码 | 1. I2C地址错误 2. 接线松动 3. I2C未启用 |
1. 使用命令 i2cdetect -y 1 扫描I2C设备,确认LCD的地址(通常是0x27或0x3F)。 2. 检查SDA、SCL、VCC、GND四根线是否接牢。 3. 在树莓派设置中启用I2C接口: sudo raspi-config -> Interface Options -> I2C -> Yes 。 |
6.3 性能优化小技巧
当系统基本跑通后,可以进一步优化体验:
- 使用多线程 :将图像接收、推理计算、硬件控制放在不同的线程中,避免因为推理耗时导致图像接收缓冲区堆积,从而减少整体延迟。
- 降低推理分辨率 :在
model.predict()中设置imgsz=320,虽然会损失一些对小物体的检测能力,但能大幅提升FPS。对于我们的玩具车场景,320分辨率可能已经足够。 - 硬件加速探索 :树莓派具有GPU和NPU(神经网络处理单元)。可以深入研究使用
libcamera替代OpenCV获取图像,并使用支持Vulkan后端的推理引擎(如TensorFlow Lite)来进一步挖掘硬件潜力。但这属于进阶内容,初期以跑通流程为主。
7. 项目总结与扩展思考
经过三周的折腾,这个微型智能交通系统原型终于活了起来。看着LCD屏上数字随着玩具车的移动而变化,信号灯依规则切换,那种将代码和算法转化为物理世界交互的成就感,是纯软件项目无法比拟的。这个项目的最大收获,不在于做出了一个多精妙的系统,而在于完整地实践了**“感知-决策-控制”** 这个在自动驾驶、智慧工厂等领域通用的范式。
如果你也想复现或在此基础上扩展,这里有几个方向值得尝试:
- 从模拟走向半真实 :尝试用真实的道路交叉口视频流(可以从公开数据集中获取)作为输入,让你的模型去检测真实的汽车、行人、自行车。这会立刻面临光照变化、尺度变化、目标密集等新挑战。
- 优化控制算法 :目前的信号灯控制逻辑非常朴素。可以引入更先进的算法,比如基于排队长度预测的优化,或者模仿强化学习(Reinforcement Learning)让系统自己学习最优的信号配时策略。
- 增加通信与云端 :让多个路口的树莓派节点通过MQTT等协议将车流数据上报到一个中央服务器(可以用Flask简单搭建),在服务器端进行区域协同的交通流分析,甚至实现简单的交通看板。
- 更换检测目标 :这套框架不止能检测车。你可以训练一个识别垃圾的模型,结合机械臂做成智能垃圾桶;或者训练一个识别特定工件的模型,用在简单的流水线分拣场景。思路是通用的。
最后,嵌入式AI项目的开发就像在螺蛳壳里做道场,处处是限制,但也处处是优化的乐趣。从数据集的精心构建,到模型训练时的参数调优,再到树莓派上每一毫秒的节省,整个过程迫使你深入理解每一个环节。希望这个详细的记录能为你点亮一盏灯,当你自己动手时,少走些弯路,多享受一些创造的快乐。
更多推荐



所有评论(0)