头图

0 简介

YOLOv3是业界当前比较流行的一种目标检测算法,具备识别准确度高及识别速度快的优势。本文将主要记录在应用该算法实现对自身构建数据集的训练过程中,比较重要的实现流程工作以及一些踩过的坑。至于YOLOv3算法的逻辑以及代码细节网上已经有很多相关的文章,在本文末尾会给出我看过的其中一些优质文章链接,之后也会自己单独写一篇文章做相应记录。在本文中,一些值得关注的点包括:

1. 自身数据集的构建
2. 锚框的聚类与尺度缩放
3. 目标与属性同时识别
4. backbone网络的替换(MobileNetV2)

1 YOLOv3实现

在应用过程中,基于Keras版的YOLOv3实现,项目链接为:https://github.com/qqwweee/ke...

2 实现过程

2.1 数据集构建

收集包含不同类别的待识别目标的图片,在原代码实现中要求图片是jpg格式,但只是因为在后续构建读取路径时写死了图片路径为xxx.jpg,可以根据需要在相应位置修改为支持多格式图片,这个很简单。在本次实现过程中,每类待识别目标相关的图片数目约为850张左右,仅做参考。

2.1.1 视频转图片

我在构建数据集的过程中是先收集相应的视频,再做帧拆分,将采样的帧保存为图像。对拆分出的图像做初步筛选,过滤掉画面模糊的、与已选择保留图像高度相似的以及不包含待识别目标的(不包含目标图像可作为环境样本加入到训练集中提升识别效果,本次实现没有验证,不作过多讨论),剩下的即为待标注数据集。这里给出将视频拆分成帧做采样后保存的方法(ffmpeg):

# -r 10表示每秒取10张
>ffmpeg -i your_path/xxx.mp4 -r 10 -f image2 your_target_path/{your_category}_%05d.png

2.1.2 数据标注

使用labelImg图片标注工具,标注目标位置框,目标类别,目标涉及属性,其中属性可同时标注多个。为了便于后续处理成训练文件,需要自定义一种标注格式。如在本次应用中,标注格式为 品类:属性1,属性2,属性3,...。使用labelImg工具标注后会生成每张图片相对应的.xml文件,记录标注框的位置,目标类别,目标属性等信息。

2.1.3 训练文件准备

在/model_data目录下创建自己的训练相关文件,包括:

  1. 类别文件:待识别目标的类别,每行一个
  2. 属性文件:待识别目标的不同属性,每行一个
  3. 锚框文件:需要通过聚类训练集标注框(聚类见2.1.4),得到9个不同尺度的anchor(tiny_yolo实现为6个)
  4. 训练文件:通过voc_annotation.py文件生成,需要将类别修改为自己的类别,如同时识别属性还需要仿照类别信息定义并增加属性信息。生成的训练文件中每行代表一条训练数据,格式为 img_path xmin,ymin,xmax,y_max,class,attribute1#attribute2 其中img_path为当前图片的存储路径,一般放在data目录下,xmin,ymin,xmax,y_max为相应.xml文件中矩形标注框的四个位置坐标,#为自定义属性分隔符。
# 解析.xml标注文件,生成训练记录
def convert_annotation(self, name, image_id, list_file):
    in_file = open('./data/%s/%s.xml' % (name, image_id))
    tree = ET.parse(in_file)
    root = tree.getroot()
    for obj in root.iter('object'):
        difficult = obj.find('difficult').text
        line = obj.find('name').text
        line = self.ch_to_en_punctuation(line) # 中文标点转英文标点
        arr = line.split(':')
        cls = arr[0]
        if cls not in classes or int(difficult) == 1:
            continue
        cls_id = classes.index(cls)
        attr_ids = []
        if len(arr) == 1:
            arr.append('无属性')
        attrs = arr[1].split(',')
        for attr in attrs:
            if attr not in attributes:
                continue
        attr_ids.append(str(attributes.index(attr)))
        xmlbox = obj.find('bndbox')
        b = (int(xmlbox.find('xmin').text), int(xmlbox.find('ymin').text), int(xmlbox.find('xmax').text), int(xmlbox.find('ymax').text))
        list_file.write(" " + ",".join([str(a) for a in b]) + ',' + str(cls_id) + ',' + '#'.join(attr_ids))

2.1.4 锚框聚类与缩放

基于对构建数据集标注生成.xml文件中的标注框位置信息,聚合成9个不同尺度的anchors,用于后续训练与识别。在不同数据集上进行聚类过程中发现,该实现版本中采用的聚类准确率计算方式,准确率最高时约为88%左右,正常会在81%以上。聚类前的一个关键步骤是要将原图的标注框的坐标映射到设定大小(如,416*416)下的坐标,生成每个图片标注框转换后坐标的文件用于后续聚类,该过程的代码实现为:

def load_dataset(dataset_path: str, resolution_ratio: int = 416):
    """
    加载标注数据集,构建所选分辨率下的锚框大小统计 
    :param dataset_path: 标注数据集路径 
    :param resolution_ratio: 设定的识别input大小
    :return:
    """ 
    dataset = []
    for xml_file in glob.glob("{}/*xml".format(dataset_path)):
        data = []
        tree = ET.parse(xml_file)
        height = int(tree.findtext("./size/height"))
        width = int(tree.findtext("./size/width"))
        for obj in tree.iter("object"):
            xmin = int(obj.findtext("bndbox/xmin")) / width
            ymin = int(obj.findtext("bndbox/ymin")) / height
            xmax = int(obj.findtext("bndbox/xmax")) / width
            ymax = int(obj.findtext("bndbox/ymax")) / height
            xmin = np.float64(xmin)
            ymin = np.float64(ymin)
            xmax = np.float64(xmax)
            ymax = np.float64(ymax)
            if xmax == xmin or ymax == ymin:
                print(xml_file)
                continue
            ratio = min(width, height) / max(width, height)
            if width < height:
                x_min_corrected = (1-ratio)/2 + ratio * xmin
                x_max_corrected = (1-ratio)/2 + ratio * xmax
                data.append((resolution_ratio*x_min_corrected, resolution_ratio*ymin, resolution_ratio*x_max_corrected, resolution_ratio*ymax))
            else:
                y_min_corrected = (1-ratio)/2 + ratio * ymin
                y_max_corrected = (1-ratio)/2 + ratio * ymax
                data.append((resolution_ratio*xmin, resolution_ratio*y_min_corrected, resolution_ratio*xmax, resolution_ratio*y_max_corrected))
        dataset.append(data)
    return dataset

聚类生成的锚框信息以文件的形式保存在model_data目录下。

由于在收集图像过程中,目标间大小差距相对较小,使得聚类后的锚框多尺度表达能力不足,可适当对聚类后的锚框做一定程度调整缩放。该部分是在网上查找相应的问题发现的一种处理方法,基于该方法的思想做了一些修改以达到我们得目的,找到原贴后会贴出,实现代码如下:

def anchors_transform(input_anchor_path, output_anchor_path, ratio=0.8, base_ratio=1):
    """
    对聚合度较高的k-means生成锚框做一定的变换,提升泛化能力 
    :param input_anchor_path: k-means生成锚框路径 
    :param output_anchor_path: 变换后保存路径 
    :param ratio: 最小的锚框缩小倍数 
    :param base_ratio: 最大的锚框扩大倍数 
    :return:
    """ 
    anchor = open(input_anchor_path)
    box = anchor.read().split(',')
    box = np.array(box)
    for i in range(0, len(box)):
        box[i] = float(box[i].strip())
    box = box.reshape((len(box)//2, 2))
    new_box = np.zeros((len(box), 2))
    length = len(box)
    new_box[0, 0] = int(float(box[0, 0])*ratio)
    new_box[0, 1] = int(float(box[0, 1])*ratio)
    new_box[-1, 0] = int(float(box[-1, 0])*base_ratio)
    new_box[-1, 1] = int(float(box[-1, 1])*base_ratio)
    for i in range(1, length-1):
        new_box[i, 0] = (float(box[i, 0])-float(box[0, 0]))/(float(box[-1, 0])-float(box[0, 0]))*(new_box[-1, 0]-new_box[0, 0])+new_box[0, 0]
        new_box[i, 1] = new_box[i, 0]*float(box[i, 1])/float(box[i, 0])
        new_box[i, 0] = int(new_box[i, 0])
        new_box[i, 1] = int(new_box[i, 1])
    expand_anchor = open(output_anchor_path, 'w')
    for i in range(length):
        if i == 0:
            x_y = "%d,%d" % (new_box[i][0], new_box[i][1])
        else:
            x_y = ", %d,%d" % (new_box[i][0], new_box[i][1])
        expand_anchor.write(x_y)
    expand_anchor.close()
    anchor.close()

2.1.5 增加属性识别代码调整

增加目标对应属性的识别,一个可以简单理解的结论是:代码中只要出现num_classes(或classes)的地方,补充num_attributes(或attributes),如原来实现中(num_classes + 5)要变为 (num_classes + 5 + num_attributes),函数参数中有num_classes也要补充对应的num_attributes。当然,这个说法也不一定绝对,需要根据代码的理解进行调整,主要的修改位置还是在yolo.py与yolo3/model.py文件中。

2.1.6 模型训练

模型训练主要依赖的几个文件为:

1. 训练图像目录
2. 训练数据文件,包含图像位置,框位置,类别,属性:XXX_bndbox.txt
3. 聚类后的锚框文件:XXX_anchors.txt
4. 定义的类别文件:XXX_classes.txt
5. 定义的属性文件:XXX_attributes.txt

要注意,类别文件与属性文件中的项之间的顺序(index)要与生成的训练数据文件相应类别与属性的index保持一致。

可根据实现目的对训练过程的损失函数的各部分增加权重,如:

# 调整confidence_loss与class_loss的权重
loss += xy_loss + wh_loss + 1.5*confidence_loss + 3*class_loss + attribute_loss

3 backbone网络替换

原实现使用Darknet53作为backbone网络,推理速度较慢,根据需要调整backbone网络。主要修改点(相比于yolov3/model.py)还是模型本身的结构定义以及从哪一层构建实现32倍,16倍以及8倍下采样,具体可参考下述实现:

3.0 mobilenetV2模型声明

from tensorflow.keras.applications.mobilenet_v2 import MobileNetV2

3.1 替换为mobilenetV2

3.1.1 mobilenetv2_yolo_body(),对应于原实现中yolo_body( )
image
image

3.2.2 make_last_layers_mobilenet( )
image
image

3.3.3 MobilenetSeparableConv2D( )
image

4 测试

配置好yolo.py中YOLO类_defaults={...}内容,如调整backbone网络需在generate()函数中定义yolo_model时用新的backbone网络类初始化,如:

self.yolo_model = mobilenetv2_yolo_body(Input(shape=(None, None, 3)), num_anchors//3, num_classes, num_attributes)

选取想要测试的视频或图片,调用在yolo_video.py中detect_video( )或detect_img( )方法检测。

5 参考:

记录一些在学习过程中看过的比较优质的文章,更多的是算法理论与实现:

  1. YOLOv3原论文:https://arxiv.org/pdf/1506.02...
  2. 《YOLOv3》深度解析:https://zhuanlan.zhihu.com/p/...
  3. PaddlePaddle《目标检测之YOLOv3算法实现》:https://zhuanlan.zhihu.com/p/...
  4. 《YOLOv3原理代码赏析》:https://zhuanlan.zhihu.com/p/...

6 补充:

整个流程涉及的内容多,有一些地方解释的可能不够清楚,由于时间关系先大致给自己记录一下,后续有时间会对文档进行补充优化。


CRANOLA
1 声望1 粉丝

引用和评论

0 条评论