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网络摄像头完成。为了融入场景,我将其伪装成了一个交通锥。这里有几个关键细节:

  1. 摄像头固定 :我使用了一个小型三脚架,将其主体藏于锥筒内部,仅让云台螺丝从锥筒顶部预先开好的小孔穿出。这样既能稳定固定摄像头,又能灵活调整俯仰角度。
  2. 视角与高度 :摄像头的安装高度和角度决定了检测范围。我将其置于“路口”正上方约30厘米处,镜头略微向下俯视,以确保能覆盖整个“道路”区域(90x200cm的游戏垫)。在真实项目中,这对应着选择合适的路灯或龙门架安装位置。
  3. 照明考虑 :尽管是室内项目,均匀的光照对检测精度影响巨大。应避免单一强光源造成的反光和阴影。我使用了两个台灯从两侧补光,模拟阴天均匀的自然光,这能有效提升模型在不同“天气”下的鲁棒性。

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 分模块调试法

不要试图一次性让所有功能跑通。务必采用分步调试:

  1. 硬件单体测试 :先写一个简单的Python脚本,单独测试每个LED是否能点亮、LCD是否能显示字符。确保所有物理连接正确。
  2. 网络通信测试 :在树莓派上运行一个简单的Socket客户端,尝试接收电脑发送的测试字符串,确保网络连通性和端口无误。
  3. 模型推理测试 :在树莓派上,用一张静态图片测试加载的ONNX模型能否正确推理并输出检测框。这一步可以排除模型格式或路径问题。
  4. 摄像头流测试 :单独运行服务端和客户端的图像传输部分,查看树莓派是否能稳定接收并显示视频流,检查是否有卡顿或延迟。
  5. 全流程集成 :最后将计数、显示、控制逻辑整合进主循环。

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项目的开发就像在螺蛳壳里做道场,处处是限制,但也处处是优化的乐趣。从数据集的精心构建,到模型训练时的参数调优,再到树莓派上每一毫秒的节省,整个过程迫使你深入理解每一个环节。希望这个详细的记录能为你点亮一盏灯,当你自己动手时,少走些弯路,多享受一些创造的快乐。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐