基于yolov5的Android版本目标检测app开发(部署安卓手机)

基于yolov5的Android版本目标检测app开发(部署安卓手机)

0、项目开发需求
(1)开发app部署到安卓手机
(2)支持VOC数据集上所有的目标检测
1、开发环境搭建
windows10+pytorch+pyCharm+Anaconda
python 3.8
torch 1.9.0+cu111
torchvision 0.10.0+cu111
显卡:NVIDIA GeForce RTX 3070

2、数据集准备
(1)下载VOC数据集(链接)
(2)将VOC数据集的格式调整为yolov5的(链接)
yolov5是通过读取.yaml文件进行数据集加载的,.yaml文件内容如下所示
其中原VOC数据集与yolov5数据集文件夹结构对比如图所示:

在需要训练模型的.yaml里修改类别个数:

3、模型训练

(1)需要修改初始参数

"""
 	 weights: 权重文件/需要提前下载,后面模型训练在此基础上进行,修改为自己的路径
    cfg: 模型配置文件 包括nc、depth_multiple、width_multiple、anchors、backbone、head等,修改为自己的路径
    data: 数据集配置文件 包括path、train、val、test、nc、names、download等,修改为自己的路径
    hyp: 初始超参文件,修改为自己的路径
    epochs: 训练轮次
    batch-size: 训练批次大小(根据自己硬件选择建议(6-32))
    img-size: 输入网络的图片分辨率大小
    resume: 断点续训, 从上次打断的训练结果处接着训练  默认False
    nosave: 不保存模型  默认False(保存)      True: only test final epoch
    notest: 是否只测试最后一轮 默认False  True: 只测试最后一轮   False: 每轮训练完都测试mAP
    workers: dataloader中的最大work数(线程个数),根据自己硬件选择
    device: 训练的设备
    single-cls: 数据集是否只有一个类别 默认False

    rect: 训练集是否采用矩形训练  默认False
    noautoanchor: 不自动调整anchor 默认False(自动调整anchor)
    evolve: 是否进行超参进化 默认False
    multi-scale: 是否使用多尺度训练 默认False
    label-smoothing: 标签平滑增强 默认0.0不增强  要增强一般就设为0.1
    adam: 是否使用adam优化器 默认False(使用SGD)
    sync-bn: 是否使用跨卡同步bn操作,再DDP中使用  默认False
    linear-lr: 是否使用linear lr  线性学习率  默认False 使用cosine lr
    cache-image: 是否提前缓存图片到内存cache,以加速训练  默认False
    image-weights: 是否使用图片采用策略(selection img to training by class weights) 默认False 不使用

    bucket: 谷歌云盘bucket 一般用不到
    project: 训练结果保存的根目录 默认是runs/train
    name: 训练结果保存的目录 默认是exp  最终: runs/train/exp
    exist-ok: 如果文件存在就ok不存在就新建或increment name  默认False(默认文件都是不存在的)
    quad: dataloader取数据时, 是否使用collate_fn4代替collate_fn  默认False
    save_period: Log model after every "save_period" epoch    默认-1 不需要log model 信息
    artifact_alias: which version of dataset artifact to be stripped  默认lastest  貌似没用到这个参数?
    local_rank: rank为进程编号  -1且gpu=1时不进行分布式  -1且多块gpu使用DataParallel模式math
    entity: wandb entity 默认None
    upload_dataset: 是否上传dataset到wandb tabel(将数据集作为交互式 dsviz表 在浏览器中查看、查询、筛选和分析数据集) 默认False
    bbox_interval: 设置界框图像记录间隔 Set bounding-box image logging interval for W&B 默认-1   opt.epochs // 10
    """
    parser = argparse.ArgumentParser()
    # --------------------------------------------------- 常用参数 ---------------------------------------------
    parser.add_argument('--weights', type=str, default='weights/yolov5s.pt', help='initial weights path')
    parser.add_argument('--cfg', type=str, default='models/yolov5s.yaml', help='model.yaml path')
    parser.add_argument('--data', type=str, default='data/VOC.yaml', help='dataset.yaml path')
    parser.add_argument('--hyp', type=str, default='data/hyps/hyp.finetune.yaml', help='hyperparameters path')
    parser.add_argument('--epochs', type=int, default=200)
    parser.add_argument('--batch-size', type=int, default=8, help='total batch size for all GPUs')
    parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='[train, test] image sizes')
    parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
    parser.add_argument('--nosave', action='store_true', help='True only save final checkpoint')
    parser.add_argument('--notest', action='store_true', help='True only test final epoch')
    parser.add_argument('--workers', type=int, default=1, help='maximum number of dataloader workers')
    parser.add_argument('--device', default='cuda:0', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
    # --------------------------------------------------- 数据增强参数 ---------------------------------------------
    parser.add_argument('--rect', action='store_true', help='rectangular training')
    parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
    parser.add_argument('--evolve', default=False, action='store_true', help='evolve hyperparameters')
    parser.add_argument('--multi-scale', default=True, action='store_true', help='vary img-size +/- 50%%')
    parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
    parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
    parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
    parser.add_argument('--linear-lr', default=False, action='store_true', help='linear LR')
    parser.add_argument('--cache-images', action='store_true', help='cache images for faster training')
    parser.add_argument('--image-weights', default=True, action='store_true', help='use weighted image selection for training')
    # --------------------------------------------------- 其他参数 ---------------------------------------------
    parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
    parser.add_argument('--project', default='runs/train', help='save to project/name')
    parser.add_argument('--name', default='exp', help='save to project/name')
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
    parser.add_argument('--quad', action='store_true', help='quad dataloader')
    parser.add_argument('--save_period', type=int, default=-1, help='Log model after every "save_period" epoch')
    parser.add_argument('--artifact_alias', type=str, default="latest", help='version of dataset artifact to be used')
    parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, wins do not modify')
    # --------------------------------------------------- 三个W&B(wandb)参数 ---------------------------------------------
    parser.add_argument('--entity', default=None, help='W&B entity')
    parser.add_argument('--upload_dataset', action='store_true', help='Upload dataset as W&B artifact table')
    parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval for W&B')
    # parser.parse_known_args()
    # 作用就是当仅获取到基本设置时,如果运行命令中传入了之后才会获取到的其他配置,不会报错;而是将多出来的部分保存起来,留到后面使用
    opt = parser.parse_known_args()[0] if known else parser.parse_args()
    return opt

修改完以上参数就可以开始训练了。
(2)训练数据记录
1)训练参数
batch size:8
works:1
epoch:200

关于batch size和works有个疑问,第一次训练的时候batch size可以设置到12和works 为2 后面就不可以了,不知道为啥!!!!

2)训练耗时:接近23个小时。。。。。

3)CPU以及GPU占用率
4)result

5)模型验证:

  """
 opt参数解析
    weights: 模型的权重地址 默认 weights/best.pt
    source: 测试数据文件(图片或视频)的保存路径 默认data/images
    imgsz: 网络输入图片的大小 默认640
    conf-thres: object置信度阈值 默认0.25
    iou-thres: 做nms的iou阈值 默认0.45
    max-det: 每张图片最大的目标个数 默认1000
    device: 设置代码执行的设备 cuda device, i.e. 0 or 0,1,2,3 or cpu
    view-img: 是否展示预测之后的图片或视频 默认False
    save-txt: 是否将预测的框坐标以txt文件格式保存 默认True 会在runs/detect/expn/labels下生成每张图片预测的txt文件
    save-conf: 是否保存预测每个目标的置信度到预测tx文件中 默认True
    save-crop: 是否需要将预测到的目标从原图中扣出来 剪切好 并保存 会在runs/detect/expn下生成crops文件,将剪切的图片保存在里面  默认False
    nosave: 是否不要保存预测后的图片  默认False 就是默认要保存预测后的图片
    classes: 在nms中是否是只保留某些特定的类 默认是None 就是所有类只要满足条件都可以保留
    agnostic-nms: 进行nms是否也除去不同类别之间的框 默认False
    augment: 预测是否也要采用数据增强 TTA
    update: 是否将optimizer从ckpt中删除  更新模型  默认False
    project: 当前测试结果放在哪个主文件夹下 默认runs/detect
    name: 当前测试结果放在run/detect下的文件名  默认是exp
    exist-ok: 是否存在当前文件 默认False 一般是 no exist-ok 连用  所以一般都要重新创建文件夹
    line-thickness: 画框的框框的线宽  默认是 3
    hide-labels: 画出的框框是否需要隐藏label信息 默认False
    hide-conf: 画出的框框是否需要隐藏conf信息 默认False
    half: 是否使用半精度 Float16 推理 可以缩短推理时间 但是默认是False
    """
    parser = argparse.ArgumentParser()
    parser.add_argument('--weights', nargs='+', type=str, default='runs/train/exp19/weights/best.pt', help='model.pt path(s)')
    parser.add_argument('--source', type=str, default='data/images/112.mp4', help='file/dir/URL/glob, 0 for webcam')
    parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
    parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')
    parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold')
    parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    parser.add_argument('--view-img', action='store_true', help='show results')
    parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
    parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
    parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes')
    parser.add_argument('--nosave', action='store_true', help='do not save images/videos')
    parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --class 0, or --class 0 2 3')
    parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
    parser.add_argument('--augment', action='store_true', help='augmented inference')
    parser.add_argument('--update', action='store_true', help='update all models')
    parser.add_argument('--project', default='runs/detect', help='save results to project/name')
    parser.add_argument('--name', default='exp', help='save results to project/name')
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
    parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)')
    parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')
    parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')
    parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
    parser.add_argument('--prune-model', default=False, action='store_true', help='model prune')
    parser.add_argument('--fuse', default=False, action='store_true', help='fuse conv and bn')
    opt = parser.parse_args()

输入一张图,输入一段视屏:
修改图片路径或者视屏路径即可。代码里面的 cv2.waitKey(1) 需要修改,图片为 cv2.waitKey(0) ,视屏为 cv2.waitKey(1)

            # 是否需要显示我们预测后的结果  img0(此时已将pred结果可视化到了img0中)
            if view_img:
                cv2.imshow(str(p), im0)
                cv2.waitKey(1)  # 1 millisecond

输入电脑摄像头:
修改detect.py配置

4、模型转换
1).将.pt 转换为onnx
在模型pt转化为onnx的时候需要先进行修改models中的cocommon.py,修改Focus 去除slice数组操作,如果不改后续onnx2ncnn转换的时候会报错。

        #原代码中训练用的
    def forward(self, x):
        return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))
		#模型转换用的代码
    def forward(self, x):
        #修改Focus去除slice数组操作,如果不改后续onnx2ncnn转换的时候会报错
        return self.conv(torch.cat([x, x,x,x], 1))

修改export.py参数

    img-size: 输入模型的图片size=(height, width) 默认=[640, 640]  可以减小一下尺寸免得手机卡爆了
    batch-size: batch大小 默认=1
    device: 模型运行设备 cuda device, i.e. 0 or 0,1,2,3 or cpu 默认=cpu
    include: 要将pt文件转为什么格式 可以为单个原始也可以为list 默认=['torchscript', 'onnx', 'coreml']
    half: 是否使用半精度FP16export转换 默认=False
    inplace: 是否set YOLOv5 Detect() inplace=True  默认=False
    train: 是否开启model.train() mode 默认=True  coreml转换必须为True
    optimize: TorchScript转化参数 是否进行移动端优化  默认=False
    dynamic: ONNX转换参数  dynamic_axes  ONNX转换是否要进行批处理变量  默认=False
    simplify: ONNX转换参数 是否简化onnx模型  默认=False
    opset-version: ONNX转换参数 设置版本  默认=10
    """
    parser = argparse.ArgumentParser()
    parser.add_argument('--weights', type=str, default='../runs/train/exp19/weights/best.pt', help='weights path')
    parser.add_argument('--img-size', nargs='+', type=int, default=[416, 416], help='image (height, width)')
    parser.add_argument('--batch-size', type=int, default=1, help='batch size')
    parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    parser.add_argument('--include', nargs='+', default=['torchscript', 'onnx', 'coreml'], help='include formats')
    parser.add_argument('--half', action='store_true', help='FP16 half-precision export')
    parser.add_argument('--inplace', action='store_true', help='set YOLOv5 Detect() inplace=True')
    parser.add_argument('--train', default="True", action='store_true', help='model.train() mode')
    parser.add_argument('--optimize', action='store_true', help='TorchScript: optimize for mobile')
    parser.add_argument('--dynamic', action='store_true', help='ONNX: dynamic axes')
    parser.add_argument('--simplify', action='store_true', help='ONNX: simplify model')
    parser.add_argument('--opset-version', type=int, default=10, help='ONNX: opset version')
    opt = parser.parse_args()
    return opt

模型简化可以用以下命令:
python -m onnxsim yolov5s.onnx yolov5ssim.onnx,自己把“yolov5s.onnx”修改成自己的名字就好了,如果没有安装onnxsim,先安装pip install onnx-simplifier再运行上面的简化指令python -m onnxsim yolov5s.onnx yolov5ssim.onnx
或者在export.py中设置参数,则转换为onnx 以后模型已经被简化了。

2).将onnx转换为ncnn
a.简单粗暴的:https://convertmodel.com/
直接选择输入输出模型就可以使用,简单粗暴,方便快捷。

b.使用cmake 进行编译,过程较复杂难道较大,切容易出错,后面进行补充。

c、在ncnn(https://github.com/Tencent/ncnn/releases)库下载对应的Windows和VS版本的文件,比如说我这里安装的VS2019,我下载的文件就是:https://github.com/Tencent/ncnn/releases/download/20220216/ncnn-20220216-windows-vs2019.zip
版本问题的话没有太多要求,下好后解压,在X64/bin文件下有对应的exe文件,把模型文件拷贝到当前目录,在文件夹里面按住shift点右键打开Powershell窗口,输入***./onnx2ncnn yolov5s.onnx yolov5s.param yolov5s.bin当前目录就会出现 yolov5s.param yolov5s.bin两个文件.
然后接着重复打开Powershell,
./ncnnoptimize yolov5s.param yolov5s.bin yolov5s_out.param yolov5s_out.bin 65536*** 压缩文件大小。

4、修改转换后的模型.param文件

打开上一步生成的 .param文件,直接拉到最后,将图中的3个数字改为-1,这一步是为了防止Android移植后,检测结果正确显示用,如果不改的话,会出现n多个框,密密麻麻的覆盖你的原本图片。
修改前:
修改后:
log.csdnimg.cn/c23f82279b344211b023cbd8e313db30.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6aqR552A5ouW5ouJ5py65Y675peF6KGM,size_18,color_FFFFFF,t_70,g_se,x_16)

自此模型准备完毕下一步在app开发里面加载。
5、Androidapp开发
官网提供了现成的模块:ncnn-android-yolov5 https://github.com/nihui/ncnn-android-yolov5

1)下载ncnn-20220216-android-vulkan(链接:https://github.com/Tencent/ncnn/releases
2)修改代码
找到3个Permute查看对应的Output的name,修改完成后,继续修改class_names,换成自己的标签,最后连上设备就可以识别图像了。

6、运行检测

来源:骑着拖拉机去旅行

物联沃分享整理
物联沃-IOTWORD物联网 » 基于yolov5的Android版本目标检测app开发(部署安卓手机)

发表评论