树莓派与YOLOv5-Lite的那些事:配置、踩坑与部署
前言
项目设计想选个简单的,于是找了个 基于硬件的目标检测 。就YOLO嗯套呗,反正网上的例子一大把,下下来改改代码就行了吧。这不,百度搜搜就有:基于树莓派4B的YOLOv5-Lite目标检测的移植与部署(含训练教程)。稳辣!
我是这么想的。
结果发现,最新版的RaspiOS的教程很少,许多的配置界面都不一样,还遇到了各种各样的申必问题,只能自己摸索踩坑了。
环境配置
我喜欢找最新的镜像,自己配置环境。
老规矩开启三件套:WIFI、SSH、VNC。过程略。
之后安装各种依赖。需要注意的是,树莓派上面的Python包是固定死的,为了避免依赖冲突。安装包需要使用sudo apt install python3-opencv
的命令。
但是有些包没有,比如python3-onnxruntime
。在树莓派5 问题汇总 - 知乎找到了解决方法:
1 | sudo mv /usr/lib/python3.x/EXTERNALLY-MANAGED /usr/lib/python3.x/EXTERNALLY-MANAGED.bk |
其中python3.x
是你树莓派上的实际Python版本。
摄像头
我喜欢选最新的,于是烧录的时候选的是最新版的RpiOS。新版的树莓派系统并没有以前的Legacy Camera
配置,似乎默认已经是启用的了。但是在使用libcamera-hello
时并没有显示。输入vcgencmd get_camera
也显示supported=0 detected=0, libcamera interfaces=0
即没检测到任何摄像头。
在树莓派4B最新系统下安装树莓派官方摄像头和树莓派4B外接摄像头无法获取画面如何处理找到了解决方法:
-
修改
/boot/config.txt
(若为新版本则是/boot/firmware/config.txt
):修改原来的摄像头检测语句:1
2
3#camera_auto_detect=1
gpu_mem=128
start_x=1 -
修改
/etc/modules
:在最后面添加bcm2835-v4l2
,这是为了加载老驱动 -
重启树莓派
之后再次使用vcgencmd get_camera
查询会变成supported=1 detected=1, libcamera interfaces=0
,也就是正常识别到摄像头并且加载。
但是在CV2调用时又出现了问题,无法正常读取。查询相关资料得知,最新版的RpiOS在Bullseye版本后,底层的树莓派驱动从Raspicam切换到了libcamera。因此,我们使用官方的picamera2
库对摄像头进行操作:
-
sudo apt install -y python3-picamera2
-
改
/boot/config.txt
(若为新版本则是/boot/firmware/config.txt
):在最后根据摄像头型号添加语句:dtoverlay=ov5647
-
参考树莓派4B使用opencv获取Camera Module 3摄像头图像(解决无法直接获取图像的问题),封装一个函数来负责采集图像:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37#!/usr/bin/python
# Picamera2_Img_et.py
from picamera2 import Picamera2
from libcamera import controls
class Imget:
def __init__(self):
# 创建一个Picamera2对象的实例
self.cam = Picamera2()
# 设置相机预览的分辨率
# 调小一点可以显著提高帧率
self.cam.preview_configuration.main.size = (320, 320)
self.cam.preview_configuration.main.format = "RGB888"
# 设置预览帧率
self.cam.preview_configuration.controls.FrameRate = 10
# 对预览帧进行校准
self.cam.preview_configuration.align()
# 配置相机为预览模式
self.cam.configure("preview")
# 设置相机控制参数为连续对焦模式(自动对焦)
# 我使用的树莓派官方摄像头v1.3(ov5647)并不支持自动对焦
# self.cam.set_controls({"AfMode": controls.AfModeEnum.Continuous})
# 启动相机
self.cam.start()
def getImg(self):
# 获取相机捕获的图像数组(numpy数组)
frame = self.cam.capture_array()
# 返回捕获的图像数组
return frame
def __del__(self):
self.cam.stop()
self.cam.close()然后调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54import cv2
from threading import Thread
import os
import time
from Picamera2_Img_et import Imget # 导入Imget类
# 下面是老的图像采集函数,无法直接使用,需要换为上面封装的采集方式
def image_collect_old(cap):
global count
while True:
success, img = cap.read()
if success:
file_name = str(uuid.uuid4())+'.jpg'
cv2.imwrite(os.path.join('images',file_name),img)
count = count+1
print("save %d %s"%(count,file_name))
time.sleep(0.4)
# 新的图像采集函数
def image_collect_new(getImg):
global count
while True:
frame = getImg.getImg() # 使用Imget类获取图像
if frame is not None:
file_name = str(uuid.uuid4()) + '.jpg'
cv2.imwrite(os.path.join('images', file_name), frame)
count = count + 1
print("save %d %s" % (count, file_name))
time.sleep(0.4)
if __name__ == "__main__":
os.makedirs("images", exist_ok=True)
getImg = Imget() # 创建Imget对象实例
m_thread = Thread(target=image_collect, args=(getImg,), daemon=True)
while True:
frame = getImg.getImg() # 使用Imget类获取图像
if frame is not None:
cv2.imshow("video", frame)
key = cv2.waitKey(1) & 0xFF
# 按键 "c" 开始采集图像
if key == ord('c'):
m_thread.start()
continue
elif key == ord('q'):
break
cv2.destroyAllWindows()然后就可以正常采集图像辣!
采集、训练与推理
数据集采集与标注
使用上面写的img_collection.py
进行采集。嫌速度太快会拍到手的把0.4s的间隔调大一点就行。
我采集了100张,然后又随机选了四十张进行左旋转和右旋转(毕竟采集分辨率设置的是320x320
,可以加快训练速度和检测速度)。
标注的话,使用wkentaro/labelme进行标注。但是yolo并不认识,还需要转换一下格式。移植与部署里转换的脚本是有问题的,转换后的yolo坐标出现负值,会导致之后训练时提示Ignoring corrupted image and/or label
,训练时会自动跳过该图片导致数据集很小。把苹果识别成橘子的原因找到了
因此,使用labelme生成的标注数据转换成yolov5格式里提供的转换脚本:
1 | # -*- coding: utf-8 -*- |
这样子就行了。
训练开始!
修改一下train.py
:
1 | ... |
其中:
-
weights: 初始基准模型
-
cfg: 初始基准模型的配置文件,不用动
-
data: 训练的配置文件,内容如下:
1
2
3
4
5
6
7
8path: data/path
train: train/images
val: valid/images
test: test/images
nc: 3
names:
- your
- label -
epoch: 训练轮次
-
batch: 批处理量,推荐别设太大以免爆显存/内存
-
img-size: 你的数据源的图片尺寸
-
device: 训练使用设备。0代表默认CUDA设备。可以改成
cpu
以仅仅使用CPU训练。
采集了140张图片,划分比例9:1
。炼丹开始!
在配置环境时遇到了问题:明明卸载了电脑上的CUDA相关环境,执行nvidia-smi
查看信息,仍提示有CUDA 12.7
。这实际上是显卡驱动自带的CUDA,不用管。安装pytorch时选择适配CUDA12.6版本的nightly release就行。
显存还剩4G内存还剩24G怎么还能爆了我请问了
我也看不懂训练结果。
导出为ONNX
这里要参考YOLOv5-Lite (onnx)(v1.5版本 5月22日) 类似报错中的导出方法:
python .\export.py --weights runs\train\91\weights\best.pt --end2end
其中--end2end
是为了带上额外的后处理。
推理
移植与部署里的代码太老了,是之前的YOLOv5-Lite版本的。同样在类似报错中找到了解决方法:将最新版本的库中的 python_demo/onnxruntime/v5lite.py
内class yolov5_lite() {...}
粘贴到原本的推理代码中,然后修改一下就行了。代码如下。
1 | #!/usr/bin/python |
自己加入了墨水屏显示的代码。可以通过传入--eink
参数来决定是否启用。驱动部分参考了微雪的官方树莓派Python例程。
实机展示
适当修改上面confThreshold
与nmsThreshold
的值后问题解决。
在YOLO(You Only Look Once)目标检测算法中,confThreshold
和 nmsThreshold
是用于过滤预测结果的两个重要参数。
-
confThreshold
(置信度阈值):- 这个参数决定了一个检测框被保留的最小置信度。YOLO 模型会为每个预测的边界框输出一个置信度分数,这个分数表示模型对边界框内存在目标的确定程度。
- 如果一个边界框的置信度分数低于
confThreshold
,那么这个边界框会被丢弃,不会被认为是有效的检测结果。 - 设置较高的
confThreshold
可以减少误报(False Positives),但可能会导致一些真实的对象没有被检测到(即漏检)。
-
nmsThreshold
(非极大值抑制阈值):- NMS(Non-Maximum Suppression)是一种后处理技术,用来解决同一物体被多次检测的问题。当多个边界框重叠并指向同一个物体时,NMS 会选择其中具有最高置信度分数的那个,并删除其他重叠的边界框。
nmsThreshold
定义了两个边界框之间的重叠程度(通常使用 IoU,Intersection over Union,即交并比来衡量)。如果两个边界框的 IoU 超过了nmsThreshold
,则其中一个会被抑制(移除)。- 较低的
nmsThreshold
值意味着更严格的抑制条件,只有那些几乎不重叠或者重叠非常小的边界框才会被保留下来;而较高的nmsThreshold
可能会导致更多重叠的边界框被保留,这可能会增加冗余检测。
调整这两个阈值可以影响模型的精度和召回率。通常需要根据具体的应用场景进行调优。