前言:本文为手把手教学树莓派4B项目——YOLOv5-Lite目标检测,本次项目采用树莓派4B(Cortex-A72)作为核心 CPU 进行部署。该篇博客算是深度学习理论的初步实战,选择的网络模型为 YOLOv5 模型的变种 YOLOv5-Lite 模型。YOLOv5-Lite 与 YOLOv5 相比虽然牺牲了部分网络模型精度,但是缺极大的提升了模型的推理速度,该模型属性将更适合实战部署使用。该项目的实践将帮助大家成功进入 “嵌入式AI” 领域,后续将在该项目上加入嵌入式的 “传统控制” 属性,读者朋友可以期待一下!(文末有代码开源!)
硬件实物图:
效果图:
一、YOLOv5-Lite概述
1.1 YOLOv5概述
YOLOv5 网络模型算是 YOLO 系列迭代后特别经典的一代网络模型,作者为:Glenn Jocher。部分学者可能认为YOlOv5的创新性不足,其是否称得上 YOLOv5 而议论纷纷。作者认为 YOLOv5 可以算是对 YOLO 系列之前的一次集大成者的总结和突破,其属于非常优秀经典的网络模型框架,各种网络结构和 trick 是非常值得借鉴的!
代码地址:ultralytics/yolov5: YOLOv5 🚀 in PyTorch > ONNX > CoreML > TFLite (github.com)
Yolov5 官方代码中,给出的目标检测网络中一共有4个版本,分别是Yolov5s、Yolov5m、Yolov5l、Yolov5x四个模型。作者仅以 Yolov5s 的网络结构为对象进行讲解,其他版本的读者朋友可以参考其他博客!
Yolov5s 网络是 Yolov5 系列中深度最小,特征图的宽度最小的网络。后面的3种都是在此基础上不断加深,不断加宽。Yolov5 的网络结构图如下(源于江大白大佬的结构图):
上图即 Yolov5 的网络结构图,可以看出,还是分为输入端、Backbone、Neck、Prediction四个部分。
(1)输入端:Mosaic数据增强、自适应锚框计算、自适应图片缩放
(2)Backbone:Focus结构,CSP结构
(3)Neck:FPN+PAN结构
(4)Prediction:GIOU_Loss
上述四部分都是属于如今很常见的模块与Trick了,受限于博客篇幅,各部分的详解就不与读者朋友好好分析和交流了。建议对 YOLO 系列陌生的朋友可以去好好看看其他博主的博客亦或是去B站看视频教学!
下面丢上 Yolov5 作者的算法性能测试图:
到现在为止,Yolov5 已经更新迭代到 v7.0 版本了,科研学术圈以 Yolov5 为基础框架进行魔改的论文数不胜数。通过上述作者的概述读者朋友可能对 Yolov5 有了一个大致的了解,不难发现 Yolov5 是非常优秀的神经网络模型。
可考虑到实际情况,部署的本地机器通常并没有 PC 端那么计算能力强劲。这时候为了整体目标检测系统的稳定运行,往往需要牺牲掉网络模型的精度以换取足够快的检测速度。因此,轻量化部署的网络模型结构就此孕育而生!
1.2 YOLOv5-Lite详解
Yolov5-Lite 网络模型作为轻量化部署网络的代表作之一,深受算法部署工程师的偏爱(作者为中国ppogg大佬)!
Yolov5-Lite地址:GitHub - ppogg/YOLOv5-Lite: 🍅🍅🍅YOLOv5-Lite: lighter, faster and easier to deploy. Evolved from yolov5 and the size of model is only 930+kb (int8) and 1.7M (fp16). It can reach 10+ FPS on the Raspberry Pi 4B when the input size is 320×320~
Yolov5-Lite 算法的模型结构如图下。该算法去除了 Focus 结构层,减小了模型体量,使模型变得更为轻便;同时,去除了 4 次 slice 操作,减少了对计算机芯片缓存的占用,降低了计算机的处理负担。与 Yolov5 算法相比,Yolov5-Lite 算法能避免反复使用 C3 Layer 模块。C3 Layer 模块会占用计算机很多运行空间,从而降低计算机的处理速度。这种方式能使 Yolov5-Lite 算法模型的精度控制在可靠范围内,从而使其更易部署。
在图像识别领域,主干网络结构(Backbone)和检测头(Head)中往往有一段中间层,即特征增强融合网络层(Neck),可更精准地提取融合特征。Yolov5-Lite 算法也采用 FPN+PAN 结构,但其对输出端(Head)进行了通道剪枝,改进了 YOLOv4 算法和 YOLOv5 算法中的 FPN+PAN 的结构,具体表现在以下2个方面:
(1)、与 YOLOv5 算法不同,Yolov5-Lite 算法自身各结构的通道数量相同,即模型特征增强网络通道网格数也是20×20×96,这样可优化对内存的访问和使用,提高模型的运行效率;
(2)、Yolov5-Lite 算法采用 PANet 结构,将 YOLOv5 算法的通道连接(cat)操作改进为叠加操作,这样可进一步优化对内存的使用,加快处理速度。如下图为 Yolov5-Lite 算法与 YOLOv4 和 YOLOv5 算法中的 PAN 结构对比:
作者总结:
Yolov5-Lite 网络模型源于 YOLOv5 模型,随处可以可见 YOLOv5 网络模型的影子。但是,出于移植部署的目的性,Yolov5-Lite 网络将工作重心放在如何进行快速推理,如何轻量化网络模型大小!
Yolov5-Lite 网络模型不仅通过直接改变网络模型的结构,偏向多使用计算量小的网络模型结构去提取和融合目标特征,同时侧重运用计算机的运行机制:通过降低计算机内存的存储和读取去变相提高网络推理速度!!!
作者这里仅对 Yolov5-Lite 做初步概述,详情读者朋友可以参考学术论文!
二、YOLOv5-Lite训练
2.1 数据集制作
★常规的神经网络模型训练是需要收集到大量语义丰富的数据集进行训练的。但是考虑实际工程下可能仅需要对已知场地且固定实物进行目标检测追踪等任务,这个时候我们可以采取偷懒的下方作者使用的方法!
1、作者使用树莓派4B的 Camera 直接在捕获需要识别目标物的图片信息(捕获期间转动待识别的目标物体);
树莓派4B的 Camera 定时捕获照片的python代码如下:
import cv2
from threading import Thread
import uuid
import os
import time
count = 0
def image_collect(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)
if __name__ == "__main__":
os.makedirs("images",exist_ok=True)
# 打开摄像头
cap = cv2.VideoCapture(0)
m_thread = Thread(target=image_collect, args=([cap]),daemon=True)
while True:
# 读取一帧图像
success, img = cap.read()
if not success:
continue
cv2.imshow("video",img)
key = cv2.waitKey(1) & 0xFF
# 按键 "q" 退出
if key == ord('c'):
m_thread.start()
continue
elif key == ord('q'):
break
cap.release()
按动 “c” 开始采集待识别目标图像,按动 “q” 退出摄像头 Camera 的图片采集;
2、将捕获到的待识别目标物照片传输到PC端,利用 Labelme 软件进行标注(Labelme不会使用的建议相关博客);
作者的标注了 3 类目标:drug,prime,glue;读者朋友可以根据自己实际情况标注自己需要的数据集!由于我们标注的数据的标签 label 默认是 JSON 格式的不能被 YOLO 系列的神经网络模型直接进行利用训练。
3、使用 JSON 转 txt 的 YOLO 格式 label 的python代码进行转换(可以直接使用作者提供的代码):
dic_lab.py:
dic_labels= {'drug':0,
'glue':1,
'prime':2,
'path_json':'labels',
'ratio':0.9}
lablemetoyolo.py:
import os
import json
import random
import base64
import shutil
import argparse
from pathlib import Path
from glob import glob
from dic_lab import dic_labels
def generate_labels(dic_labs):
path_input_json = dic_labels['path_json']
ratio = dic_labs['ratio']
for index, labelme_annotation_path in enumerate(glob(f'{path_input_json}/*.json')):
# 读取文件名
image_id = os.path.basename(labelme_annotation_path).rstrip('.json')
# 计算是train 还是 valid
train_or_valid = 'train' if random.random() < ratio else 'valid'
# 读取labelme格式的json文件
labelme_annotation_file = open(labelme_annotation_path, 'r')
labelme_annotation = json.load(labelme_annotation_file)
# yolo 格式的 lables
yolo_annotation_path = os.path.join(train_or_valid, 'labels',image_id + '.txt')
yolo_annotation_file = open(yolo_annotation_path, 'w')
# yolo 格式的图像保存
yolo_image = base64.decodebytes(labelme_annotation['imageData'].encode())
yolo_image_path = os.path.join(train_or_valid, 'images', image_id + '.jpg')
yolo_image_file = open(yolo_image_path, 'wb')
yolo_image_file.write(yolo_image)
yolo_image_file.close()
# 获取位置信息
for shape in labelme_annotation['shapes']:
if shape['shape_type'] != 'rectangle':
print(
f'Invalid type `{shape["shape_type"]}` in annotation `annotation_path`')
continue
points = shape['points']
scale_width = 1.0 / labelme_annotation['imageWidth']
scale_height = 1.0 / labelme_annotation['imageHeight']
width = (points[1][0] - points[0][0]) * scale_width
height = (points[1][1] - points[0][1]) * scale_height
x = ((points[1][0] + points[0][0]) / 2) * scale_width
y = ((points[1][1] + points[0][1]) / 2) * scale_height
object_class = dic_labels[shape['label']]
yolo_annotation_file.write(f'{object_class} {x} {y} {width} {height}\n')
yolo_annotation_file.close()
print("creat lab %d : %s"%(index,image_id))
if __name__ == "__main__":
os.makedirs(os.path.join("train",'images'),exist_ok=True)
os.makedirs(os.path.join("train",'labels'),exist_ok=True)
os.makedirs(os.path.join("valid",'images'),exist_ok=True)
os.makedirs(os.path.join("valid",'labels'),exist_ok=True)
generate_labels(dic_labels)
我们需要根据自己的需要自定义字典 dic_lab,字典中的 ratio = 0.9 的作用是将数据集拆分成训练集和验证集 9:1。读者朋友可以根据自己的实际情况去修改字典的标签内容,成功执行 lablemetoyolo.py 代码后效果如下:
labels文件夹下的标签成功转换了 YOLO 系列可以使用的 label 标签,到此时就已经成功准备好我们需要的训练集了!
特别说明:该方法仅适用于上述作者所说的场景下,实际情况下,建议大家还是使用合格的数据集进行训练(即目标与背景语义丰富的数据集),使得训练出来的神经网络具有良好的泛化性与鲁棒性,否则训练出来的网络很容易过拟合!
2.2 YOLOv5-Lite训练
Yolov5-Lite 训练就是常规的神经网络模型训练,我们从 GitHub 上下载 Yolov5-Lite 的源代码,训练平台为:PyCharm 2020.1 x64,GPU:RTX3060 6G,CPU:AMD Ryzen 7 5800H 3.2GHZ。
读者朋友可以使用 PyCharm 或者 VsCode 打开 Yolov5-Lite 的源码(作者使用PyCharm 2020.1 x64);
在 Yolov5-Lite 的目录下找到 train.py (训练文件)的 main 函数入口,进行如下配置:
我们设置如下几个核心配置:
--weights v5lite-s.pt
--cfg models/v5Lite-s.yaml
--data data/mydata.yaml
--img-size 320
--batch-size 16
--data data/mydata.yaml
device 0/cpu (可以不使用CUDA训练)
读者朋友一定要将数据集存放的地址位置搞正确!!!
mydata.yaml:
Yolov5-Lite 网络模型的训练可以不一定必须使用 CUDA 进行加速,但是 pytorch 架构等依赖库一定需要满足,模型训练依赖要求如下:
# base ----------------------------------------
matplotlib>=3.2.2
numpy>=1.18.5
opencv-python>=4.1.2
Pillow
PyYAML>=5.3.1
scipy>=1.4.1
torch>=1.8.0
torchvision>=0.9.0
tqdm>=4.41.0
# logging -------------------------------------
tensorboard>=2.4.1
# wandb
# plotting ------------------------------------
seaborn>=0.11.0
pandas
# export --------------------------------------
# coremltools>=4.1
# onnx>=1.9.1
# scikit-learn==0.19.2 # for coreml quantization
# extras --------------------------------------
thop # FLOPS computation
pycocotools>=2.0 # COCO mAP
将训练环境与数据集都搞定之后,就可以点击运行按钮进行 Yolov5-Lite 的模型训练了!
训练成功之后,将会在当前目录下的 run 文件下的 trian 文件下找到 expx (x代表数字),expx 则存放了第 x 次训练时候的各种数据内容,包括:历史最优权重best_weight,当前权重last_weight,训练结果result等等;
三、树莓派4B部署YOLOv5-Lite
树莓派4B运行 Yolov5-Lite 网络模型进行目标检测需要依赖 OpenCV 等视觉Lib,读者朋友可以直接使用作者第一篇博客的配置。
博客地址:http://t.csdn.cn/jbHQm
3.1 ONNX概述
Open Neural Network Exchange(ONNX)是一个开放的生态系统,它使人工智能开发人员在推进项目时选择合适的工具,不用被框架或者生态系统所束缚。ONNX支持不同框架之间的互操作性,简化从研究到生产之间的道路。ONNX支持许多框架(TensorFlow, Pytorch, Keras, MxNet, MATLAB等等),这些框架中的模型都可以导出或者转换为标准ONNX格式。模型采用ONNX格式后,就可在各种平台和设备上运行。
开发者根据深度学习框架优劣选择某个框架,但是这些框架适应不同的开发阶段,由于必须进行转换,从而导致了研究和生产之间的重大延迟。ONNX格式一个通用的IR,能够使得开发人员在开发或者部署的任何阶段选择最适合他们项目的框架。ONNX通过提供计算图的通用表示,帮助开发人员为他们的任务选择合适的框架。
ONNX可视化:ONNX 模型可以通过 netron 进行可视化。
作者总结:
ONNX 顾名思义就是开放的神经网络模型转换,利用它可以轻松将模型更换框架,从而适配亦或是部署在各类设备上。
3.2 ONNX模型转换和移植
如今的开源 YOLO 系列神经网络模型的目录下作者都会预留 export.py 文件将该神经网络模型进行转换到 ONNX 模型,方便大家实际情况下部署使用!
将我们训练好的最优训练权重 weights 存放到 YOLOv5-Lite 主目录下,之后运行如下代码:
python export.py --weights best.pt
运行该指令后将会通过 best.pt 文件,生成 best.onnx 的 ONNX 模型的权重文件;
同时为了成功运行 ONNX 格式的 YOLOv5-Lite 网络模型,需要在树莓派4B中安装 onnxruntim (可以直接使用作者提供的安装包),当然值得注意的是 onnxruntim 的安装需要依赖的 Numpy版本1.21 以上(这点需要大家注意,当然如果使用了作者的镜像完全没有问题!)。
pip install onnx (tab补全安装包)
将转换成 ONNX 格式的权重文件导入到树莓派4B中,并于目标检测程序保持同一目录下:
到此 ONNX 模型的转换与移植工作就可以完成了!
四、YOLOv5-Lite目标检测
YOLOv5-Lite 的目标检测前向推理程序是很简单的,可以直接借鉴作者如下提供的代码:
import cv2
import numpy as np
import onnxruntime as ort
import time
def plot_one_box(x, img, color=None, label=None, line_thickness=None):
"""
description: Plots one bounding box on image img,
this function comes from YoLov5 project.
param:
x: a box likes [x1,y1,x2,y2]
img: a opencv image object
color: color to draw rectangle, such as (0,255,0)
label: str
line_thickness: int
return:
no return
"""
tl = (
line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1
) # line/font thickness
color = color or [random.randint(0, 255) for _ in range(3)]
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
if label:
tf = max(tl - 1, 1) # font thickness
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
cv2.putText(
img,
label,
(c1[0], c1[1] - 2),
0,
tl / 3,
[225, 255, 255],
thickness=tf,
lineType=cv2.LINE_AA,
)
def _make_grid( nx, ny):
xv, yv = np.meshgrid(np.arange(ny), np.arange(nx))
return np.stack((xv, yv), 2).reshape((-1, 2)).astype(np.float32)
def cal_outputs(outs,nl,na,model_w,model_h,anchor_grid,stride):
row_ind = 0
grid = [np.zeros(1)] * nl
for i in range(nl):
h, w = int(model_w/ stride[i]), int(model_h / stride[i])
length = int(na * h * w)
if grid[i].shape[2:4] != (h, w):
grid[i] = _make_grid(w, h)
outs[row_ind:row_ind + length, 0:2] = (outs[row_ind:row_ind + length, 0:2] * 2. - 0.5 + np.tile(
grid[i], (na, 1))) * int(stride[i])
outs[row_ind:row_ind + length, 2:4] = (outs[row_ind:row_ind + length, 2:4] * 2) ** 2 * np.repeat(
anchor_grid[i], h * w, axis=0)
row_ind += length
return outs
def post_process_opencv(outputs,model_h,model_w,img_h,img_w,thred_nms,thred_cond):
conf = outputs[:,4].tolist()
c_x = outputs[:,0]/model_w*img_w
c_y = outputs[:,1]/model_h*img_h
w = outputs[:,2]/model_w*img_w
h = outputs[:,3]/model_h*img_h
p_cls = outputs[:,5:]
if len(p_cls.shape)==1:
p_cls = np.expand_dims(p_cls,1)
cls_id = np.argmax(p_cls,axis=1)
p_x1 = np.expand_dims(c_x-w/2,-1)
p_y1 = np.expand_dims(c_y-h/2,-1)
p_x2 = np.expand_dims(c_x+w/2,-1)
p_y2 = np.expand_dims(c_y+h/2,-1)
areas = np.concatenate((p_x1,p_y1,p_x2,p_y2),axis=-1)
areas = areas.tolist()
ids = cv2.dnn.NMSBoxes(areas,conf,thred_cond,thred_nms)
if len(ids)>0:
return np.array(areas)[ids],np.array(conf)[ids],cls_id[ids]
else:
return [],[],[]
def infer_img(img0,net,model_h,model_w,nl,na,stride,anchor_grid,thred_nms=0.4,thred_cond=0.5):
# 图像预处理
img = cv2.resize(img0, [model_w,model_h], interpolation=cv2.INTER_AREA)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32) / 255.0
blob = np.expand_dims(np.transpose(img, (2, 0, 1)), axis=0)
# 模型推理
outs = net.run(None, {net.get_inputs()[0].name: blob})[0].squeeze(axis=0)
# 输出坐标矫正
outs = cal_outputs(outs,nl,na,model_w,model_h,anchor_grid,stride)
# 检测框计算
img_h,img_w,_ = np.shape(img0)
boxes,confs,ids = post_process_opencv(outs,model_h,model_w,img_h,img_w,thred_nms,thred_cond)
return boxes,confs,ids
if __name__ == "__main__":
# 模型加载
model_pb_path = "best.onnx"
so = ort.SessionOptions()
net = ort.InferenceSession(model_pb_path, so)
# 标签字典
dic_labels= {0:'drug',
1:'glue',
2:'prime'}
# 模型参数
model_h = 320
model_w = 320
nl = 3
na = 3
stride=[8.,16.,32.]
anchors = [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]]
anchor_grid = np.asarray(anchors, dtype=np.float32).reshape(nl, -1, 2)
video = 0
cap = cv2.VideoCapture(video)
flag_det = False
while True:
success, img0 = cap.read()
if success:
if flag_det:
t1 = time.time()
det_boxes,scores,ids = infer_img(img0,net,model_h,model_w,nl,na,stride,anchor_grid,thred_nms=0.4,thred_cond=0.5)
t2 = time.time()
for box,score,id in zip(det_boxes,scores,ids):
label = '%s:%.2f'%(dic_labels[id],score)
plot_one_box(box.astype(np.int16), img0, color=(255,0,0), label=label, line_thickness=None)
str_FPS = "FPS: %.2f"%(1./(t2-t1))
cv2.putText(img0,str_FPS,(50,50),cv2.FONT_HERSHEY_COMPLEX,1,(0,255,0),3)
cv2.imshow("video",img0)
key=cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key & 0xFF == ord('s'):
flag_det = not flag_det
print(flag_det)
cap.release()
就该目标检测算法作者简单给大家讲解一下:
上方的 model_pb_path 和 dic_labels 是需要根据自己实际情况去改一下的,其余基本保持不变即可!
上述基本除了 anchor 锚框这个数据,其余都是不需要改动的,这里 anchor 直接使用了默认值。如果大家想在目标检测的时候可以更好地框选出自己地识别目标,可以用 K-means 聚类算法去自适应 anchor 的大小,从小聚类出符合自己数据类型的 anchor ,这样目标检测的时候可能效果更好!
一切准备就绪,直接运行咱们的 test_video.py 程序进行目标检测:
python3 test_video.py
按键 “s” 开启目标检测功能,按键 “q” 退出当前目标检测程序!
五、项目效果
5.1 实战视频
基于树莓派4B的YOLOv5-Lite目标检测
5.2 作者有话
作者本次仅使用了 ONNX 模型下直接跑 YOLOv5-Lite 的网络模型,目前该状态下的FPS仅维持在5左右,效果其实比直接跑 YOLOv5 网络模型已经好很多了(YOLOv5的FPS在0.3FPS左右)。但是距离可以与控制结合感觉还是差了点,所以,后续作者将对目标检测进行加速处理(使用NCNN,MNN等模型加速)。
当然,作者也会分享自己实验室提出的轻量化目标检测网络从网络模型出发加速推理。将计算机视觉与控制结合的嵌入式AI教学后续也会出博客分享给各位,希望给大家日常的工作或者是电赛提供些许帮助!
六、项目代码
代码地址:基于树莓派4B的YOLOv5-Lite目标检测的资源包资源-CSDN文库
如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!