问题描述
专业综合实践课的一个project。主要目的是实现一个app,可以传入两张鸽子的图片、确定两只鸽子是不是同一品种的。
主要任务拆解为:
- 识别鸽子眼睛的模型训练:使用yolo系列进行目标检测(眼睛的检测)(github地址:https://github.com/eriklindernoren/PyTorch-YOLOv3)
- 眼睛比对与相似度计算的模型训练:使用孪生网络
- 模型转换为ncnn格式(param和bin)
- 基于开源android studio项目,部署(地址:https://github.com/nihui/nvcnn-android-mobilenetssd)
我负责第一个模型的训练,并且实现相应的3和4。
project还没结束,现在先记录目前的状况,以及踩的一堆——坑,和解决方法TAT
数据
首先是鸽子的数据标注。使用了windows的Vott工具进行标注,简单地学习了工具的使用。
得保证标记后的数据和模型要的格式是可以互相转换的。
刚开始(我以为)遇到这些问题:
(1)文件名称问题:查看标注的工程文件夹,发现图片名与生成的标记名不对应,标记名是随机生成的
(2)文件格式问题:pascal voc网上说是xml格式,但是标记出来是json格式。找到的将pascal voc转yolo v3的只能将xml转过来
后来发现,其实名称是对应的,标注的文件格式也都是xml。那些json只是标注的辅助文件。
在工程中点击export之后就会生成正确的Pascal voc所需的training数据格式。
但是yolov3要的数据是txt。数据格式如下:
于是需要一些转换。很简单地在网上找到了代码*(声明:这里代码不是我写的555,但是原网址我找不着了,在这放出来,侵删!)*
import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join
sets = []
# 这里是你的类别
classes = ["class1", "class2"]
# 原样保留。size为图片大小
# 将ROI的坐标转换为yolo需要的坐标
# size是图片的w和h
# box里保存的是ROI的坐标(x,y的最大值和最小值)
# 返回值为ROI中心点相对于图片大小的比例坐标,和ROI的w、h相对于图片大小的比例
def convert(size, box):
dw = 1. / (size[0])
dh = 1. / (size[1])
x = (box[0] + box[1]) / 2.0 - 1
y = (box[2] + box[3]) / 2.0 - 1
w = box[1] - box[0]
h = box[3] - box[2]
x = x * dw
w = w * dw
y = y * dh
h = h * dh
return (x, y, w, h)
def convert_annotation(image_add):
# image_add进来的是带地址的.jpg
image_add = os.path.split(image_add)[1] # 截取文件名带后缀
image_add = image_add[0:image_add.find('.', 1)] # 删除后缀,现在只有文件名没有后缀
print(image_add)
# 现在传进来的只有图片名没有后缀
in_file = open('adress' + image_add + '.xml')
out_file = open('labels/%s.txt' % (image_add), 'w')
tree = ET.parse(in_file)
root = tree.getroot()
size = root.find('size')
w = int(size.find('width').text)
h = int(size.find('height').text)
# 在一个XML中每个Object的迭代
for obj in root.iter('object'):
# iter()方法可以递归遍历元素/树的所有子元素
difficult = obj.find('difficult').text
cls = obj.find('name').text
# 如果训练标签中的品种不在程序预定品种,或者difficult = 1,跳过此object
if cls not in classes or int(difficult) == 1:
continue
# cls_id 只等于1
cls_id = classes.index(cls)
xmlbox = obj.find('bndbox')
# b是每个Object中,一个bndbox上下左右像素的元组
b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
float(xmlbox.find('ymax').text))
bb = convert((w, h), b)
out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
if __name__ == '__main__':
# os.path是python文件打开的位置
if not os.path.exists('labels/'):
os.makedirs('labels/')
#image_adds = open("D:/DL/geteyes/eye-PascalVOC-export/ImageSets/Main/eye_train.txt")
image_adds = open("adress/eye_val.txt")
for image_add in image_adds:
# print(image_add)
image_add = image_add.strip()
# print (image_add)
convert_annotation(image_add)
总之,只需要标注好export,然后修改一下Voc2Yolo.py的参数运行,就可以得到正确的yolov3数据格式啦!
训练yolov3-tiny
其实这里我是先尝试用这个代码以及预训练好的yolov3来detect,之后确定代码能用、环境配置正确了才开始训练的。detect的过程听起来很简单很弱智,但是更弱智的我也花了得有1~2小时才detect出来的5555之后另外写一篇小短文来记录吧。
预处理数据和配置:
- 按照readme组织数据:包括生成正确的txt,配置相关数据配置文件,移动到对应文件夹等。
- 将图片(.jpg)全部放到文件夹:\data\custom\images
- 编辑classes.name文件夹,写入标签名,这里只有一类,即eye
- 生成train和val对应的图片的路径txt文件。这里不能直接用Vott标出来的train.txt和val.txt,因为得加上路径和后缀。格式如图:
可以自己编一个python来转换,我写了一个可移植性很差的智障脚本:
in_file=open('eye_train.txt')
out_file=open('eye_train_final_2.txt','w')
for line in in_file:
name=line.split(' ')[0]
string='../data/custom/images/'+name
out_file.write(string+'\n')
out_file.close()
- 编辑config文件夹里的custom.txt,配置custom data对应的一些参数(路径等)
classes= 1
train=../data/custom/eye_train_final_2.txt
valid=../data/custom/eye_val_final_2.txt
names=../data/custom/classes.names
其实这时候直接按照train.py的参数要求训练就行了:
指定data,model就行
python train.py --data ../config/custom.data --epochs 300 --model ../config/yolov3-tiny.cfg
但是因为我需要后续导出onnx格式,要知道input shape;后续有可能和队友要合并模型,要知道Output shape,于是我在本地运行了一下,看两者的shape:
- input简单:(batch_size,3,416,416)
- outputs是一个List,输出list[0]看看
outputs: 2
torch.Size([32, 3, 26, 26, 85])
那我当然得理解这个Output是什么意思,于是开始查资料(因为懒得看原文,所以就找各种博客看)
然后发现——这个shape很奇怪啊!
根据网上资料,说yolov3-tiny的output维度应该是2626(或1313)3(5+classes)
85从哪来???它甚至不能被3整除??
经过一系列演算认为,85=80+5,shape的第2个维度“3”是那三个框框的3。
即classes并没有被改成1,而是coco dataset的80!
于是查找如何修改模型中classes的数量为1
在代码中没发现config/custom.txt中的classes这个参数被用到,因此肯定是代码内部哪里要改——
查找资料后发现(又没保存网址,等我找到了补一下555),应修改cfg文件:
打开查找classes,果然找到2个classes,都是yolo layer的,都是80,改成1;以最后一个yolo层为例:
# 23
[yolo]
mask = 1,2,3
anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
classes=1 #80
num=6
jitter=.3
ignore_thresh = .7
truth_thresh = 1
random=1
改后竟然给我报错了??
RuntimeError: shape '[32, 3, 6, 13, 13]' is invalid for input of size 1379040
解决办法:
修改yolov3-tiny.cfg的classes和对应classes(1)前的filters(3*(5+classes))
以第1个YOLO层以及其上层为例:
# 15
[convolutional]
size=1
stride=1
pad=1
filters=18 #(3*(5+classes))
activation=linear
# 16
[yolo]
mask = 3,4,5
anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
classes=1
num=6
jitter=.3
ignore_thresh = .7
truth_thresh = 1
random=1
然后就快了运行啦!
因为本地电脑跑不动,上服务器,blabla配置好之后运行:
nohup python train.py --data ../config/custom.data --model ../config/yolov3-tiny.cfg >yolo 2>&1 &
导出onnx,导出ncnn
这里遇到了巨无敌多的坑,时至今日从onnx导ncnn都失败了,不过采取了一个方法曲线救国。
先上我的转onnx代码,也参考了很多大佬的博客……
# import torch
# import os
# import sys
import onnx
# import onnxruntime
# import numpy as np
import torch
from pytorchyolo.models import load_model
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def transform_to_onnx(cfg_file, weight_file, batch_size, in_h, in_w):
model = load_model(cfg_file,weight_file)
model.eval()
x = torch.ones((batch_size, 3, in_h, in_w)).to(device)#, requires_grad=True) #* 120 / 255.0
onnx_file_name = 'yolov3tinyPigion1010.onnx'
torch.onnx.export(model, x, onnx_file_name, input_names=["input"], output_names=["outputs0"],
verbose=False, opset_version=11)
# dynamic_axes=None)
print('Onnx model exporting done')
return onnx_file_name, x
def main(cfg_file, weight_file, batch_size, in_h, in_w):
onnx_path_demo, x = transform_to_onnx(cfg_file, weight_file, batch_size, in_h, in_w)
# session = onnxruntime.InferenceSession(onnx_path_demo)
# output = session.run(['output1'], {'input':x.detach().numpy()})
model = onnx.load(onnx_path_demo)
onnx.checker.check_model(model)
if __name__ == "__main__":
cfg_file = f'yolov3-tiny.cfg'
weight_file = f'yolov3_ckpt_290.pth'
batch_size = 1
in_h = 416
in_w = 416
main(cfg_file, weight_file, batch_size, in_h, in_w)
这个就是:
导入cfg和pth,load模型,然后把模型转成onnx;结果大概是这样的:
TracerWarning: Converting a tensor to a Python index might cause the trace to be incorrect. We can't record the data flow of Python values, so this value will be treated as a constant in the future. This means that the trace might not generalize to other inputs!
x = combined_outputs[:, group_size * group_id : group_size * (group_id + 1)] # Slice groupings used by yolo v4
一个warning完全没引起我的警醒,直接用ncnn的转化工具来转化了。进入到工具的bin文件夹中运行:
onnx2ncnn.exe [onnxpb] [ncnnparam] [ncnnbin]
然后出错了(刚开始我也没警醒,后来进入AS直接闪退了,才发现是onnx转ncnn的错误)
Shape not supported yet!
Gather not supported yet!
# axis=0
ConstantOfShape not supported yet!
# value 4
Unsupported slice step !
Cast not supported yet!
# to=7
Unknown data type 0
然后同学推给我一个网站:
https://convertmodel.com/
还是失败了,报了更多类似上面的错误。
于是想了一个最大可能性的问题:
yolov3原代码太复杂,转化的onnx格式有问题(毕竟它报了warning):于是花了一个上午简化源代码去掉slice操作、找其他转onnx成功的源代码(如:https://github.com/qqsuhao/YOLOv3-YOLOv3-tiny-yolo-fastest-xl–pytorch;),但是修改调slice后重新尝试训练1轮、转onnx还是报warning(而且正是我改的地方??)、onnx转ncnn还是出错。
于是再思考,想起我的一个demo、用单层CNN和MNIST数据集训练的模型,转onnx完全正确,onnx转ncnn一样报错:
Shape not supported yet!
Gather not supported yet!
# axis=0
Unsupported unsqueeze axes!
Unknown data type 0
所以我十分疑惑:模型转onnx的代码看起来很正常(网上也有许多类似的),onnx转ncnn也是用的工具,怎么会有这么多错?
最后曲线救国:
用刚才提到的同学给的网站执行darknet转ncnn。
写了一段把.pth转成weights的简单脚本,使用原来的代码里的save_darknet_weights函数=A=
from pytorchyolo.models import load_model
cfg_file = '../config/yolov3-tiny.cfg '
weight_file = 'yolov3_ckpt_290.pth'
model = load_model(cfg_file,weight_file)
model.eval()
#model.eval()
model.save_darknet_weights("yoloP.weights")
print("successfully save weights!")
部署上Android Studio
配置环境全部省去(没有记录,忘了)
直接看运行时的一些报错
1.报错如下:
device supports x86,but APK only supports arms64-v8a
解决方法:修改gradle,添加"x86",注意加逗号
2.sdk报错:
连接老版本安卓机(api23)时报错,说设备sdkVersion为23,而项目minSdkVersion为24
解决方法1::修改minSdkVersion为23,报了各种错误(如找不到vulkan(vulkan missing)等),修改失败
改回24无错误;
解决方法2:考虑换一个安卓机;用了我服役中的华为nova3e,终于找到了USB调试;开放了几乎所有权限终于连接上了!
然后使用代码原来的.param和.bin实验成功!但是:
(1)闪退1
把我训练的鸽子.param和.bin放上去,闪退!
解决方法:
发现报错是说“ncnn识别不了某一层",于是转到导出onnx、导出ncnn的部分
(2)闪退2
闪退1解决后,选择图片、点击识别后闪退:
解决方法: 导出.weights没有把模型改成.eval模式;改后再转ncnn后问题解决
(3)识别不了
选择图片、点击识别后,识别不了;查看log,发现:
find_blob_index_by_name detection_out failed
于是根据这篇进行了修改:https://zhuanlan.zhihu.com/p/362679768
我理解的是这样,我的网络没有指定各层的名字,所以在识别的时候出错,是在这input和extract两行:
//ex.input("data", input);这句就有可能会报错
ex.input(mobilenetv2_param_id::BLOB_actual_input, input);
ncnn::Mat out;
ex.extract(mobilenetv2_param_id::BLOB_output, out);
于是按照指南,将.param和.bin转成两个头文件:
ncnn2mem yoloP2.param yoloP2.bin yoloP2.id.h yoloP2.mem.h
然后在AS中引用头文件,并修改:
注意,input和extract的第一个参数每个人网络不一样,自己打开id那个头文件,看看输入和输出的名字分别是啥,我的是:
BLOB_data和BLOB_yolo1
(4)识别不了
解决了(3)的识别不了,终于不报错了,但是我点了好几下识别,都没有出现框出鸽子眼睛的效果,怎么回事!
查看AS的控制台信息,发现它是识别成功了的!
嗯?
不会是我想的那样吧……我的模型训练失败了?
2021/10/10,我悲伤地再次打开yolov3文件夹,使用最后的weights(或者pth)执行detect……
Detecting: 100%|██████████████████████████████| 3/3 [00:10<00:00, 3.61s/it]
Image ..\data\testP\2467.jpg:
Image ..\data\testP\2493.jpg:
Image ..\data\testP\2731.jpg:
which means:模型没有在我的图片上检测到god damn鸽子的眼睛!
对比coco数据集实例检测成功是这样的:
所以我又得重新训练模型了,我还不知道错在哪5555
等成功了再来不错