【project记录】yolov3-tiny custom data训练、转化为ncnn并部署1.0(尚未成功)

问题描述

专业综合实践课的一个project。主要目的是实现一个app,可以传入两张鸽子的图片、确定两只鸽子是不是同一品种的。
主要任务拆解为:

  1. 识别鸽子眼睛的模型训练:使用yolo系列进行目标检测(眼睛的检测)(github地址:https://github.com/eriklindernoren/PyTorch-YOLOv3
  2. 眼睛比对与相似度计算的模型训练:使用孪生网络
  3. 模型转换为ncnn格式(param和bin)
  4. 基于开源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。数据格式如下:
yolov3数据格式示意
于是需要一些转换。很简单地在网上找到了代码*(声明:这里代码不是我写的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之后另外写一篇小短文来记录吧。
预处理数据和配置:

  1. 按照readme组织数据:包括生成正确的txt,配置相关数据配置文件,移动到对应文件夹等。
  2. 将图片(.jpg)全部放到文件夹:\data\custom\images
  3. 编辑classes.name文件夹,写入标签名,这里只有一类,即eye

classes.name

  1. 生成train和val对应的图片的路径txt文件。这里不能直接用Vott标出来的train.txt和val.txt,因为得加上路径和后缀。格式如图:

path.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()

  1. 编辑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",注意加逗号
AS gradle

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数据集实例检测成功是这样的:

coco成功案例
所以我又得重新训练模型了,我还不知道错在哪5555
等成功了再来不错

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值