1.项目介绍
1.1项目简介
本项目基于YOLOv8目标检测算法开发了行空板M10智能小车自动驾驶系统,通过部署ONNX格式模型可实现26类交通标志(含"禁止驶入"、"左转指示"、"右转指示")的实时检测。整个系统基于Python开发,在本项目中,当识别到摄像头画面中的"左转"、"右转"、"鸣笛"标志时,小车会自动执行对应的转向、鸣笛和灯光控制。
项目亮点在于完整实现了从图像采集→AI推理→硬件控制的全链条开发流程,使用多进程架构确保视觉识别(30FPS)与电机控制实时同步,特别适合初学者学习嵌入式AI开发、多进程编程以及硬件交互的完整实现过程。所有功能通过不到200行代码完成,代码中已包含详细的注释说明,即使没有深度学习背景也能快速理解运行原理。
1.2项目效果视频
【【YOLO+行空板】交通标志识别自动驾驶小车】
2.项目制作框架
本项目通过YOLO目标检测算法,使用交通标识数据集训练公共交通标志目标检测模型,实现视频流中交通标识的检测,显示标识类别标签,并根据检测结果控制小车的电机,led灯,喇叭等执行器的状态,实现小车的自动行驶。

3.软硬件环境准备
3.1软硬件器材清单
注意行空板M10固件在0.3.5——0.4.0的版本均可以用于制作本项目,但都需要为行空板装conda环境,详情见3.3硬件环境准备。
3.2软件环境准备
由于我们使用电脑训练水果检测模型,因此需要在电脑端安装相应的库。
首先按下win+R,输入cmd进入窗口。
在命令行窗口中依次输入以下指令,安装ultralytics库
pip install ultralytics
pip install onnx==1.16.1
pip install onnxruntime==1.17.1
输入之后会出现以下页面。
当命令运行完成,出现以下截图表示安装成功。
3.3硬件环境准备
在本项目中我们将要将训练好的YOLOv8框架的模型部署到行空板中,进行推理和执行操作。为了在行空板上成功运行YOLOv8,我们将使用Ultralytics官方提供的库进行部署 。
使用USB数据线连接行空板与电脑,等待行空板屏幕亮起表示行空板开机成功。
打开编程软件Mind+,点击左下角的扩展,在官方库中找到行空板库点击加载
点击返回,点击连接设备,找到10.1.2.3.点击连接,等待连接成功.
请参考此篇帖子的环境配置教程:如何在行空板上运行 YOLOv10n? 请按照这篇帖子的教程完成到Step 7 安装utralytics,如下图。
除了utralytics库本项目还需要使用onnx、onnxruntime、opencv -python 和pinpong库 依次在Mind+终端输入以下命令
(注意行空板需要联网,行空板联网操作教程:https://www.unihiker.com.cn/wiki/m10/webmenu#%E7%BD%91%E7%BB%9C%E8%AE%BE%E7%BD%AE)
pip install onnx==1.16.1
pip install onnxruntime==1.17.1
pip install opencv-python
pip install pinpong
在Mind+终端输入以下指令,可以检查相应库是否安装成功
pip list
4. 制作步骤
4.1数据集准备
为了训练交通标志检测模型,我们需要准备COCO格式的交通标志数据集。一个标准的目标检测数据集包括训练集和验证集,每个集都包含图像和标注文件(txt文件)。标注文件能提供目标的位置和类别信息。
标准的用于目标检测的数据集的格式如下。
我们使用的数据集总共包含26种不同类别的交通标志和场景如左转指示、鸣笛提醒、信号灯状态等。
traffic_signs_dataset/ //交通标识数据集
│
├── train/ // 训练集,使用训练集的数据进行模型训练
│ ├── images/ // 训练集的图片文件夹
│ └── labels/ // 训练集中图片的标注信息文件夹,作用是提供图像中目标的位置和类别信息。
│
├── val/ // 验证集,验证集用于评估模型在未见过的数据上的表现
│ ├── images/ // 验证集的图片文件夹
│ └── labels/ // 验证集图片的标注信息文件夹
│
└── data.yaml // 数据集配置文件,用于定义数据集的参数,通常是一个 YAML 文件。
其中YAML文件一个对于目标检测模型训练很重要的文件。YAML 文件通常包含数据集的路径信息,这些路径告诉模型训练脚本在哪里找到训练集和验证集的图像和标注文件。除此之外,YAML 文件定义了类别索引与类别名称之间的映射关系。这对于模型在训练和推理过程中正确识别和分类目标至关重要。如下图,是交通标志检测模型的YAML文件。
准备好数据集后,接下来我们就可以进入模型的训练环节了。
4.2模型训练
我们首先要去ultralytics的官方仓库下载YOLO项目文件。链接:https://github.com/ultralytics/ultralytics。如下图,将官方文件夹下载下来,并解压(文件夹已附在在本篇文档的最后)。
将文件放到一个能找到的路径,打开Mind+,选择Python模式下的代码模式,如下图。
在右侧“文件系统”中找到"电脑中的文件",找到此文件夹进行添加。
添加好后可以观察到如此下图。
点击"新建文件夹",在ultralytics文件夹中分别新建三个文件夹,依次命名为"datasets"(用于存放数据集),"yamls"(用于存放数据集对应的yaml文件)"runs"(用于存放训练模型的py文件)。
建好后如下图。
将水果数据集的训练集和验证集文件夹放入"dataset"文件夹,将水果数据集的yaml文件放入"yamls"文件夹。
接着我们在"runs"文件夹中建立一个叫做"train.py"的文件,在此文件中编写训练YOLO模型的代码。
将训练代码粘贴到train.py中点击运行。我们在预训练模型"yolov8s"基础上进行水果目标检测模型的训练,设置训练轮次是2轮。图片的尺寸是320。
from ultralytics import YOLO # 导入YOLO类,用于加载和训练模型
import time # 导入time模块,用于延迟操作
import os # 导入os模块,用于操作文件路径
# 打印当前工作目录,帮助确认代码运行的路径是否正确
#print("Current working directory:", os.getcwd())
# 确保路径正确
model = YOLO('yolov8s.pt') # 加载YOLOv8s预训练模型,yolov8s.pt是一个轻量级模型,适合资源有限的环境
# 开始训练模型
results = model.train(
data=os.path.join(os.getcwd(), '..', 'yamls', 'data.yaml'), # 数据配置文件路径
# 使用相对路径,确保指向正确的yamls文件夹,data.yaml 文件中定义了数据集的路径、类别数等信息
epochs=2, # 设置训练轮数为2(通常需要更多轮数,这里可能是测试)
imgsz=320, # 设置输入图像大小为320x320,降低图像大小可以减少显存占用,适合资源有限的环境
device='cpu', # 使用CPU进行训练(如果无GPU或CUDA环境)
# 如果有GPU,可以设置为 '0' 或 'cuda'
workers=0, # 数据加载线程数(Windows系统建议设置为0)
# workers=0 表示主进程加载数据,避免多线程问题
batch=2, # 设置批量大小为2,降低批量大小可以减少显存占用,但可能影响训练效率
cache=False # 不使用缓存(避免内存占用过多)
)
运行时可观察终端,自动下载预训练模型"yolov8s.pt",进行模型的训练。
YOLO模型的训练对电脑的配置要求比较高,使用电脑CPU一般训练时间较长,可以考虑使用GPU或者云端算力进行训练,这里我们使用的时本地电脑的CPU训练方法,操作比较简单,耗时略长。
当训练完成后,我们可以观察"runs"文件夹中自动生成了"detect"文件夹(代表了目标检测任务),里面存放了训练模型的数据和训练好的模型文件。
4.3模型转换
我们训练得到的"best.pt"可以直接用于推理,我们也可以将"best.pt"转成onnx格式的模型文件。ONNX 格式,也是一种模型文件的格式,更加通用,可以与各种推理引擎兼容,提供更高效的推理速度
使用以下代码将pt格式的模型文件转成onnx格式的模型文件。
from ultralytics import YOLO
# 模型路径,这里填你的pt文件的实际路径
model_path = r'C:\Users\li720\Downloads\ultralytics-main\ultralytics-main\runs\runs\detect\train5\weights\best.pt' # 训练完成后保存的模型路径
# 加载模型
model = YOLO(model_path)
# 导出为onnx格式
model.export(format='onnx')
print("模型已成功导出为onnx格式")
转换好后,可以在同一目录下找到转换好的onnx模型。
4.4电脑端模型测评
我们先在电脑端测试一下模型的性能,将训练好的模型拖入Mind+项目中的文件下
再在项目中的文件中新建一个叫做"img_inference.py"(用来测试图片推理效果),同时将测试图片"test.png"也拖入项目中的文件夹下。
测试图片如下图所示。
将以下代码复制到"img_inference.py"文件中进行图片推理测试。
from ultralytics import YOLO
import cv2
# 1. 加载预训练模型
model = YOLO('traffic_yolo_3xs_v9.onnx', task='detect') # 显式指定任务为检测
# 2. 进行图像推理(带类别统计)
def predict_image(image_path, conf=0.5):
# 读取图像并调整尺寸
img = cv2.imread(image_path)
img = cv2.resize(img, (320, 320))
# 执行推理
results = model.predict(
source=img,
conf=conf,
save=False,
imgsz=320
)
# 初始化类别计数器
class_counts = {}
# 遍历检测结果
for result in results:
# 获取检测到的所有类别
detected_classes = result.boxes.cls.tolist()
# 统计每个类别的出现次数
for class_id in detected_classes:
class_name = result.names[int(class_id)]
class_counts[class_name] = class_counts.get(class_name, 0) + 1
# 可视化结果
annotated_img = results[0].plot()
# 显示结果
cv2.imshow('Detection', annotated_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
return results, class_counts # 返回结果和统计信息
if __name__ == '__main__':
try:
# 执行检测并获取统计结果
image_results, counts = predict_image('test.png', conf=0.6)
# 打印检测统计
print("\n检测结果统计:")
for class_name, count in counts.items():
print(f"{class_name}: {count}个")
# 原始检测信息(可选)
print("\n详细检测信息:")
for result in image_results:
for box in result.boxes:
print(f"类别: {result.names[int(box.cls)]}, 置信度: {box.conf.item():.2f}")
except Exception as e:
print(f"发生错误: {str(e)}")
点击Mind+右上角的”运行",运行代码观察效果。
当程序运行时,弹出弹窗显示图片识别结果如下:
关闭弹窗后,观察终端的输出,可以看到终端出现了检测到的标志类别的统计信息。
4.5部署模型到行空板
接着我们可以部署模型到行空板中制作水果检测装置。使用USB数据线连接电脑与行空板。
连接行空板到小车上,连接摄像头于行空板,左右led灯与行空板(左转向灯连到p22,右转向灯连到p24)。
打开编程软件Mind+,点击左下角的扩展,在官方库中找到行空板库点击加载
点击返回,点击连接设备,找到10.1.2.3.点击连接,等待连接成功.
我们选中文件拖动到项目中的文件中。
4.6实时推理与控制小车
保持行空板的连接,在终端输入以下指令来激活行空板的yolo环境(请确保完成3.2硬件环境部署准备)。
conda activate yolo
新建一个yolo_car_inf.py文件,将以下代码复制到文件中进行实时视频推理与小车控制。
# 导入必要的库
import sys # 系统相关操作
import cv2 # OpenCV图像处理库
import numpy as np # 数学计算库
import onnxruntime as ort # ONNX模型推理库
import time # 时间相关操作
import collections # 数据结构工具
sys.path.append("/root/mindplus/.lib/thirdExtension/winster-maxbot_driver-thirdex") # 添加硬件驱动路径
# 导入硬件控制库
from pinpong.board import Board, Pin # 开发板控制
from pinpong.board import NeoPixel # LED灯带控制
import multiprocessing as mp # 多进程处理
from ctypes import c_int # C语言类型支持
from pinpong.extension.unihiker import * # UniHiker开发板扩展功能
# 定义YOLO目标检测类
class Traffic_YOLO_onnx_inf:
"""YOLOv8目标检测模型类,用于处理推理、可视化推理结果"""
def __init__(self, onnx_model, classes, camera_res, inf_res, camera=0, confidence_thres=0.7, iou_thres=0.45, predict_len = 10):
"""
初始化YOLOv8类的实例。
参数:
onnx_model: ONNX模型的路径。
classes: 可识别的类别名称列表(比如'红绿灯','停止标志')。
camera_res: 摄像头读取的分辨率(宽度,高度)。
inf_res: 推理时的图片分辨率,可以是整数或(宽度,高度)。
camera: 摄像头索引,默认为0。
confidence_thres: 置信度阈值,用于过滤检测结果。
iou_thres: IoU(交并比)阈值,用于非极大值抑制。
predict_len: 预测结果缓存长度(类似短期记忆的容量)
"""
self.onnx_model = onnx_model
self.camera = camera
self.confidence_thres = confidence_thres
self.iou_thres = iou_thres
self.camera_res = camera_res
self.inf_res = inf_res
self.fps_deque = collections.deque()
# 加载类别名称列表
self.classes = classes
# 为类别生成颜色调色板
self.color_palette = np.random.uniform(0, 255, size=(len(self.classes), 3))
# 初始化预测变量
self.prediction = collections.deque()
self.predict_len = predict_len
self.counts = {}
self.max_count = 0
self.max_numbers = set()
self.current_predict = -1
@staticmethod
def find_most_num(nums):
nums_array = np.array(nums)
unique, counts = np.unique(nums_array, return_counts=True)
max_count_index = np.argmax(counts)
max_num = unique[max_count_index]
return max_num
def add_prediction(self, class_id):
# 入队
self.prediction.append(class_id)
# 更新计数
self.counts[class_id] = self.counts.get(class_id, 0) + 1
count = self.counts[class_id]
# 更新最大计数和对应的数字集合
if count > self.max_count:
self.max_count = count
self.max_numbers = {class_id}
elif count == self.max_count:
self.max_numbers.add(class_id)
# 出队(如果超过最大长度)
if len(self.prediction) > self.predict_len:
removed = self.prediction.popleft()
self.counts[removed] -= 1
removed_count = self.counts[removed]
if removed_count + 1 == self.max_count:
if removed_count < self.max_count:
if removed in self.max_numbers:
self.max_numbers.remove(removed)
if not self.max_numbers:
# 重新计算 max_count 和 max_numbers
self.max_count = max(self.counts.values(), default=0)
self.max_numbers = {class_id for class_id, cnt in self.counts.items() if cnt == self.max_count}
def draw_detections(self, img, box, score, class_id):
"""
在输入图像上绘制边界框和标签。
参数:
img: 要绘制检测结果的输入图像。
box: 检测到的边界框。
score: 对应的检测得分。
class_id: 检测到的对象的类别ID。
"""
# 提取边界框的坐标
x1, y1, w, h = box
# 获取类别ID对应的颜色
color = self.color_palette[class_id]
# 在图像上绘制边界框
cv2.rectangle(img, (int(x1), int(y1)), (int(x1 + w), int(y1 + h)), color, 2)
# 创建包含类别名称和得分的标签文本
label = f"{self.classes[class_id]}: {score:.2f}"
# 计算标签文本的尺寸
(label_width, label_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
# 计算标签文本的位置
label_x = x1
label_y = y1 - 10 if y1 - 10 > label_height else y1 + 10
# 绘制填充的矩形作为标签文本的背景
cv2.rectangle(
img,
(int(label_x), int(label_y - label_height)),
(int(label_x + label_width), int(label_y + label_height)),
color,
cv2.FILLED
)
# 在图像上绘制标签文本
cv2.putText(img, label, (int(label_x), int(label_y)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
def preprocess(self, img):
"""
在执行推理之前对输入图像进行预处理。
参数:
img: 输入的图像。
返回:
image_data: 预处理后的图像数据,准备进行推理。
"""
# 将图像从BGR转换为RGB颜色空间
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 调整图像大小以匹配输入形状
img = cv2.resize(img, (self.input_width, self.input_height), interpolation=cv2.INTER_NEAREST)
# 归一化图像数据
image_data = np.array(img) / 255.0
# 调整通道顺序,将通道维度放在第一个维度
image_data = np.transpose(image_data, (2, 0, 1)) # 通道优先
# 扩展图像数据的维度以匹配预期的输入形状
image_data = np.expand_dims(image_data, axis=0).astype(np.float32)
# 返回预处理后的图像数据
return image_data
def postprocess(self, input_image, outputs):
# 对模型输出进行压缩并转置
outputs = np.transpose(np.squeeze(outputs[0]))
# 获取类别得分和类别编号
confidences = outputs[:, 4:]
class_ids = np.argmax(confidences, axis=1)
scores = np.max(confidences, axis=1)
# 筛选得分高于阈值的检测结果
mask = scores >= self.confidence_thres
boxes = outputs[mask, :4]
scores = scores[mask]
class_ids = class_ids[mask]
x_factor = self.img_width / self.input_width
y_factor = self.img_height / self.input_height
# 计算边界框,并转换为整数形式
boxes[:, 0] = (boxes[:, 0] - boxes[:, 2] / 2) * x_factor # x1
boxes[:, 1] = (boxes[:, 1] - boxes[:, 3] / 2) * y_factor # y1
boxes[:, 2] = boxes[:, 2] * x_factor # w
boxes[:, 3] = boxes[:, 3] * y_factor # h
boxes = boxes.astype(np.int32)
# 将边界框和得分转换为列表形式
boxes_list = boxes.tolist()
scores_list = scores.tolist()
# 应用非极大值抑制(NMS)
indices = cv2.dnn.NMSBoxes(
boxes_list, scores_list, self.confidence_thres, self.iou_thres
)
# 处理 NMS 返回的索引
if len(indices) > 0:
indices = indices.flatten() # 将索引转换为一维数组
for i in indices:
box = boxes_list[i]
score = scores_list[i]
class_id = class_ids[i]
# 在输入图像上绘制检测结果
self.draw_detections(input_image, box, score, class_id)
if len(class_ids) == 0:
return input_image, -1
nums = class_ids.astype(np.int8)
return input_image, self.find_most_num(nums)
def get_prediction_window_max(self):
return list(self.max_numbers)[-1]
def run(self, predict, event, win = True):
"""
运行实时目标检测,使用摄像头捕获视频流,进行推理并显示结果。
"""
# 创建ONNX推理会话,指定执行提供者
session = ort.InferenceSession(self.onnx_model)
# 打开摄像头,并设置分辨率
cap = cv2.VideoCapture(self.camera)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.camera_res[0])
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.camera_res[1])
# 设置摄像头帧率为30 FPS
cap.set(cv2.CAP_PROP_FPS, 30)
cv2.namedWindow("Traffic_Yolo Detection", cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty("Traffic_Yolo Detection", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
# 处理推理分辨率
if isinstance(self.inf_res, int):
# 如果inf_res是整数,设置输入宽度和高度为该值
self.input_width = self.input_height = self.inf_res
else:
# 如果inf_res是元组,分别设置输入宽度和高度
self.input_width = self.inf_res[0]
self.input_height = self.inf_res[1]
# 初始化用于计算FPS的变量
prev_time = 0
while True:
# 读取摄像头帧
ret, frame = cap.read()
if not ret:
break
# 获取原始图像的高度和宽度
self.img_height, self.img_width = frame.shape[:2]
# 预处理帧
image_data = self.preprocess(frame)
# 执行推理
input_name = session.get_inputs()[0].name
output = session.run(None, {input_name: image_data})
# 后处理,绘制检测结果
result_image, predict_id = self.postprocess(frame, output)
if win:
self.add_prediction(predict_id)
win_max_id = self.get_prediction_window_max()
predict.value = win_max_id
else:
predict.value = predict_id
# 计算FPS
curr_time = time.time()
fps = 1 / (curr_time - prev_time)
self.fps_deque.append(fps)
if len(self.fps_deque) > 30:
self.fps_deque.popleft()
fps = sum(self.fps_deque) / len(self.fps_deque)
prev_time = curr_time
# 显示结果图像
display_img = cv2.resize(result_image, (240, 320)) # 根据实际屏幕尺寸调整
cv2.imshow("Traffic_Yolo Detection", display_img)
if predict.value != self.current_predict:
self.current_predict = predict.value
event.set()
# 按'a'键退出
if cv2.waitKey(1) & 0xFF == ord('a'):
break
# 释放摄像头并关闭所有窗口
cap.release()
cv2.destroyAllWindows()
def car_control(yolo_predict, event):
"""小车控制模块(AI的四肢)"""
Board().begin() # 初始化开发板
# 初始化电机和LED引脚
pin_ml_d = Pin(Pin.P5, Pin.OUT) # 左电机方向
pin_ml_a = Pin(Pin.P8, Pin.PWM) # 左电机速度
pin_mr_d = Pin(Pin.P6, Pin.OUT) # 右电机方向
pin_mr_a = Pin(Pin.P16, Pin.PWM) # 右电机速度
np_l = Pin(Pin.P22, Pin.OUT) # 左侧LED
np_r = Pin(Pin.P24, Pin.OUT) # 右侧LED
while True:
event.wait() # 等待检测到新结果
predict = yolo_predict.value
# 根据预测结果执行动作
if predict == 20: # 左转
# 设置电机转动方向和速度
pin_ml_d.write_digital(0)
pin_mr_d.write_digital(1)
pin_mr_a.write_analog(400)
pin_ml_a.write_analog(400)
np_l.write_digital(1) # 开启左侧LED
time.sleep(0.5)
# 停止电机和LED
pin_mr_a.write_analog(0)
pin_ml_a.write_analog(0)
np_l.write_digital(0)
elif predict == 21: # 右转(同上,方向相反)
pin_ml_d.write_digital(1)
pin_mr_d.write_digital(0)
pin_ml_a.write_analog(400)
pin_mr_a.write_analog(400)
np_r.write_digital(1)
print("右转")
time.sleep(0.5)
pin_ml_a.write_analog(0)
pin_mr_a.write_analog(0)
np_r.write_digital(0)
elif predict == 22: # 停止并鸣笛
pin_ml_a.write_analog(0)
pin_mr_a.write_analog(0)
buzzer.pitch(131,4) # 触发蜂鸣器
else: # 默认停止
pin_ml_a.write_analog(0)
pin_mr_a.write_analog(0)
np_l.write_digital(0)
np_r.write_digital(0)
event.clear()
if __name__ == "__main__":
# 初始化多进程通信
predict = mp.Value('i', -1) # 共享整型变量
event = mp.Event() # 进程间事件通知
# 类别定义(模型能识别的26种物体)
classes = [
'block_green', 'block_purple', 'block_red', 'block_yellow', 'cone',
'hand_0', 'hand_1', 'hand_2', 'hand_3', 'hand_4', 'hand_5',
'people_man', 'people_police', 'people_woman',
'sign_leftbypass', 'sign_rightbypass', 'sign_round', 'sign_speedlimit',
'sign_speedunlimit', 'sign_stop', 'sign_turnleft', 'sign_turnright',
'sign_whistle', 'traffic_green', 'traffic_none', 'traffic_red'
]
model ="traffic_yolo_3xs_v9.onnx"
camera_res = (240, 320) # 摄像头读取的分辨率
inf_res = 128 # 推理时的图片分辨率
# 创建YOLOv8类的实例
detection = Traffic_YOLO_onnx_inf(model, classes, camera_res, inf_res, camera=0, confidence_thres=0.5, iou_thres=0.45)
# 创建进程
detection_process = mp.Process(target= detection.run, args=(predict, event, False))
control_process = mp.Process(target=car_control, args=(predict, event))
# 启动进程
detection_process.start()
control_process.start()
# 等待进程完成
detection_process.join()
control_process.join()
点击右上角运行按钮,运行yolo_car_inf.py文件,查看推理和自动驾驶效果。
4.7核心代码解析
我们一起来看一下在行空板上运行的 'yolo_car_inf.py' 代码的核心功能代码。
1.模型推理模块
视觉感知模块
class Traffic_YOLO_onnx_inf:
def __init__(self, onnx_model, classes, camera_res, inf_res):
# 硬件初始化
self.camera_res = (240, 320) # 摄像头采集分辨率(宽×高)
self.inf_res = 128 # 图片输入尺寸(像素)
self.confidence_thres = 0.5 # 置信度阈值(仅输出概率>50%的检测结果)
self.classes = classes # 26类交通标志标签
def preprocess(self, img):
# 图像标准化流程
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # OpenCV BGR转RGB
img = cv2.resize(img, (128, 128)) # 统一缩放至模型输入尺寸
img = img.astype(np.float32) / 255.0 # 归一化至[0,1]区间
return np.transpose(img, (2, 0, 1)) # 维度转换(HWC→CHW)
def postprocess(self, outputs):
# 模型输出解码
boxes = outputs[:, :4] # 边界框坐标(x_center,y_center,width,height)
scores = outputs[:, 4] # 置信度得分
class_ids = np.argmax(outputs[:,5:], axis=1) # 类别预测
# 非极大值抑制(NMS)
indices = cv2.dnn.NMSBoxes(boxes, scores,
self.confidence_thres, self.iou_thres)
return [boxes[i] for i in indices], [class_ids[i] for i in indices]
技术要点:
①输入适配 :将摄像头原始图像(240×320)缩放至模型训练尺寸(128×128)
②归一化处理 :将像素值从0-255映射到0-1区间,提升模型收敛稳定性
③NMS优化 :通过交并比(IoU=0.45)过滤重叠检测框,保留最优结果
预测结果稳定性优化
# 滑动窗口统计(10帧历史记录)
self.prediction_window = collections.deque(maxlen=10)
def update_prediction(self, class_id):
# 多数表决机制
self.prediction_window.append(class_id)
counter = collections.Counter(self.prediction_window)
return counter.most_common(1)[0][0] # 返回最高频次类别
技术要点:
时间维度滤波 :基于10帧历史数据消除单帧误检
模式统计 :采用多数表决机制确定最终输出类别
实时更新 :队列结构实现先进先出(FIFO)更新策略
2.控制执行模块
def car_control(yolo_predict, event):
"""小车控制模块(AI的四肢)"""
Board().begin() # 初始化开发板
# 初始化电机和LED引脚
pin_ml_d = Pin(Pin.P5, Pin.OUT) # 左电机方向
pin_ml_a = Pin(Pin.P8, Pin.PWM) # 左电机速度
pin_mr_d = Pin(Pin.P6, Pin.OUT) # 右电机方向
pin_mr_a = Pin(Pin.P16, Pin.PWM) # 右电机速度
np_l = Pin(Pin.P22, Pin.OUT) # 左侧LED
np_r = Pin(Pin.P24, Pin.OUT) # 右侧LED
while True:
event.wait() # 等待检测到新结果
predict = yolo_predict.value
# 根据预测结果执行动作
if predict == 20: # 左转
# 设置电机转动方向和速度
pin_ml_d.write_digital(0)
pin_mr_d.write_digital(1)
pin_mr_a.write_analog(400)
pin_ml_a.write_analog(400)
np_l.write_digital(1) # 开启左侧LED
time.sleep(0.5)
# 停止电机和LED
pin_mr_a.write_analog(0)
pin_ml_a.write_analog(0)
np_l.write_digital(0)
elif predict == 21: # 右转(同上,方向相反)
pin_ml_d.write_digital(1)
pin_mr_d.write_digital(0)
pin_ml_a.write_analog(400)
pin_mr_a.write_analog(400)
np_r.write_digital(1)
print("右转")
time.sleep(0.5)
pin_ml_a.write_analog(0)
pin_mr_a.write_analog(0)
np_r.write_digital(0)
elif predict == 22: # 停止并鸣笛
pin_ml_a.write_analog(0)
pin_mr_a.write_analog(0)
buzzer.pitch(131,4) # 触发蜂鸣器
else: # 默认停止
pin_ml_a.write_analog(0)
pin_mr_a.write_analog(0)
np_l.write_digital(0)
np_r.write_digital(0)
event.clear()
通过GPIO控制直流电机驱动(PWM频率400Hz)
对应检测结果执行动作:
类别20(sign_turnleft):左轮反转实现左转,激活左侧LED
类别21(sign_turnright):右轮反转实现右转,激活右侧LED
类别22(sign_whistle):触发蜂鸣器警报(频率131Hz持续4秒)
3.双进程运行
采用事件驱动机制,通过共享内存变量实现进程间通信
# 进程间通信(IPC)
shared_predict = multiprocessing.Value('i', -1) # 共享整型变量
event_flag = multiprocessing.Event() # 事件同步信号
# 视觉检测进程
def detection_process():
while True:
frame = camera.capture()
result = model.predict(frame)
with shared_predict.get_lock():
shared_predict.value = result # 更新共享变量
event_flag.set() # 触发事件
# 控制执行进程
def control_process():
while True:
event_flag.wait() # 阻塞等待事件
execute_control(shared_predict.value)
event_flag.clear() # 重置事件
技术要点:
多进程隔离 :视觉处理(CPU密集型)与硬件控制(I/O密集型)解耦
共享内存 :通过Value实现跨进程数据传递(预测结果)
事件驱动 :采用Event信号实现精准时序控制
5.项目相关资料附录

见网盘链接: https://pan.baidu.com/s/1D4Q3_IJhKfE_csHuq4ve6g?pwd=aiw5 提取码: aiw5
6.新手常见疑问解答
Q:为什么要用两个进程?
A:就像开车时不能一边看导航一边踩油门,分成两个独立模块可以让:
图像识别更专注,不会因为控制延迟漏看标志
运动控制更及时,收到指令立即响应
Q:confidence_thres参数有什么用?
A:相当于AI的"自信度门槛"。设置0.5表示只有当AI有50%以上把握认为看到的是真标志才会采取行动。如果经常漏检可以降低到0.3,如果误检多就提高到0.7。
Q:PWM值400是什么意思?
A:这是控制电机转速的"油门深度",就像开车时轻踩油门,调低这个值会让转弯更柔和。
评论