本项目基于弗里斯兰牛的检测与身份ID识别任务,演示了从数据集准备、标注,图像分类模型训练、目标检测模型训练、改进、推理等全方位的实现流程。

一 简介

1.1 背景

通过在挤奶通道口采集奶牛图像,并用人工智能实现奶牛个体的快速身份识别,从而为实时动态监测奶牛行为提供了基础条件,在畜牧业有广阔的应用价值。

本项目的目的通过深度学习对荷斯坦弗里西亚牛进行视觉定位和个体识别 需完成分类检测两个任务 PaddleClas+PaddleDetection 奶牛身份识别,这是实现智慧养殖的基础性工作。

思考1:当我看到这个题目的时候,想的这个直接一个目标检测就能完成任务吧,先别急,咱们看看数据集。

1.2 数据集

原始数据地址 https://data.bris.ac.uk/data/dataset/2yizcfbkuv4352pzc32n54371r

总共940张弗里斯兰牛在挤奶通道时候的照片

都是通过矫正后JPG格式照片

数据集结构如下:

1、89个文件夹对应了89头不同牛,可以理解为文件名就是每个牛的ID,但是每个文件夹里面的照片里面不一定只有该id对应的牛,也就是还有其他牛牛同框。

2、没有标签,我似乎没找到该数据集对应的标签在哪,不过我在一个开源项目中找到了有标签的数据集(https://aistudio.baidu.com/aistudio/datasetdetail/31879),
可惜的是他每张图片只标了一头牛(因为他是用这个做了个分类模型),其他同框的牛并没有标。

1.3 任务分析

基于以上分析,如果要完成目标检测任务(包括的是图像中所有的牛牛)那么就得自己标数据集了,之所以还得再加一个图像分类,是因为图像中存在多个牛的情况,并不知道其它牛牛的ID,所以还得准备个图像分类模型来识别牛牛的ID,而目标检测只用来检测定位出牛牛即可。

所以本项目任务如下:

1、图像分类数据集的制作

2、图像分类模型训练

3、目标检测数据集制作

4、目标检测模型训练

5、整合模型

1.4 任务开发流程

我将本次任务分为了四部分

1、数据集的制作,包括了图像分类数据集和目标检测数据集。该部分放在main.ipynb里面,首先执行该部分生成所需要的数据集(图像分类+目标检测数据集)再进行模型的训练。

在有了数据集后便可以进行以下任务:

2、独立创建 RecTrain.ipynb 来用于图像分类模型训练,这个项目中所有训练任务都是用的后台任务训练,独立个ipynb生成版本进行后台训练能大大的节省项目开发时间,帮助并行开发。

3、独立创建 DetTrain.ipynb 来用于目标检测模型训练,同上。

在模型都准备好后进行以下任务:

4、整合代码,进行串行推理,即:目标检测+图像分类->结果。该部分主要是代码的修改与整合,该部分代码都位于/home/aistudio/deploy文件夹下。执行predict_all.py即可,这部分放在了main.ipynb后面,一行命令。

除以上之外。

1、mian.ipynb 将整个项目所有流程操作都罗列了出来(即将所有分开的ipynb内容以项目开发顺序罗列),也是整体思路。

2、文件里面有许多配置文件,都是修改好后的模型训练配置文件,每次训练只需要修改对应的数据集地址即可。

3、在本次项目中,任务挺繁杂的,可以看见生成了许多版本,都是对应的训练模型使用的版本,编号最新的即为最终采用的模型训练版本,可直接执行对应版本进行训练。

注:该任务生成的数据集、模型都已存在文件当中,所以可以直接执行/home/aistudio/deploy下的predict_all.py即可完成推理,在main.ipynb第五章整合部分。

#导入和下载必要的库
from PIL import Image
import matplotlib.pyplot as plt
import os
import xml.etree.ElementTree as ET
import cv2
import math
import shutil
import random
import numpy as np

%cd /home/aistudio/
#git paddledetection 目标检测框架
! git clone https://gitee.com/paddlepaddle/PaddleDetection.git
%cd ~/PaddleDetection/
#这里修改了镜像源--下载相关依赖
!ls
! python3 -m pip install paddledet -i https://mirror.baidu.com/pypi/simple
%cd ~/PaddleDetection/
!pip install -r requirements.txt
!python setup.py install
%env PYTHONPATH=.:$PYTHONPATH
%env CUDA_VISIBLE_DEVICES=0
# 引入PaddleX
# !pip install paddlex

#下载PaddleClas
%cd /home/aistudio/
!git clone https://gitee.com/paddlepaddle/PaddleClas.git -b release/2.5
!pip install paddleclas

二 数据集制作

#解压数据集
!tar -xf /home/aistudio/data/data31879/data.tar.gz
!tar -xf /home/aistudio/data/data31879/annotations.tar.gz

2.1 查看数据集

这里主要包括了查看数据集基本情况,以及修改xml配置文件。

#查看数据数量与标签文件数量
print("num of annotations", len(os.listdir('/home/aistudio/FriesianCattle2017_Annotations')))
flag = 0
for dirfile in os.listdir("/home/aistudio/FriesianCattle2017"):
    if os.path.isfile(os.path.join("/home/aistudio/FriesianCattle2017", dirfile)):
        # print(os.path.join("/home/aistudio/FriesianCattle2017", dirfile))
        continue
    for imgdir in os.listdir(os.path.join("/home/aistudio/FriesianCattle2017", dirfile)):
        if "jpg" in imgdir:
            flag += 1
print("num of images: ", flag)
#处理annotations -----------修改所有xml文件里面的图片路径为当前图片路径
#这里采用的是别人公开分享的数据集,所里里面的图片绝对路径(“path”)需要修改为当前的图片存储路径
annoPath='/home/aistudio/FriesianCattle2017_Annotations/'
newannoPath = '/home/aistudio/annotations/'
if not os.path.exists(newannoPath):
    os.mkdir(newannoPath)
for annodir in os.listdir(annoPath):
    if 'xml' in annodir:
        annofilepath = os.path.join(annoPath, annodir)
        tree = ET.parse(annofilepath)
        # tree.findall("path")[0].text = tree.findall("path")[0].text.replace("D:\Research\Datasets\cattle", "\home\aistudio")
        tree.findall("path")[0].text = "\home\\aistudio\FriesianCattle2017" + tree.findall("path")[0].text.split("FriesianCattle2017")[1]
        tree.findall("path")[0].text = tree.findall("path")[0].text.replace("\\", "/")
        tree.write(os.path.join(newannoPath, annodir))
#查看标注文件中有多少个目标
nums = 0
for annodir in os.listdir(annoPath):
    if 'xml' in annodir:
        annofilepath = os.path.join(annoPath, annodir)
        tree = ET.parse(annofilepath)
        nums+=len(tree.findall('object'))
print("目标数量: ", nums)

question

1、发现该数据集当中在一个图片存在多头牛的时候只标注了一头(目标牛)

2、但是任务是实现目标检测+图像分类

既然这样,在已有的数据标注的情况下,可以使用当前的存在标注文件来生成图像分类数据集。

对于目标检测的数据集,我选择手动标注。

2.2 图像分类数据集生成

通过现有的xml配置文件中的信息提取目标的牛牛图像进行存储即可。

import datetime as dt
imagesNums = 0
def save_img(path, xmin, xmax, ymin, ymax, saveimgPath):
    global imagesNums
    if not os.path.exists(saveimgPath):
        os.mkdir(saveimgPath)
    savepath = os.path.join(saveimgPath, dt.datetime.now().strftime("%Y%m%d%H%M%S%f")+".png")
    img = cv2.imread(path)
    roi = img[int(ymin):int(ymax), int(xmin):int(xmax)]
    cv2.imwrite(savepath, roi)
    imagesNums+=1
def get_cls(annoPath, savePath):
    if not os.path.exists(savePath):
        os.mkdir(savePath)
    for annodir in os.listdir(annoPath):
        if 'xml' in annodir:
            annofilepath = os.path.join(annoPath, annodir)
            tree = ET.parse(annofilepath)
            # print(annofilepath)
            imgPath = tree.findall("path")[0].text
            for obj in tree.findall("object"):
                cls_id = obj.find('name').text
                saveImgPath = os.path.join(savePath, cls_id)
                bbox = obj.find('bndbox')
                xmin = bbox.find('xmin').text
                ymin = bbox.find('ymin').text
                xmax = bbox.find('xmax').text
                ymax = bbox.find('ymax').text
                save_img(imgPath, xmin, xmax, ymin, ymax, saveImgPath)
def split_images(path, rootPath,ratio_train=0.9, ratio_val=0.1):
    trainnums = 0
    valnums = 0
    if not os.path.exists(rootPath):
        os.mkdir(rootPath)
    trainPath = os.path.join(rootPath, "train")
    if not os.path.exists(trainPath):
        os.mkdir(trainPath)
    valPath = os.path.join(rootPath, "val")
    if not os.path.exists(valPath):
        os.mkdir(valPath)
    for dirpath in os.listdir(path):
        imgPath = os.path.join(path, dirpath)
        lenth = len(os.listdir(imgPath))
        # print(lenth)
        imglist = os.listdir(imgPath)
        # print(imglist, lenth)
        # print(math.ceil(lenth*ratio_val))
        for i in range(lenth):
            if i<=math.ceil(lenth*ratio_val):
                if not os.path.exists(os.path.join(valPath, dirpath)):
                    os.mkdir(os.path.join(valPath, dirpath))
                oldpath = os.path.join(imgPath, imglist[i])
                new_name = valPath + oldpath.split("clsImages")[1]
                shutil.copyfile(oldpath, new_name)
                valnums+=1
            else:
                if not os.path.exists(os.path.join(trainPath, dirpath)):
                    os.mkdir(os.path.join(trainPath, dirpath))
                oldpath = os.path.join(imgPath, imglist[i])
                new_name = trainPath + oldpath.split("clsImages")[1]
                shutil.copyfile(oldpath, new_name)
                trainnums+=1
    print("train : ", trainnums, "val: ", valnums)
#提取图像分类数据集至新的文件夹
anno = '/home/aistudio/annotations/'
savePath = "/home/aistudio/clsImages/"
get_cls(anno, savePath)
print("NUM OF IMAGES: ", imagesNums)

#划分新文件夹存储 验证集 与 训练集
imgpath = "/home/aistudio/clsImages"
save_path = "/home/aistudio/clsdatas"
split_images(imgpath, save_path)#默认划分比例为9:1
#使用写好的代码一键生成txt文件,即图像分类的配置文件
%cd /home/aistudio/clsdatas
!cp /home/aistudio/process_format_train.py /home/aistudio/clsdatas/process_format_train.py
!python process_format_train.py

三 图像分类模型训练

89类939张图像分类数据集,该部分我单独新建了一个RecTrain.ipynb,便于生成版本进行后台任务训练模型。

3.1 自定义数据训练模型

参考链接:https://gitee.com/paddlepaddle/PaddleClas/blob/release/2.5/docs/zh_CN/image_recognition_pipeline/feature_extraction.md#https://gitee.com/link?target=https%3A%2F%2Fdata.vision.ee.ethz.ch%2Fcvl%2Fdatasets_extra%2Ffood-101%2F

修改配置文件

这部分我对多个模型都试了以下,轻量级神经网络PPLCNETV2_base、重型神经网络ResNet50_vd,以及基于PPLCNET改进后的PULC。

对应配置文件:

PPLCNETV2_base:/home/aistudio/PPLCNetV2_base.yaml

PULC: /home/aistudio/PPLCNet_x1_0.yaml

ResNet50_vd: /home/aistudio/ResNet50_vd.yaml

数据集文件:/home/aistudio/clsdatas

注: 有一个类别数需要填写,类别数填写为90,以及修改以下数据集路径。

#直接使用配置好的yaml文件即可开始训练
%cd /home/aistudio/PaddleClas
%env CUDA_VISIBLE_DEVICES=0,1,2,3
!python tools/train.py \
-c /home/aistudio/PPLCNet_x1_0.yaml
#导出模型
!python3.7 tools/export_model.py \
-c /home/aistudio/PPLCNet_x1_0.yaml \
-o Global.pretrained_model="/home/aistudio/PaddleClas/output/PPLCNet_x1_0/best_model"

3.2 效果与总结

在该部分当中,进行了实验了多个模型,包括了PPLCNETV2_base 和 ResNet50_vd模型。

对于ResNet50_vd模型训练过程的各项指标如下:

训练了200个epoch,eval_acc达到了0.69左右,这个准确率显然不是很理想。

同样也使用了PPLCNETV2,其效果如下所示:

eval_acc 在训练了480个epoch后也只有0.8,而在该项目当中,ID识别模块的重要性也比较高,所以接下来就分析以下改进的措施,获得一个更好的模型,

3.2.1 模型改进

我初步认为是数据集较少的原因,在该项目中图像分类数据只有900张,200张验证集、700张训练集。所以接下来就要尝试一系列的改进,增加一些数据增广的方法来提升模型分数。

于是我就去看了一下PaddleClas官网上PULC做出以PP-LCNet为基础做出了以下优化:

1、SSLD预训练权重

2、EDA数据增强策略

3、SKL-UGI模型蒸馏

这里主要关注他的数据增强方法,增加了RandomErasing和RandomAugment方法。

上图显示了PULC在各个场景中的应用效果,整体上来说,PULC都在达到模型轻量化的情况下也具有很高的精度。

基于此我也选择训练一个PULC模型,我使用的是分类任务中-交通标志分类的PPLCNet_x1_0配置文件,在/home/aistudio/PaddleClas/ppcls/configs/PULC/traffic_sign当中。

通过上图比较可以发现,该模型在数据增强方面,增加了RandomErasing和RandomAugment方法。除此之外训练参数也进行了相应的优化,选择的最优参数,该模型配置文件也对应着项目文件里的/home/aistudio/PPLCNet_x1_0.yaml

训练300个epoch的各项指标如下:

可以看出改进后的PPLCNET在eval_acc上有了显著的提升,各项指标也都得到了优化,并且该模型也是轻量化模型,最终也是采用该模型作为本项目的图像分类模型。

四 目标检测数据集制作

1、把原数据集中所有的图像转移至一个新的文件夹里面存储所有的图片。(便于线下标注时不用挨个选择文件夹进行标注)

2、修改xml文件信息,虽然目前的xml文件只有一头牛的标注信息,不过也简化了部分标注工作。

3、该部分标注标签统一只设为1,所有类别都是相同的标签。

#处理annotations -----------修改所有xml文件里面的图片路径为当前图片路径
#因为要在本地标注数据集,所以这里的rootpath设置的是我本地数据集图片存储的位置
def change_xmlDetect(annoPath, newannoPath, rootpath="E:\PPDE\\datas\FriesianCattle2017\\"):
    if not os.path.exists(newannoPath):
        os.mkdir(newannoPath)
    for annodir in os.listdir(annoPath):
        if 'xml' in annodir:
            annofilepath = os.path.join(annoPath, annodir)
            tree = ET.parse(annofilepath)
            # tree.findall("path")[0].text = tree.findall("path")[0].text.replace("D:\Research\Datasets\cattle", "\home\aistudio")
            # tree.findall("path")[0].text = "E:\PPDE\\datas\FriesianCattle2017" + tree.findall("path")[0].text.split("FriesianCattle2017")[1]
            tree.findall("path")[0].text = rootpath + tree.findall("filename")[0].text #修改文件路径
            tree.findall("path")[0].text = tree.findall("path")[0].text.replace("\\", "/")
            # tree.findall('object')[0].find('name').text = 1
            for obj in tree.findall("object"):
                obj.find('name').text = str(1)#修改所有的类标签为1
                # print(newannoPath,annodir)
            tree.write(os.path.join(newannoPath, annodir))
def remove_images(imgrootPath, savepath):
    if not os.path.exists(savepath):
        os.mkdir(savepath)
    for filedir in os.listdir(imgrootPath):
        filePath = os.path.join(imgrootPath, filedir)
        if os.path.isfile(filePath):
            continue        
        for imgdir in os.listdir(filePath):
            if 'jpg' not in imgdir:
                continue
            imgpath = os.path.join(filePath, imgdir)
            newImgPath = os.path.join(savepath, imgdir)
            shutil.copyfile(imgpath, newImgPath)

annoPath='/home/aistudio/FriesianCattle2017_Annotations/'
newannoPath = '/home/aistudio/detect_annotations1/'
imgsPath = "/home/aistudio/FriesianCattle2017/"
savePath = '/home/aistudio/detect_images/'
change_xmlDetect(annoPath, newannoPath)
remove_images(imgsPath, savePath)

在准备好上述步骤后,生成了两个文件:detect_iamges 和 detect_annotations,将这两个文件下载到本地,然后使用labelimg进行数据标注。

#这里我已经在本地把数据集重新标注了一遍上传了过来
%cd /home/aistudio
!rar x /home/aistudio/detect_annotations.rar
#经过这步后,detect_annotations里面的已经是重新标注后的文件。
#同样的修改一下xml配置文件中的图片路径信息(path)
change_xmlDetect("/home/aistudio/detect_annotations", "/home/aistudio/detect_annotations", rootpath="\home\\aistudio\\detect_images\\")
#划分训练集和测试集
#生成train.txt和val.txt
def split_datas(xml_dir, img_dir):
    random.seed(2020)
    path_list = list()
    for img in os.listdir(img_dir):
        img_path = os.path.join(img_dir,img)
        xml_path = os.path.join(xml_dir,img.replace('jpg', 'xml'))
        path_list.append((img_path, xml_path))
    random.shuffle(path_list)
    ratio = 0.9
    train_f = open('/home/aistudio/work/train.txt','w') 
    val_f = open('/home/aistudio/work/val.txt' ,'w')

    for i ,content in enumerate(path_list):
        img, xml = content
        text = img + ' ' + xml + '\n'
        if i < len(path_list) * ratio:
            train_f.write(text)
        else:
            val_f.write(text)
    train_f.close()
    val_f.close()

    #生成标签文档
    label = ['1']
    with open('/home/aistudio/work/label_list.txt', 'w') as f:
        for text in label:
            f.write(text+'\n')
xml_dir  = '/home/aistudio/detect_annotations'
img_dir = '/home/aistudio/detect_images'
split_datas(xml_dir, img_dir)
#习惯用COCO数据集格式,所以接下来数据集格式转变
#使用PaddleDetection 里面的代码实现
%cd /home/aistudio/PaddleDetection 
!python tools/x2coco.py \
        --dataset_type voc \
        --voc_anno_dir /home/aistudio/detect_annotations \
        --voc_anno_list /home/aistudio/work/train.txt \
        --voc_label_list /home/aistudio/work/label_list.txt \
        --voc_out_name /home/aistudio/train.json
!python tools/x2coco.py \
        --dataset_type voc \
        --voc_anno_dir /home/aistudio/detect_annotations \
        --voc_anno_list /home/aistudio/work/val.txt \
        --voc_label_list /home/aistudio/work/label_list.txt \
        --voc_out_name /home/aistudio/val.json

经过以上步骤

detect_images : 目标检测训练集

detect_annotations: 目标检测标注文件

接下来就可以进行目标检测模型训练了

五 PaddleDetection目标检测训练

该部分内容我还是重新建了一个DetTrain.ipynb来执行以下内容。

该部分需要的文件有:

1\配置文件:

/home/aistudio/picodet_640_reader.yml

/home/aistudio/picodet_l_640_coco_lcnet.yml

/home/aistudio/optimizer_300e.yml

/home/aistudio/coco_detection.yml

主要修改coco_detection(路径配置) 和 optimizer(训练次数)。

2、数据集:

detect_images : 目标检测训练集

train.json

val.json 标注文件

执行DetTrain.ipynb

5.1 训练

使用修改好后的配置文件一键训练

#选好模型,写好配置文件
#训练
%cd /home/aistudio/PaddleDetection/
%env PYTHONPATH=.:$PYTHONPATH
%env CUDA_VISIBLE_DEVICES=0,1,2,3
# !python setup.py install
# 覆盖文件(做了修改)
!cp /home/aistudio/optimizer_300e.yml /home/aistudio/PaddleDetection/configs/picodet/_base_/optimizer_300e.yml
!cp /home/aistudio/picodet_l_640_coco_lcnet.yml /home/aistudio/PaddleDetection/configs/picodet/picodet_l_640_coco_lcnet.yml
!cp /home/aistudio/picodet_640_reader.yml /home/aistudio/PaddleDetection/configs/picodet/_base_/picodet_640_reader.yml
!cp /home/aistudio/coco_detection.yml /home/aistudio/PaddleDetection/configs/datasets/coco_detection.yml
!mkdir /home/aistudio/PaddleDetection/output/
!mkdir /home/aistudio/PaddleDetection/output/picodet/
# !mv /home/aistudio/best_model.pdema /home/aistudio/PaddleDetection/output/picodet/best_model.pdema
# !mv /home/aistudio/best_model.pdopt /home/aistudio/PaddleDetection/output/picodet/best_model.pdopt
# !mv /home/aistudio/best_model.pdparams /home/aistudio/PaddleDetection/output/picodet/best_model.pdparams
!python /home/aistudio/PaddleDetection/tools/train.py -c /home/aistudio/PaddleDetection/configs/picodet/picodet_l_640_coco_lcnet.yml --use_vdl=true --vdl_log_dir=/home/aistudio/PaddleDetection/picodet --eval 
#导出模型
%cd PaddleDetection
!python /home/aistudio/PaddleDetection/tools/export_model.py -c  /home/aistudio/PaddleDetection/configs/picodet/picodet_l_640_coco_lcnet.yml --output_dir=/home/aistudio/det_inference \
 -o weights=/home/aistudio/PaddleDetection/output/picodet_l_640_coco_lcnet/best_model
#预测
%cd ~/PaddleDetection/
!python deploy/python/infer.py --model_dir=/home/aistudio/det_inference/picodet_l_640_coco_lcnet --image_dir=/home/aistudio/testImages/ --device=GPU --threshold=0.5

5.2 结果

在该部分,考虑整体使用轻量化模型,所以就只使用了picodet目标检测模型进行训练,对于该模型的分数也是可以接受的范围之内。

六 Detect+Recongnize

接下来进行二者的融合

该部分代码都在/home/aistudio/deploy

# -t 待预测图像根目录 -s 结果图像保存路径
%cd /home/aistudio/deploy
# !pip install paddleclas
!python predict_all.py -t "/home/aistudio/testImages" -s "/home/aistudio/output/"

最终结果

总结

本文使用PaddleDetection完成目标检测任务,PaddleClas完成分类任务,分别讲述了对应的实现方法以及对应的数据集制作方法,以及模型优化的方法。

本质上算是重头到尾复现了一遍没有向量检索的PP-ShiTu,跟PP-ShiTu大致流程类似,整体项目的两个模型也采用轻量化的模型,这样有助于在大多数算力较低的设备上进行部署,并且本文训练的两个模型都具有不错的效果。

在本文当中,对原数据进行了标注,每个文件夹下有一个主体牛牛,即主要包含了目标牛牛,知道这个牛牛的ID可以是文件夹名的ID,但是其余牛牛的ID(该图像中的其余牛牛),并不能知道,除非主观的先认识每一个牛牛,然后在标注的时候标注上他的ID。

改进

1、标注方式的改进

人工标注还是很费时间的,有幸于当前数据量并不多并且部分已经标了部分标签。对于数据量多的话,可以采用EasyDL。

3、模型改进

图像分类模型训练当中,最初使用的是PPLCNETV2和ResNet50作为本项目的基线模型,可是二者都没有表现出很好的分数,eval_acc基本都在0.7左右,并不是很理想,经过初步分析人为是数据集太少的原因,在本项目当中,只有900图像数据集,200张作为验证,700张作为训练。所以我从图像数据增强方向上去进行优化采用了官网在PULC上使用的RandomErasing和RandomAugment方法,经过优化的模型在eval_acc上也是提升至了0.9,在保持推理速度的同时也获得了更高的准确率。

最后本文也是提供了一个从只有图像数据到任务实现的全流程,希望可以帮助大家也着手于自己的项目当中。


此文章为搬运
原项目链接

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐