对计算机而言,能够“看到”的是图像被编码之后的数字,但它很难解高层语义概念,比如图像或者视频帧中出现目标的是人还是物体,更无法定位目标出现在图像中哪个区域。目标检测的主要目的是让计算机可以自动识别图片或者视频帧中所有目标的类别,并在该目标周围绘制边界框,标示出每个目标的位置,如 图1 所示。

目标检测 - 图1

图1:图像分类和目标检测示意图

  • 图1(a)是图像分类任务,只需识别出这是一张斑马的图片。
  • 图1(b)是目标检测任务,不仅要识别出这是一张斑马的图片,还要标出图中斑马的位置。

目标检测发展历程

在上一节中我们学习了图像分类处理基本流程,先使用卷积神经网络提取图像特征,然后再用这些特征预测分类概率,根据训练样本标签建立起分类损失函数,开启端到端的训练,如 图2 所示。

目标检测 - 图2

图2:图像分类流程示意图

但对于目标检测问题,按照 图2 的流程则行不通。因为在图像分类任务中,对整张图提取特征的过程中没能体现出不同目标之间的区别,最终也就没法分别标示出每个物体所在的位置。

为了解决这个问题,结合图片分类任务取得的成功经验,我们可以将目标检测任务进行拆分。假设我们现在有某种方式可以在输入图片上生成一系列可能包含物体的区域,这些区域称为候选区域,在一张图上可以生成很多个候选区域。然后对每个候选区域,可以把它单独当成一幅图像来看待,使用图像分类模型对它进行分类,看它属于哪个类别或者背景(即不包含任何物体的类别)。

上一节我们学过如何解决图像分类任务,使用卷积神经网络对一幅图像进行分类不再是一件困难的事情。那么,现在问题的关键就是如何产生候选区域?比如我们可以使用穷举法来产生候选区域,如图3所示。

目标检测 - 图3

图3:候选区域

A为图像上的某个像素点,B为A右下方另外一个像素点,A、B两点可以确定一个矩形框,记作AB。

  • 如图3(a)所示:A在图片左上角位置,B遍历除A之外的所有位置,生成矩形框A1B1, …, A1Bn, …
  • 如图3(b)所示:A在图片中间某个位置,B遍历A右下方所有位置,生成矩形框AkB1, …, AkBn, …

当A遍历图像上所有像素点,B则遍历它右下方所有的像素点,最终生成的矩形框集合{AiBj}将会包含图像上所有可以选择的区域。

只要我们对每个候选区域的分类足够的准确,则一定能找到跟实际物体足够接近的区域来。穷举法也许能得到正确的预测结果,但其计算量也是非常巨大的,其所生成的总的候选区域数目约为

目标检测 - 图4 ,假设 目标检测 - 图5 ,总数将会达到 目标检测 - 图6 个,如此多的候选区域使得这种方法几乎没有什么实用性。但是通过这种方式,我们可以看出,假设分类任务完成的足够完美,从理论上来讲检测任务也是可以解决的,亟待解决的问题是如何设计出合适的方法来产生候选区域。

科学家们开始思考,是否可以应用传统图像算法先产生候选区域,然后再用卷积神经网络对这些区域进行分类?

  • 2013年,Ross Girshick 等人于首次将CNN的方法应用在目标检测任务上,他们使用传统图像算法selective search产生候选区域,取得了极大的成功,这就是对目标检测领域影响深远的区域卷积神经网络(R-CNN)模型。
  • 2015年,Ross Girshick 对此方法进行了改进,提出了Fast RCNN模型。通过将不同区域的物体共用卷积层的计算,大大缩减了计算量,提高了处理速度,而且还引入了调整目标物体位置的回归方法,进一步提高了位置预测的准确性。
  • 2015年,Shaoqing Ren 等人提出了Faster RCNN模型,提出了RPN的方法来产生物体的候选区域,这一方法里面不再需要使用传统的图像处理算法来产生候选区域,进一步提升了处理速度。
  • 2017年,Kaiming He 等人于提出了Mask RCNN模型,只需要在Faster RCNN模型上添加比较少的计算量,就可以同时实现目标检测和物体实例分割两个任务。

以上都是基于R-CNN系列的著名模型,对目标检测方向的发展有着较大的影响力。此外,还有一些其他模型,比如SSD、YOLO(1, 2, 3)、R-FCN等也都是目标检测领域流行的模型结构。

R-CNN的系列算法分成两个阶段,先在图像上产生候选区域,再对候选区域进行分类并预测目标物体位置,它们通常被叫做两阶段检测算法。SSD和YOLO算法则只使用一个网络同时产生候选区域并预测出物体的类别和位置,所以它们通常被叫做单阶段检测算法。由于篇幅所限,本章将重点介绍YOLO-V3算法,并用其完成林业病虫害数据集中的昆虫检测任务,主要涵盖如下内容:

  • 图像检测基础概念:介绍与目标检测任相关的基本概念,包括边界框、锚框和交并比等。
  • 林业病虫害数据集:介绍数据集结构及数据预处理方法。
  • YOLO-V3目标检测模型:介绍算法原理,及如何应用林业病虫害数据集进行模型训练和测试。

目标检测基础概念

在介绍目标检测算法之前,先介绍一些跟检测相关的基本概念,包括边界框、锚框和交并比等。

边界框(bounding box)

检测任务需要同时预测物体的类别和位置,因此需要引入一些跟位置相关的概念。通常使用边界框(bounding box,bbox)来表示物体的位置,边界框是正好能包含住物体的矩形框,如 图4 所示,图中3个人分别对应3个边界框。

目标检测 - 图7

图4:边界框

通常有两种格式来表示边界框的位置:

  • xyxy,即 目标检测 - 图8 ,其中 目标检测 - 图9 是矩形框左上角的坐标, 目标检测 - 图10 是矩形框右下角的坐标。图4中3个红色矩形框用xyxy格式表示如下:
  • 左: 目标检测 - 图11
  • 中: 目标检测 - 图12
  • 右: 目标检测 - 图13
  • xywh,即 目标检测 - 图14 ,其中 目标检测 - 图15 是矩形框中心点的坐标,w是矩形框的宽度,h是矩形框的高度。

在检测任务中,训练数据集的标签里会给出目标物体真实边界框所对应的

目标检测 - 图16 ,这样的边界框也被称为真实框(ground truth box),如 图4 所示,图中画出了3个人像所对应的真实框。模型会对目标物体可能出现的位置进行预测,由模型预测出的边界框则称为预测框(prediction box)。


注意:

  • 在阅读代码时,请注意使用的是哪一种格式的表示方式。
  • 图片坐标的原点在左上角,x轴向右为正方向,y轴向下为正方向。

要完成一项检测任务,我们通常希望模型能够根据输入的图片,输出一些预测的边界框,以及边界框中所包含的物体的类别或者说属于某个类别的概率,例如这种格式:

目标检测 - 图17 ,其中L是类别标签,P是物体属于该类别的概率。一张输入图片可能会产生多个预测框,接下来让我们一起学习如何完成这样一项任务。

锚框(Anchor)

锚框与物体边界框不同,是由人们假想出来的一种框。先设定好锚框的大小和形状,再以图像上某一个点为中心画出矩形框。在下图中,以像素点[300, 500]为中心可以使用下面的程序生成3个框,如图中蓝色框所示,其中锚框A1跟人像区域非常接近。

  1. # 画图展示如何绘制边界框和锚框
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. import matplotlib.patches as patches
  5. from matplotlib.image import imread
  6. import math
  7. # 定义画矩形框的程序
  8. def draw_rectangle(currentAxis, bbox, edgecolor = 'k', facecolor = 'y', fill=False, linestyle='-'):
  9. # currentAxis,坐标轴,通过plt.gca()获取
  10. # bbox,边界框,包含四个数值的list, [x1, y1, x2, y2]
  11. # edgecolor,边框线条颜色
  12. # facecolor,填充颜色
  13. # fill, 是否填充
  14. # linestype,边框线型
  15. # patches.Rectangle需要传入左上角坐标、矩形区域的宽度、高度等参数
  16. rect=patches.Rectangle((bbox[0], bbox[1]), bbox[2]-bbox[0]+1, bbox[3]-bbox[1]+1, linewidth=1,
  17. edgecolor=edgecolor,facecolor=facecolor,fill=fill, linestyle=linestyle)
  18. currentAxis.add_patch(rect)
  19. plt.figure(figsize=(10, 10))
  20. filename = '/home/aistudio/work/images/section3/000000086956.jpg'
  21. im = imread(filename)
  22. plt.imshow(im)
  23. # 使用xyxy格式表示物体真实框
  24. bbox1 = [214.29, 325.03, 399.82, 631.37]
  25. bbox2 = [40.93, 141.1, 226.99, 515.73]
  26. bbox3 = [247.2, 131.62, 480.0, 639.32]
  27. currentAxis=plt.gca()
  28. draw_rectangle(currentAxis, bbox1, edgecolor='r')
  29. draw_rectangle(currentAxis, bbox2, edgecolor='r')
  30. draw_rectangle(currentAxis, bbox3,edgecolor='r')
  31. # 绘制锚框
  32. def draw_anchor_box(center, length, scales, ratios, img_height, img_width):
  33. """
  34. 以center为中心,产生一系列锚框
  35. 其中length指定了一个基准的长度
  36. scales是包含多种尺寸比例的list
  37. ratios是包含多种长宽比的list
  38. img_height和img_width是图片的尺寸,生成的锚框范围不能超出图片尺寸之外
  39. """
  40. bboxes = []
  41. for scale in scales:
  42. for ratio in ratios:
  43. h = length*scale*math.sqrt(ratio)
  44. w = length*scale/math.sqrt(ratio)
  45. x1 = max(center[0] - w/2., 0.)
  46. y1 = max(center[1] - h/2., 0.)
  47. x2 = min(center[0] + w/2. - 1.0, img_width - 1.0)
  48. y2 = min(center[1] + h/2. - 1.0, img_height - 1.0)
  49. print(center[0], center[1], w, h)
  50. bboxes.append([x1, y1, x2, y2])
  51. for bbox in bboxes:
  52. draw_rectangle(currentAxis, bbox, edgecolor = 'b')
  53. img_height = im.shape[0]
  54. img_width = im.shape[1]
  55. draw_anchor_box([300., 500.], 100., [2.0], [0.5, 1.0, 2.0], img_height, img_width)
  56. ################# 以下为添加文字说明和箭头###############################
  57. plt.text(285, 285, 'G1', color='red', fontsize=20)
  58. plt.arrow(300, 288, 30, 40, color='red', width=0.001, length_includes_head=True, \
  59. head_width=5, head_length=10, shape='full')
  60. plt.text(190, 320, 'A1', color='blue', fontsize=20)
  61. plt.arrow(200, 320, 30, 40, color='blue', width=0.001, length_includes_head=True, \
  62. head_width=5, head_length=10, shape='full')
  63. plt.text(160, 370, 'A2', color='blue', fontsize=20)
  64. plt.arrow(170, 370, 30, 40, color='blue', width=0.001, length_includes_head=True, \
  65. head_width=5, head_length=10, shape='full')
  66. plt.text(115, 420, 'A3', color='blue', fontsize=20)
  67. plt.arrow(127, 420, 30, 40, color='blue', width=0.001, length_includes_head=True, \
  68. head_width=5, head_length=10, shape='full')
  69. #draw_anchor_box([200., 200.], 100., [2.0], [0.5, 1.0, 2.0])
  70. plt.show()
  1. 300.0 500.0 282.84271247461896 141.4213562373095
  2. 300.0 500.0 200.0 200.0
  3. 300.0 500.0 141.42135623730948 282.842712474619
  1. <Figure size 1000x1000 with 1 Axes>

在目标检测模型中,通常会以某种规则在图片上生成一系列锚框,将这些锚框当成可能的候选区域。模型对这些候选区域是否包含物体进行预测,如果包含目标物体,则还需要进一步预测出物体所属的类别。还有更为重要的一点是,由于锚框位置是固定的,它不大可能刚好跟物体边界框重合,所以需要在锚框的基础上进行微调以形成能准确描述物体位置的预测框,模型需要预测出微调的幅度。在训练过程中,模型通过学习不断的调整参数,最终能学会如何判别出锚框所代表的候选区域是否包含物体,如果包含物体的话,物体属于哪个类别,以及物体边界框相对于锚框位置需要调整的幅度。

不同的模型往往有着不同的生成锚框的方式,在后面的内容中,会详细介绍YOLO-V3算法里面产生锚框的规则,理解了它的设计方案,也很容易类推到其它模型上。

交并比

上面我们画出了以点

目标检测 - 图18 为中心,生成的三个锚框,我们可以看到锚框A1 与真实框 G1的重合度比较好。那么如何衡量这三个锚框跟真实框之间的关系呢,在检测任务中是使用交并比(Intersection of Union,IoU)作为衡量指标。这一概念来源于数学中的集合,用来描述两个集合 目标检测 - 图19目标检测 - 图20 之间的关系,它等于两个集合的交集里面所包含的元素个数,除以它们的并集里面所包含的元素个数,具体计算公式如下:

目标检测 - 图21

我们将用这个概念来描述两个框之间的重合度。两个框可以看成是两个像素的集合,它们的交并比等于两个框重合部分的面积除以它们合并起来的面积。下图a中红色区域是两个框的重合面积,图b中蓝色区域是两个框的相并面积。用这两个面积相除即可得到它们之间的交并比,如 图5 所示。

目标检测 - 图22

图5:交并比

假设两个矩形框A和B的位置分别为:

目标检测 - 图23

目标检测 - 图24

假如位置关系如 图6 所示:

目标检测 - 图25

图6:计算交并比

如果二者有相交部分,则相交部分左上角坐标为:

目标检测 - 图26

相交部分右下角坐标为:

目标检测 - 图27

计算先交部分面积:

目标检测 - 图28

矩形框A和B的面积分别是:

目标检测 - 图29

目标检测 - 图30

计算相并部分面积:

目标检测 - 图31

计算交并比:

目标检测 - 图32


思考:

两个矩形框之间的相对位置关系,除了上面的示意图之外,还有哪些可能,上面的公式能否覆盖所有的情形?


并交比计算程序如下:

  1. # 计算IoU,矩形框的坐标形式为xyxy,这个函数会被保存在box_utils.py文件中
  2. def box_iou_xyxy(box1, box2):
  3. # 获取box1左上角和右下角的坐标
  4. x1min, y1min, x1max, y1max = box1[0], box1[1], box1[2], box1[3]
  5. # 计算box1的面积
  6. s1 = (y1max - y1min + 1.) * (x1max - x1min + 1.)
  7. # 获取box2左上角和右下角的坐标
  8. x2min, y2min, x2max, y2max = box2[0], box2[1], box2[2], box2[3]
  9. # 计算box2的面积
  10. s2 = (y2max - y2min + 1.) * (x2max - x2min + 1.)
  11. # 计算相交矩形框的坐标
  12. xmin = np.maximum(x1min, x2min)
  13. ymin = np.maximum(y1min, y2min)
  14. xmax = np.minimum(x1max, x2max)
  15. ymax = np.minimum(y1max, y2max)
  16. # 计算相交矩形行的高度、宽度、面积
  17. inter_h = np.maximum(ymax - ymin + 1., 0.)
  18. inter_w = np.maximum(xmax - xmin + 1., 0.)
  19. intersection = inter_h * inter_w
  20. # 计算相并面积
  21. union = s1 + s2 - intersection
  22. # 计算交并比
  23. iou = intersection / union
  24. return iou
  25. bbox1 = [100., 100., 200., 200.]
  26. bbox2 = [120., 120., 220., 220.]
  27. iou = box_iou_xyxy(bbox1, bbox2)
  28. print('IoU is {}'.format(iou))
  1. IoU is 0.47402644317607107
  1. # 计算IoU,矩形框的坐标形式为xywh
  2. def box_iou_xywh(box1, box2):
  3. x1min, y1min = box1[0] - box1[2]/2.0, box1[1] - box1[3]/2.0
  4. x1max, y1max = box1[0] + box1[2]/2.0, box1[1] + box1[3]/2.0
  5. s1 = box1[2] * box1[3]
  6. x2min, y2min = box2[0] - box2[2]/2.0, box2[1] - box2[3]/2.0
  7. x2max, y2max = box2[0] + box2[2]/2.0, box2[1] + box2[3]/2.0
  8. s2 = box2[2] * box2[3]
  9. xmin = np.maximum(x1min, x2min)
  10. ymin = np.maximum(y1min, y2min)
  11. xmax = np.minimum(x1max, x2max)
  12. ymax = np.minimum(y1max, y2max)
  13. inter_h = np.maximum(ymax - ymin, 0.)
  14. inter_w = np.maximum(xmax - xmin, 0.)
  15. intersection = inter_h * inter_w
  16. union = s1 + s2 - intersection
  17. iou = intersection / union
  18. return iou

为了直观的展示交并比的大小跟重合程度之间的关系,图7 示意了不同交并比下两个框之间的相对位置关系,从 IoU = 0.95 到 IoU = 0.

目标检测 - 图33

图7:不同交并比下两个框之间相对位置示意图


问题:

  • 什么情况下两个矩形框的IoU等于1?
  • 什么情况下两个矩形框的IoU等于0?

林业病虫害数据集和数据预处理方法介绍

在本次的课程中,将使用百度与林业大学合作开发的林业病虫害防治项目中用到昆虫数据集,关于该项目和数据集的更多信息,可以参考相关报道。在这一小节中将为读者介绍该数据集,以及计算机视觉任务中常用的数据预处理方法。

读取AI识虫数据集标注信息

AI识虫数据集结构如下:

  • 提供了2183张图片,其中训练集1693张,验证集245,测试集245张。
  • 包含7种昆虫,分别是Boerner、Leconte、Linnaeus、acuminatus、armandi、coleoptera和linnaeus。
  • 包含了图片和标注,请读者先将数据解压,并存放在insects目录下。
  1. # 解压数据脚本,第一次运行时打开注释,将文件解压到work目录下
  2. # !unzip -d /home/aistudio/work /home/aistudio/data/data19638/insects.zip

将数据解压之后,可以看到insects目录下的结构如下所示。

  1. insects
  2. |---train
  3. | |---annotations
  4. | | |---xmls
  5. | | |---100.xml
  6. | | |---101.xml
  7. | | |---...
  8. | |
  9. | |---images
  10. | |---100.jpeg
  11. | |---101.jpeg
  12. | |---...
  13. |
  14. |---val
  15. | |---annotations
  16. | | |---xmls
  17. | | |---1221.xml
  18. | | |---1277.xml
  19. | | |---...
  20. | |
  21. | |---images
  22. | |---1221.jpeg
  23. | |---1277.jpeg
  24. | |---...
  25. |
  26. |---test
  27. |---images
  28. |---1833.jpeg
  29. |---1838.jpeg
  30. |---...

insects包含train、val和test三个文件夹。train/annotations/xmls目录下存放着图片的标注。每个xml文件是对一张图片的说明,包括图片尺寸、包含的昆虫名称、在图片上出现的位置等信息。

  1. <annotation>
  2. <folder>刘霏霏</folder>
  3. <filename>100.jpeg</filename>
  4. <path>/home/fion/桌面/刘霏霏/100.jpeg</path>
  5. <source>
  6. <database>Unknown</database>
  7. </source>
  8. <size>
  9. <width>1336</width>
  10. <height>1336</height>
  11. <depth>3</depth>
  12. </size>
  13. <segmented>0</segmented>
  14. <object>
  15. <name>Boerner</name>
  16. <pose>Unspecified</pose>
  17. <truncated>0</truncated>
  18. <difficult>0</difficult>
  19. <bndbox>
  20. <xmin>500</xmin>
  21. <ymin>893</ymin>
  22. <xmax>656</xmax>
  23. <ymax>966</ymax>
  24. </bndbox>
  25. </object>
  26. <object>
  27. <name>Leconte</name>
  28. <pose>Unspecified</pose>
  29. <truncated>0</truncated>
  30. <difficult>0</difficult>
  31. <bndbox>
  32. <xmin>622</xmin>
  33. <ymin>490</ymin>
  34. <xmax>756</xmax>
  35. <ymax>610</ymax>
  36. </bndbox>
  37. </object>
  38. <object>
  39. <name>armandi</name>
  40. <pose>Unspecified</pose>
  41. <truncated>0</truncated>
  42. <difficult>0</difficult>
  43. <bndbox>
  44. <xmin>432</xmin>
  45. <ymin>663</ymin>
  46. <xmax>517</xmax>
  47. <ymax>729</ymax>
  48. </bndbox>
  49. </object>
  50. <object>
  51. <name>coleoptera</name>
  52. <pose>Unspecified</pose>
  53. <truncated>0</truncated>
  54. <difficult>0</difficult>
  55. <bndbox>
  56. <xmin>624</xmin>
  57. <ymin>685</ymin>
  58. <xmax>697</xmax>
  59. <ymax>771</ymax>
  60. </bndbox>
  61. </object>
  62. <object>
  63. <name>linnaeus</name>
  64. <pose>Unspecified</pose>
  65. <truncated>0</truncated>
  66. <difficult>0</difficult>
  67. <bndbox>
  68. <xmin>783</xmin>
  69. <ymin>700</ymin>
  70. <xmax>856</xmax>
  71. <ymax>802</ymax>
  72. </bndbox>
  73. </object>
  74. </annotation>

上面列出的xml文件中的主要参数说明如下:

-size:图片尺寸

-object:图片中包含的物体,一张图片可能中包含多个物体

  • name:昆虫名称
  • bndbox:物体真实框
  • difficult:识别是否困难

下面我们将从数据集中读取xml文件,将每张图片的标注信息读取出来。在读取具体的标注文件之前,我们先完成一件事情,就是将昆虫的类别名字(字符串)转化成数字表示的类别。因为神经网络里面计算时需要的输入类型是数值型的,所以需要将字符串表示的类别转化成具体的数字。昆虫类别名称的列表是:[‘Boerner’, ‘Leconte’, ‘Linnaeus’, ‘acuminatus’, ‘armandi’, ‘coleoptera’, ‘linnaeus’],这里我们约定此列表中:'Boerner’对应类别0,'Leconte’对应类别1,…,'linnaeus’对应类别6。使用下面的程序可以得到表示名称字符串和数字类别之间映射关系的字典。

  1. INSECT_NAMES = ['Boerner', 'Leconte', 'Linnaeus',
  2. 'acuminatus', 'armandi', 'coleoptera', 'linnaeus']
  3. def get_insect_names():
  4. """
  5. return a dict, as following,
  6. {'Boerner': 0,
  7. 'Leconte': 1,
  8. 'Linnaeus': 2,
  9. 'acuminatus': 3,
  10. 'armandi': 4,
  11. 'coleoptera': 5,
  12. 'linnaeus': 6
  13. }
  14. It can map the insect name into an integer label.
  15. """
  16. insect_category2id = {}
  17. for i, item in enumerate(INSECT_NAMES):
  18. insect_category2id[item] = i
  19. return insect_category2id
  1. cname2cid = get_insect_names()
  2. cname2cid
  1. {'Boerner': 0,
  2. 'Leconte': 1,
  3. 'Linnaeus': 2,
  4. 'acuminatus': 3,
  5. 'armandi': 4,
  6. 'coleoptera': 5,
  7. 'linnaeus': 6}

调用get_insect_names函数返回一个dict,其键-值对描述了昆虫名称-数字类别之间的映射关系。

下面的程序从annotations/xml目录下面读取所有文件标注信息。

  1. import os
  2. import numpy as np
  3. import xml.etree.ElementTree as ET
  4. def get_annotations(cname2cid, datadir):
  5. filenames = os.listdir(os.path.join(datadir, 'annotations', 'xmls'))
  6. records = []
  7. ct = 0
  8. for fname in filenames:
  9. fid = fname.split('.')[0]
  10. fpath = os.path.join(datadir, 'annotations', 'xmls', fname)
  11. img_file = os.path.join(datadir, 'images', fid + '.jpeg')
  12. tree = ET.parse(fpath)
  13. if tree.find('id') is None:
  14. im_id = np.array([ct])
  15. else:
  16. im_id = np.array([int(tree.find('id').text)])
  17. objs = tree.findall('object')
  18. im_w = float(tree.find('size').find('width').text)
  19. im_h = float(tree.find('size').find('height').text)
  20. gt_bbox = np.zeros((len(objs), 4), dtype=np.float32)
  21. gt_class = np.zeros((len(objs), ), dtype=np.int32)
  22. is_crowd = np.zeros((len(objs), ), dtype=np.int32)
  23. difficult = np.zeros((len(objs), ), dtype=np.int32)
  24. for i, obj in enumerate(objs):
  25. cname = obj.find('name').text
  26. gt_class[i] = cname2cid[cname]
  27. _difficult = int(obj.find('difficult').text)
  28. x1 = float(obj.find('bndbox').find('xmin').text)
  29. y1 = float(obj.find('bndbox').find('ymin').text)
  30. x2 = float(obj.find('bndbox').find('xmax').text)
  31. y2 = float(obj.find('bndbox').find('ymax').text)
  32. x1 = max(0, x1)
  33. y1 = max(0, y1)
  34. x2 = min(im_w - 1, x2)
  35. y2 = min(im_h - 1, y2)
  36. # 这里使用xywh格式来表示目标物体真实框
  37. gt_bbox[i] = [(x1+x2)/2.0 , (y1+y2)/2.0, x2-x1+1., y2-y1+1.]
  38. is_crowd[i] = 0
  39. difficult[i] = _difficult
  40. voc_rec = {
  41. 'im_file': img_file,
  42. 'im_id': im_id,
  43. 'h': im_h,
  44. 'w': im_w,
  45. 'is_crowd': is_crowd,
  46. 'gt_class': gt_class,
  47. 'gt_bbox': gt_bbox,
  48. 'gt_poly': [],
  49. 'difficult': difficult
  50. }
  51. if len(objs) != 0:
  52. records.append(voc_rec)
  53. ct += 1
  54. return records
  1. TRAINDIR = '/home/aistudio/work/insects/train'
  2. TESTDIR = '/home/aistudio/work/insects/test'
  3. VALIDDIR = '/home/aistudio/work/insects/val'
  4. cname2cid = get_insect_names()
  5. records = get_annotations(cname2cid, TRAINDIR)
  1. len(records)
  1. 1693
  1. records[0]
  1. {'im_file': '/home/aistudio/work/insects/train/images/1915.jpeg',
  2. 'im_id': array([0]),
  3. 'h': 1268.0,
  4. 'w': 1268.0,
  5. 'is_crowd': array([0, 0, 0, 0, 0, 0, 0], dtype=int32),
  6. 'gt_class': array([1, 0, 2, 3, 4, 5, 5], dtype=int32),
  7. 'gt_bbox': array([[411.5, 583. , 142. , 199. ],
  8. [654.5, 418.5, 128. , 132. ],
  9. [678.5, 736.5, 70. , 90. ],
  10. [758. , 647.5, 53. , 76. ],
  11. [843. , 538.5, 69. , 96. ],
  12. [559.5, 788. , 68. , 83. ],
  13. [831.5, 754.5, 56. , 56. ]], dtype=float32),
  14. 'gt_poly': [],
  15. 'difficult': array([0, 0, 0, 0, 0, 0, 0], dtype=int32)}

通过上面的程序,将所有训练数据集的标注数据全部读取出来了,存放在records列表下面,其中每一个元素是一张图片的标注数据,包含了图片存放地址,图片id,图片高度和宽度,图片中所包含的目标物体的种类和位置。

数据读取和预处理

数据预处理是训练神经网络时非常重要的步骤。合适的预处理方法,可以帮助模型更好的收敛并防止过拟合。首先我们需要从磁盘读入数据,然后需要对这些数据进行预处理,为了保证网络运行的速度通常还要对数据预处理进行加速。

数据读取

前面已经将图片的所有描述信息保存在records中了,其中的每一个元素包含了一张图片的描述,下面的程序展示了如何根据records里面的描述读取图片及标注。

  1. ### 数据读取
  2. import cv2
  3. def get_bbox(gt_bbox, gt_class):
  4. # 对于一般的检测任务来说,一张图片上往往会有多个目标物体
  5. # 设置参数MAX_NUM = 50, 即一张图片最多取50个真实框;如果真实
  6. # 框的数目少于50个,则将不足部分的gt_bbox, gt_class和gt_score的各项数值全设置为0
  7. MAX_NUM = 50
  8. gt_bbox2 = np.zeros((MAX_NUM, 4))
  9. gt_class2 = np.zeros((MAX_NUM,))
  10. for i in range(len(gt_bbox)):
  11. gt_bbox2[i, :] = gt_bbox[i, :]
  12. gt_class2[i] = gt_class[i]
  13. if i >= MAX_NUM:
  14. break
  15. return gt_bbox2, gt_class2
  16. def get_img_data_from_file(record):
  17. """
  18. record is a dict as following,
  19. record = {
  20. 'im_file': img_file,
  21. 'im_id': im_id,
  22. 'h': im_h,
  23. 'w': im_w,
  24. 'is_crowd': is_crowd,
  25. 'gt_class': gt_class,
  26. 'gt_bbox': gt_bbox,
  27. 'gt_poly': [],
  28. 'difficult': difficult
  29. }
  30. """
  31. im_file = record['im_file']
  32. h = record['h']
  33. w = record['w']
  34. is_crowd = record['is_crowd']
  35. gt_class = record['gt_class']
  36. gt_bbox = record['gt_bbox']
  37. difficult = record['difficult']
  38. img = cv2.imread(im_file)
  39. img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  40. # check if h and w in record equals that read from img
  41. assert img.shape[0] == int(h), \
  42. "image height of {} inconsistent in record({}) and img file({})".format(
  43. im_file, h, img.shape[0])
  44. assert img.shape[1] == int(w), \
  45. "image width of {} inconsistent in record({}) and img file({})".format(
  46. im_file, w, img.shape[1])
  47. gt_boxes, gt_labels = get_bbox(gt_bbox, gt_class)
  48. # gt_bbox 用相对值
  49. gt_boxes[:, 0] = gt_boxes[:, 0] / float(w)
  50. gt_boxes[:, 1] = gt_boxes[:, 1] / float(h)
  51. gt_boxes[:, 2] = gt_boxes[:, 2] / float(w)
  52. gt_boxes[:, 3] = gt_boxes[:, 3] / float(h)
  53. return img, gt_boxes, gt_labels, (h, w)
  1. record = records[0]
  2. img, gt_boxes, gt_labels, scales = get_img_data_from_file(record)
  1. img.shape
  1. (1268, 1268, 3)
  1. gt_boxes.shape
  1. (50, 4)
  1. gt_labels
  1. array([1., 0., 2., 3., 4., 5., 5., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
  2. 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
  3. 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
  1. scales
  1. (1268.0, 1268.0)

get_img_data_from_file()函数可以返回图片数据的数据,它们是图像数据img, 真实框坐标gt_boxes, 真实框包含的物体类别gt_labels, 图像尺寸scales。

数据预处理

在计算机视觉中,通常会对图像做一些随机的变化,产生相似但又不完全相同的样本。主要作用是扩大训练数据集,抑制过拟合,提升模型的泛化能力,常用的方法见下面的程序。

随机改变亮暗、对比度和颜色等

  1. import numpy as np
  2. import cv2
  3. from PIL import Image, ImageEnhance
  4. import random
  5. # 随机改变亮暗、对比度和颜色等
  6. def random_distort(img):
  7. # 随机改变亮度
  8. def random_brightness(img, lower=0.5, upper=1.5):
  9. e = np.random.uniform(lower, upper)
  10. return ImageEnhance.Brightness(img).enhance(e)
  11. # 随机改变对比度
  12. def random_contrast(img, lower=0.5, upper=1.5):
  13. e = np.random.uniform(lower, upper)
  14. return ImageEnhance.Contrast(img).enhance(e)
  15. # 随机改变颜色
  16. def random_color(img, lower=0.5, upper=1.5):
  17. e = np.random.uniform(lower, upper)
  18. return ImageEnhance.Color(img).enhance(e)
  19. ops = [random_brightness, random_contrast, random_color]
  20. np.random.shuffle(ops)
  21. img = Image.fromarray(img)
  22. img = ops[0](img)
  23. img = ops[1](img)
  24. img = ops[2](img)
  25. img = np.asarray(img)
  26. return img

随机填充

  1. # 随机填充
  2. def random_expand(img,
  3. gtboxes,
  4. max_ratio=4.,
  5. fill=None,
  6. keep_ratio=True,
  7. thresh=0.5):
  8. if random.random() > thresh:
  9. return img, gtboxes
  10. if max_ratio < 1.0:
  11. return img, gtboxes
  12. h, w, c = img.shape
  13. ratio_x = random.uniform(1, max_ratio)
  14. if keep_ratio:
  15. ratio_y = ratio_x
  16. else:
  17. ratio_y = random.uniform(1, max_ratio)
  18. oh = int(h * ratio_y)
  19. ow = int(w * ratio_x)
  20. off_x = random.randint(0, ow - w)
  21. off_y = random.randint(0, oh - h)
  22. out_img = np.zeros((oh, ow, c))
  23. if fill and len(fill) == c:
  24. for i in range(c):
  25. out_img[:, :, i] = fill[i] * 255.0
  26. out_img[off_y:off_y + h, off_x:off_x + w, :] = img
  27. gtboxes[:, 0] = ((gtboxes[:, 0] * w) + off_x) / float(ow)
  28. gtboxes[:, 1] = ((gtboxes[:, 1] * h) + off_y) / float(oh)
  29. gtboxes[:, 2] = gtboxes[:, 2] / ratio_x
  30. gtboxes[:, 3] = gtboxes[:, 3] / ratio_y
  31. return out_img.astype('uint8'), gtboxes

随机裁剪

随机裁剪之前需要先定义两个函数,multi_box_iou_xywh和box_crop这两个函数将被保存在box_utils.py文件中。

  1. import numpy as np
  2. def multi_box_iou_xywh(box1, box2):
  3. """
  4. In this case, box1 or box2 can contain multi boxes.
  5. Only two cases can be processed in this method:
  6. 1, box1 and box2 have the same shape, box1.shape == box2.shape
  7. 2, either box1 or box2 contains only one box, len(box1) == 1 or len(box2) == 1
  8. If the shape of box1 and box2 does not match, and both of them contain multi boxes, it will be wrong.
  9. """
  10. assert box1.shape[-1] == 4, "Box1 shape[-1] should be 4."
  11. assert box2.shape[-1] == 4, "Box2 shape[-1] should be 4."
  12. b1_x1, b1_x2 = box1[:, 0] - box1[:, 2] / 2, box1[:, 0] + box1[:, 2] / 2
  13. b1_y1, b1_y2 = box1[:, 1] - box1[:, 3] / 2, box1[:, 1] + box1[:, 3] / 2
  14. b2_x1, b2_x2 = box2[:, 0] - box2[:, 2] / 2, box2[:, 0] + box2[:, 2] / 2
  15. b2_y1, b2_y2 = box2[:, 1] - box2[:, 3] / 2, box2[:, 1] + box2[:, 3] / 2
  16. inter_x1 = np.maximum(b1_x1, b2_x1)
  17. inter_x2 = np.minimum(b1_x2, b2_x2)
  18. inter_y1 = np.maximum(b1_y1, b2_y1)
  19. inter_y2 = np.minimum(b1_y2, b2_y2)
  20. inter_w = inter_x2 - inter_x1
  21. inter_h = inter_y2 - inter_y1
  22. inter_w = np.clip(inter_w, a_min=0., a_max=None)
  23. inter_h = np.clip(inter_h, a_min=0., a_max=None)
  24. inter_area = inter_w * inter_h
  25. b1_area = (b1_x2 - b1_x1) * (b1_y2 - b1_y1)
  26. b2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1)
  27. return inter_area / (b1_area + b2_area - inter_area)
  28. def box_crop(boxes, labels, crop, img_shape):
  29. x, y, w, h = map(float, crop)
  30. im_w, im_h = map(float, img_shape)
  31. boxes = boxes.copy()
  32. boxes[:, 0], boxes[:, 2] = (boxes[:, 0] - boxes[:, 2] / 2) * im_w, (
  33. boxes[:, 0] + boxes[:, 2] / 2) * im_w
  34. boxes[:, 1], boxes[:, 3] = (boxes[:, 1] - boxes[:, 3] / 2) * im_h, (
  35. boxes[:, 1] + boxes[:, 3] / 2) * im_h
  36. crop_box = np.array([x, y, x + w, y + h])
  37. centers = (boxes[:, :2] + boxes[:, 2:]) / 2.0
  38. mask = np.logical_and(crop_box[:2] <= centers, centers <= crop_box[2:]).all(
  39. axis=1)
  40. boxes[:, :2] = np.maximum(boxes[:, :2], crop_box[:2])
  41. boxes[:, 2:] = np.minimum(boxes[:, 2:], crop_box[2:])
  42. boxes[:, :2] -= crop_box[:2]
  43. boxes[:, 2:] -= crop_box[:2]
  44. mask = np.logical_and(mask, (boxes[:, :2] < boxes[:, 2:]).all(axis=1))
  45. boxes = boxes * np.expand_dims(mask.astype('float32'), axis=1)
  46. labels = labels * mask.astype('float32')
  47. boxes[:, 0], boxes[:, 2] = (boxes[:, 0] + boxes[:, 2]) / 2 / w, (
  48. boxes[:, 2] - boxes[:, 0]) / w
  49. boxes[:, 1], boxes[:, 3] = (boxes[:, 1] + boxes[:, 3]) / 2 / h, (
  50. boxes[:, 3] - boxes[:, 1]) / h
  51. return boxes, labels, mask.sum()
  1. # 随机裁剪
  2. def random_crop(img,
  3. boxes,
  4. labels,
  5. scales=[0.3, 1.0],
  6. max_ratio=2.0,
  7. constraints=None,
  8. max_trial=50):
  9. if len(boxes) == 0:
  10. return img, boxes
  11. if not constraints:
  12. constraints = [(0.1, 1.0), (0.3, 1.0), (0.5, 1.0), (0.7, 1.0),
  13. (0.9, 1.0), (0.0, 1.0)]
  14. img = Image.fromarray(img)
  15. w, h = img.size
  16. crops = [(0, 0, w, h)]
  17. for min_iou, max_iou in constraints:
  18. for _ in range(max_trial):
  19. scale = random.uniform(scales[0], scales[1])
  20. aspect_ratio = random.uniform(max(1 / max_ratio, scale * scale), \
  21. min(max_ratio, 1 / scale / scale))
  22. crop_h = int(h * scale / np.sqrt(aspect_ratio))
  23. crop_w = int(w * scale * np.sqrt(aspect_ratio))
  24. crop_x = random.randrange(w - crop_w)
  25. crop_y = random.randrange(h - crop_h)
  26. crop_box = np.array([[(crop_x + crop_w / 2.0) / w,
  27. (crop_y + crop_h / 2.0) / h,
  28. crop_w / float(w), crop_h / float(h)]])
  29. iou = multi_box_iou_xywh(crop_box, boxes)
  30. if min_iou <= iou.min() and max_iou >= iou.max():
  31. crops.append((crop_x, crop_y, crop_w, crop_h))
  32. break
  33. while crops:
  34. crop = crops.pop(np.random.randint(0, len(crops)))
  35. crop_boxes, crop_labels, box_num = box_crop(boxes, labels, crop, (w, h))
  36. if box_num < 1:
  37. continue
  38. img = img.crop((crop[0], crop[1], crop[0] + crop[2],
  39. crop[1] + crop[3])).resize(img.size, Image.LANCZOS)
  40. img = np.asarray(img)
  41. return img, crop_boxes, crop_labels
  42. img = np.asarray(img)
  43. return img, boxes, labels

随机缩放

  1. # 随机缩放
  2. def random_interp(img, size, interp=None):
  3. interp_method = [
  4. cv2.INTER_NEAREST,
  5. cv2.INTER_LINEAR,
  6. cv2.INTER_AREA,
  7. cv2.INTER_CUBIC,
  8. cv2.INTER_LANCZOS4,
  9. ]
  10. if not interp or interp not in interp_method:
  11. interp = interp_method[random.randint(0, len(interp_method) - 1)]
  12. h, w, _ = img.shape
  13. im_scale_x = size / float(w)
  14. im_scale_y = size / float(h)
  15. img = cv2.resize(
  16. img, None, None, fx=im_scale_x, fy=im_scale_y, interpolation=interp)
  17. return img

随机翻转

  1. # 随机翻转
  2. def random_flip(img, gtboxes, thresh=0.5):
  3. if random.random() > thresh:
  4. img = img[:, ::-1, :]
  5. gtboxes[:, 0] = 1.0 - gtboxes[:, 0]
  6. return img, gtboxes

随机打乱真实框排列顺序

  1. # 随机打乱真实框排列顺序
  2. def shuffle_gtbox(gtbox, gtlabel):
  3. gt = np.concatenate(
  4. [gtbox, gtlabel[:, np.newaxis]], axis=1)
  5. idx = np.arange(gt.shape[0])
  6. np.random.shuffle(idx)
  7. gt = gt[idx, :]
  8. return gt[:, :4], gt[:, 4]

图像增广方法

  1. # 图像增广方法汇总
  2. def image_augment(img, gtboxes, gtlabels, size, means=None):
  3. # 随机改变亮暗、对比度和颜色等
  4. img = random_distort(img)
  5. # 随机填充
  6. img, gtboxes = random_expand(img, gtboxes, fill=means)
  7. # 随机裁剪
  8. img, gtboxes, gtlabels, = random_crop(img, gtboxes, gtlabels)
  9. # 随机缩放
  10. img = random_interp(img, size)
  11. # 随机翻转
  12. img, gtboxes = random_flip(img, gtboxes)
  13. # 随机打乱真实框排列顺序
  14. gtboxes, gtlabels = shuffle_gtbox(gtboxes, gtlabels)
  15. return img.astype('float32'), gtboxes.astype('float32'), gtlabels.astype('int32')
  1. img, gt_boxes, gt_labels, scales = get_img_data_from_file(record)
  2. size = 512
  3. img, gt_boxes, gt_labels = image_augment(img, gt_boxes, gt_labels, size)
  1. img.shape
  1. (512, 512, 3)
  1. gt_boxes.shape
  1. (50, 4)
  1. gt_labels.shape
  1. (50,)

这里得到的img数据数值需要调整,需要除以255.,并且减去均值和方差,再将维度从[H, W, C]调整为[C, H, W]

  1. img, gt_boxes, gt_labels, scales = get_img_data_from_file(record)
  2. size = 512
  3. img, gt_boxes, gt_labels = image_augment(img, gt_boxes, gt_labels, size)
  4. mean = [0.485, 0.456, 0.406]
  5. std = [0.229, 0.224, 0.225]
  6. mean = np.array(mean).reshape((1, 1, -1))
  7. std = np.array(std).reshape((1, 1, -1))
  8. img = (img / 255.0 - mean) / std
  9. img = img.astype('float32').transpose((2, 0, 1))
  10. img
  1. array([[[ 0.99880135, 0.99880135, 0.99880135, ..., -2.117904 ,
  2. -2.117904 , -2.117904 ],
  3. [ 0.99880135, 0.99880135, 0.99880135, ..., -2.117904 ,
  4. -2.117904 , -2.117904 ],
  5. [ 0.99880135, 0.99880135, 0.99880135, ..., -2.117904 ,
  6. -2.117904 , -2.117904 ],
  7. ...,
  8. [-2.117904 , -2.117904 , -2.117904 , ..., -2.117904 ,
  9. -2.117904 , -2.117904 ],
  10. [-2.117904 , -2.117904 , -2.117904 , ..., -2.117904 ,
  11. -2.117904 , -2.117904 ],
  12. [-2.117904 , -2.117904 , -2.117904 , ..., -2.117904 ,
  13. -2.117904 , -2.117904 ]],
  14.  
  15. [[ 1.1505603 , 1.1505603 , 1.1505603 , ..., -2.0357144 ,
  16. -2.0357144 , -2.0357144 ],
  17. [ 1.1505603 , 1.1505603 , 1.1505603 , ..., -2.0357144 ,
  18. -2.0357144 , -2.0357144 ],
  19. [ 1.1505603 , 1.1505603 , 1.1505603 , ..., -2.0357144 ,
  20. -2.0357144 , -2.0357144 ],
  21. ...,
  22. [-2.0357144 , -2.0357144 , -2.0357144 , ..., -2.0357144 ,
  23. -2.0357144 , -2.0357144 ],
  24. [-2.0357144 , -2.0357144 , -2.0357144 , ..., -2.0357144 ,
  25. -2.0357144 , -2.0357144 ],
  26. [-2.0357144 , -2.0357144 , -2.0357144 , ..., -2.0357144 ,
  27. -2.0357144 , -2.0357144 ]],
  28.  
  29. [[ 1.3676689 , 1.3676689 , 1.3676689 , ..., -1.8044444 ,
  30. -1.8044444 , -1.8044444 ],
  31. [ 1.3676689 , 1.3676689 , 1.3502398 , ..., -1.8044444 ,
  32. -1.8044444 , -1.8044444 ],
  33. [ 1.3676689 , 1.3676689 , 1.3676689 , ..., -1.8044444 ,
  34. -1.8044444 , -1.8044444 ],
  35. ...,
  36. [-1.8044444 , -1.8044444 , -1.8044444 , ..., -1.8044444 ,
  37. -1.8044444 , -1.8044444 ],
  38. [-1.8044444 , -1.8044444 , -1.8044444 , ..., -1.8044444 ,
  39. -1.8044444 , -1.8044444 ],
  40. [-1.8044444 , -1.8044444 , -1.8044444 , ..., -1.8044444 ,
  41. -1.8044444 , -1.8044444 ]]], dtype=float32)

将上面的过程整理成一个函数get_img_data

  1. def get_img_data(record, size=640):
  2. img, gt_boxes, gt_labels, scales = get_img_data_from_file(record)
  3. img, gt_boxes, gt_labels = image_augment(img, gt_boxes, gt_labels, size)
  4. mean = [0.485, 0.456, 0.406]
  5. std = [0.229, 0.224, 0.225]
  6. mean = np.array(mean).reshape((1, 1, -1))
  7. std = np.array(std).reshape((1, 1, -1))
  8. img = (img / 255.0 - mean) / std
  9. img = img.astype('float32').transpose((2, 0, 1))
  10. return img, gt_boxes, gt_labels, scales
  1. TRAINDIR = '/home/aistudio/work/insects/train'
  2. TESTDIR = '/home/aistudio/work/insects/test'
  3. VALIDDIR = '/home/aistudio/work/insects/val'
  4. cname2cid = get_insect_names()
  5. records = get_annotations(cname2cid, TRAINDIR)
  6. record = records[0]
  7. img, gt_boxes, gt_labels, scales = get_img_data(record, size=480)
  1. img.shape
  1. (3, 480, 480)
  1. gt_boxes.shape
  1. (50, 4)
  1. gt_labels
  1. array([0, 0, 0, 0, 0, 0, 2, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0,
  2. 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  3. 4, 0, 1, 0, 0, 0], dtype=int32)
  1. scales
  1. (1268.0, 1268.0)

批量数据读取与加速

上面的程序展示了如何读取一张图片的数据并加速,下面的代码实现了批量数据读取。

  1. # 获取一个批次内样本随机缩放的尺寸
  2. def get_img_size(mode):
  3. if (mode == 'train') or (mode == 'valid'):
  4. inds = np.array([0,1,2,3,4,5,6,7,8,9])
  5. ii = np.random.choice(inds)
  6. img_size = 320 + ii * 32
  7. else:
  8. img_size = 608
  9. return img_size
  10. # 将 list形式的batch数据 转化成多个array构成的tuple
  11. def make_array(batch_data):
  12. img_array = np.array([item[0] for item in batch_data], dtype = 'float32')
  13. gt_box_array = np.array([item[1] for item in batch_data], dtype = 'float32')
  14. gt_labels_array = np.array([item[2] for item in batch_data], dtype = 'int32')
  15. img_scale = np.array([item[3] for item in batch_data], dtype='int32')
  16. return img_array, gt_box_array, gt_labels_array, img_scale
  17. # 批量读取数据,同一批次内图像的尺寸大小必须是一样的,
  18. # 不同批次之间的大小是随机的,
  19. # 由上面定义的get_img_size函数产生
  20. def data_loader(datadir, batch_size= 10, mode='train'):
  21. cname2cid = get_insect_names()
  22. records = get_annotations(cname2cid, datadir)
  23. def reader():
  24. if mode == 'train':
  25. np.random.shuffle(records)
  26. batch_data = []
  27. img_size = get_img_size(mode)
  28. for record in records:
  29. #print(record)
  30. img, gt_bbox, gt_labels, im_shape = get_img_data(record,
  31. size=img_size)
  32. batch_data.append((img, gt_bbox, gt_labels, im_shape))
  33. if len(batch_data) == batch_size:
  34. yield make_array(batch_data)
  35. batch_data = []
  36. img_size = get_img_size(mode)
  37. if len(batch_data) > 0:
  38. yield make_array(batch_data)
  39. return reader
  1. d = data_loader('/home/aistudio/work/insects/train', batch_size=2, mode='train')
  1. img, gt_boxes, gt_labels, im_shape = next(d())
  1. img.shape, gt_boxes.shape, gt_labels.shape, im_shape.shape
  1. ((2, 3, 544, 544), (2, 50, 4), (2, 50), (2, 2))

由于在数据预处理耗时较长,可能会成为网络训练速度的瓶颈,所以需要对预处理部分进行优化。通过使用Paddle提供的API paddle.reader.xmap_readers可以开启多线程读取数据,具体实现代码如下。

  1. import functools
  2. import paddle
  3. # 使用paddle.reader.xmap_readers实现多线程读取数据
  4. def multithread_loader(datadir, batch_size= 10, mode='train'):
  5. cname2cid = get_insect_names()
  6. records = get_annotations(cname2cid, datadir)
  7. def reader():
  8. if mode == 'train':
  9. np.random.shuffle(records)
  10. img_size = get_img_size(mode)
  11. batch_data = []
  12. for record in records:
  13. batch_data.append((record, img_size))
  14. if len(batch_data) == batch_size:
  15. yield batch_data
  16. batch_data = []
  17. img_size = get_img_size(mode)
  18. if len(batch_data) > 0:
  19. yield batch_data
  20. def get_data(samples):
  21. batch_data = []
  22. for sample in samples:
  23. record = sample[0]
  24. img_size = sample[1]
  25. img, gt_bbox, gt_labels, im_shape = get_img_data(record, size=img_size)
  26. batch_data.append((img, gt_bbox, gt_labels, im_shape))
  27. return make_array(batch_data)
  28. mapper = functools.partial(get_data, )
  29. return paddle.reader.xmap_readers(mapper, reader, 8, 10)
  1. d = multithread_loader('/home/aistudio/work/insects/train', batch_size=2, mode='train')
  1. img, gt_boxes, gt_labels, im_shape = next(d())
  1. img.shape, gt_boxes.shape, gt_labels.shape, im_shape.shape
  1. ((2, 3, 352, 352), (2, 50, 4), (2, 50), (2, 2))

至此,我们完成了如何查看数据集中的数据、提取数据标注信息、从文件读取图像和标注数据、数据增多、批量读取和加速等过程,通过multithread_loader可以返回img, gt_boxes, gt_labels, im_shape等数据,接下来就可以将它们输入神经网络应用在具体算法上面了。

在开始具体的算法讲解之前,先补充一下测试数据的读取代码,测试数据没有标注信息,也不需要做图像增广,代码如下所示。

  1. # 测试数据读取
  2. # 将 list形式的batch数据 转化成多个array构成的tuple
  3. def make_test_array(batch_data):
  4. img_name_array = np.array([item[0] for item in batch_data])
  5. img_data_array = np.array([item[1] for item in batch_data], dtype = 'float32')
  6. img_scale_array = np.array([item[2] for item in batch_data], dtype='int32')
  7. return img_name_array, img_data_array, img_scale_array
  8. # 测试数据读取
  9. def test_data_loader(datadir, batch_size= 10, test_image_size=608, mode='test'):
  10. """
  11. 加载测试用的图片,测试数据没有groundtruth标签
  12. """
  13. image_names = os.listdir(datadir)
  14. def reader():
  15. batch_data = []
  16. img_size = test_image_size
  17. for image_name in image_names:
  18. file_path = os.path.join(datadir, image_name)
  19. img = cv2.imread(file_path)
  20. img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  21. H = img.shape[0]
  22. W = img.shape[1]
  23. img = cv2.resize(img, (img_size, img_size))
  24. mean = [0.485, 0.456, 0.406]
  25. std = [0.229, 0.224, 0.225]
  26. mean = np.array(mean).reshape((1, 1, -1))
  27. std = np.array(std).reshape((1, 1, -1))
  28. out_img = (img / 255.0 - mean) / std
  29. out_img = out_img.astype('float32').transpose((2, 0, 1))
  30. img = out_img #np.transpose(out_img, (2,0,1))
  31. im_shape = [H, W]
  32. batch_data.append((image_name.split('.')[0], img, im_shape))
  33. if len(batch_data) == batch_size:
  34. yield make_test_array(batch_data)
  35. batch_data = []
  36. if len(batch_data) > 0:
  37. yield make_test_array(batch_data)
  38. return reader

单阶段目标检测模型YOLO-V3

上面介绍的R-CNN系列算法需要先产生候选区域,再对RoI做分类和位置坐标的预测,这类算法被称为两阶段目标检测算法。近几年,很多研究人员相继提出一系列单阶段的检测算法,只需要一个网络即可同时产生RoI并预测出物体的类别和位置坐标。

与R-CNN系列算法不同,YOLO-V3使用单个网络结构,在产生候选区域的同时即可预测出物体类别和位置,不需要分成两阶段来完成检测任务。另外,YOLO-V3算法产生的预测框数目比Faster-RCNN少很多。Faster-RCNN中每个真实框可能对应多个标签为正的候选区域,而YOLO-V3里面每个真实框只对应一个正的候选区域。这些特性使得YOLO-V3算法具有更快的速度,能到达实时响应的水平。

Joseph Redmon等人在2015年提出YOLO(You Only Look Once,YOLO)算法,通常也被称为YOLO V1;2016年,他们对算法进行改进,又提出YOLO V2版本;2018年发展出YOLO V3版本。

主要涵盖如下内容:

  • YOLO-V3模型设计思想
  • 产生候选区域
    • 生成锚框
    • 生成预测框
    • 标注候选区域
  • 卷积神经网络提取特征
  • 建立损失函数
    • 获取样本标签
    • 建立各项损失函数
  • 多层级检测
  • 预测输出
    • 计算预测框得分和位置
    • 非极大值抑制

YOLO-V3 模型设计思想

YOLO V3算法的基本思想可以分成两部分:

  • 按一定规则在图片上产生一系列的候选区域,然后根据这些候选区域与图片上物体真实框之间的位置关系对候选区域进行标注。跟真实框足够接近的那些候选区域会被标注为正样本,同时将真实框的位置作为正样本的位置目标。偏离真实框较大的那些候选区域则会被标注为负样本,负样本不需要预测位置或者类别。
  • 使用卷积神经网络提取图片特征并对候选区域的位置和类别进行预测。这样每个预测框就可以看成是一个样本,根据真实框相对它的位置和类别进行了标注而获得标签值,通过网络模型预测其位置和类别,将网络预测值和标签值进行比较,就可以建立起损失函数。

YOLO-V3算法训练过程的流程图如 图8 所示:

目标检测 - 图34

图8:YOLO-V3算法训练流程图

  • 图8 左边是输入图片,上半部分所示的过程是使用卷积神经网络对图片提取特征,随着网络不断向前传播,特征图的尺寸越来越小,每个像素点会代表更加抽象的特征模式,直到输出特征图,其尺寸减小为原图的 目标检测 - 图35
  • 图8 下半部分描述了生成候选区域的过程,首先将原图划分成多个小方块,每个小方块的大小是 目标检测 - 图36 ,然后以每个小方块为中心分别生成一系列锚框,整张图片都会被锚框覆盖到,在每个锚框的基础上产生一个与之对应的预测框,根据锚框和预测框与图片上物体真实框之间的位置关系,对这些预测框进行标注。
  • 将上方支路中输出的特征图与下方支路中产生的预测框标签建立关联,创建损失函数,开启端到端的训练过程。

接下来具体介绍流程中各节点的原理和代码实现。

产生候选区域

如何产生候选区域,是检测模型的核心设计方案。目前大多数基于卷积神经网络的模型所采用的方式大体如下:

  • 按一定的规则在图片上生成一系列位置固定的锚框,将这些锚框看作是可能的候选区域,
  • 对锚框是否包含目标物体进行预测,如果包含目标物体,还需要预测所包含物体的类别,以及预测框相对于锚框位置需要调整的幅度。

生成锚框

将原始图片划分成

目标检测 - 图37 个区域,如下图所示,原始图片高度H=640, 宽度W=480,如果我们选择小块区域的尺寸为 目标检测 - 图38 ,则m和n分别为:

目标检测 - 图39

目标检测 - 图40

图9 所示,将原始图像分成了20行15列小方块区域。

目标检测 - 图41

图9:将图片划分成多个32x32的小方块

YOLO-V3算法会在每个区域的中心,生成一系列锚框。为了展示方便,我们先在图中第十行第四列的小方块位置附近画出生成的锚框,如 图10 所示。


注意:

这里为了跟程序中的编号对应,最上面的行号是第0行,最左边的列号是第0列**


目标检测 - 图42

图10:在第10行第4列的小方块区域生成3个锚框

图11 展示在每个区域附近都生成3个锚框,很多锚框堆叠在一起可能不太容易看清楚,但过程跟上面类似,只是需要以每个区域的中心点为中心,分别生成3个锚框。

目标检测 - 图43

图11:在每个小方块区域生成3个锚框

生成预测框

在前面已经指出,锚框的位置都是固定好的,不可能刚好跟物体边界框重合,需要在锚框的基础上进行位置的微调以生成预测框。预测框相对于锚框会有不同的中心位置和大小,采用什么方式能产生出在锚框上面微调得到的预测框呢,我们先来考虑如何生成其中心位置坐标。

比如上面图中在第10行第4列的小方块区域中心生成的一个锚框,如绿色虚线框所示。以小方格的宽度为单位长度,

此小方块区域左上角的位置坐标是:

目标检测 - 图44

目标检测 - 图45

此锚框的区域中心坐标是

目标检测 - 图46

目标检测 - 图47

可以通过下面的方式生成预测框的中心坐标:

目标检测 - 图48

目标检测 - 图49

其中

目标检测 - 图50目标检测 - 图51 为实数, 目标检测 - 图52 是我们之前学过的Sigmoid函数,其定义如下:

目标检测 - 图53

由于Sigmoid的函数值总是在

目标检测 - 图54 之间,所以由上式计算出来的预测框中心点总是落在第十行第四列的小区域内部。

目标检测 - 图55 时, 目标检测 - 图56目标检测 - 图57 ,预测框中心与锚框中心重合,都是小区域的中心。

锚框的大小是预先设定好的,在模型中可以当作是超参数,下图中画出的锚框尺寸是

目标检测 - 图58

目标检测 - 图59

通过下面的公式生成预测框的大小:

目标检测 - 图60

目标检测 - 图61

如果

目标检测 - 图62 ,则预测框跟锚框重合。

如果给

目标检测 - 图63 随机赋值如下:

目标检测 - 图64

则可以得到预测框的坐标是(154.98, 357.44, 276.29, 310.42),如 图12 中蓝色框所示。

  • 备注:这里坐标采用xywh的格式

目标检测 - 图65

图12:生成预测框

这里我们会问:当

目标检测 - 图66 取值为多少的时候,预测框能够跟真实框重合?为了回答问题,只需要将上面预测框坐标中的 目标检测 - 图67 设置为真实框的位置,即可求解出t的数值。

令:

目标检测 - 图68

目标检测 - 图69

目标检测 - 图70

目标检测 - 图71

可以求解出

目标检测 - 图72

如果

目标检测 - 图73 是网络预测的输出值,将 目标检测 - 图74 作为目标值,以他们之间的差距作为损失函数,则可以建立起一个回归问题,通过学习网络参数,使得 目标检测 - 图75 足够接近 目标检测 - 图76 ,从而能够求解出预测框的位置坐标跟大小。

预测框可以看作是在锚框基础上的一个微调,每个锚框会有一个跟它对应的预测框,我们需要确定上面计算式中的

目标检测 - 图77 ,从而计算出与锚框对应的预测框的位置和形状。

对候选区域进行标注

每个在区域可以产生3种不同形状的锚框,每个锚框都是一个可能的候选区域,对这些候选区域我们希望知道这么几件事情:

  • 锚框是否包含了物体,这可以看成是一个二分类问题,包含了物体和没有包含物体,我们使用标签objectness来表示。当锚框包含了物体时,objectness=1,表示预测框属于正类;当锚框不包含物体时,设置objectness=0,表示锚框属于负类。

  • 如果锚框包含了物体,那么它对应的预测框的中心位置和大小应该是多少,或者说上面计算式中的

目标检测 - 图78 应该是多少。

  • 如果锚框包含了物体,那么具体的具体类别是什么,这里使用变量label来表示其所属类别的标签。

现在对于任意一个锚框,我们需要对它进行标注,也就是需要确定其对应的objectness,

目标检测 - 图79 和label,下面将分别讲述如何确定这三个标签的值。

标注锚框是否包含物体的objectness标签

图13 所示,这里一共有3个目标,以最左边的人像为例,其真实框是

目标检测 - 图80

目标检测 - 图81

图13:选出与真实框中心位于同一区域的锚框

真实框的中心点坐标是:

目标检测 - 图82

目标检测 - 图83

目标检测 - 图84

目标检测 - 图85

它落在了第10行第4列的小方块内,如图(b)所示。此小方块区域可以生成3个不同形状的锚框,其在图上的编号和大小分别是

目标检测 - 图86

用这3个不同形状的锚框跟真实框计算IoU,选出IoU最大的锚框。这里为了简化计算,只考虑锚框的形状,不考虑其跟真实框中心之间的偏移,具体计算结果如 图14 所示。

目标检测 - 图87

图14:选出与真实框与锚框的IoU

其中跟真实框IoU最大的是锚框

目标检测 - 图88 ,形状是 目标检测 - 图89 ,将它所对应的预测框的objectness标签设置为1,其所包括的物体类别就是真实框里面的物体所属类别。

依次可以找出其他几个真实框对应的IoU最大的锚框,然后将它们的预测框的objectness标签也都设置为1。这里一共有

目标检测 - 图90 个锚框,只有3个预测框会被标注为正。

由于每个真实框只对应一个objectness标签为正的预测框,如果有些预测框跟真实框之间的IoU很大,但并不是最大的那个,那么直接将其objectness标签设置为0当作负样本,可能并不妥当。为了避免这种情况,YOLO-V3算法设置了一个IoU阈值iou_thresh,当预测框的objectness不为1,但是其与某个真实框的IoU大于iou_thresh时,就将其objectness标签设置为-1,不参与损失函数的计算。

所有其他的预测框,其objectness标签均设置为0,表示负类。

对于objectness=1的预测框,需要进一步确定其位置和包含物体的具体分类标签,但是对于objectness=0或者-1的预测框,则不用管他们的位置和类别。

标注预测框的位置坐标标签

当锚框objectness=1时,需要确定预测框位置相对于它微调的幅度,也就是锚框的位置标签。

在前面我们已经问过这样一个问题:当

目标检测 - 图91 取值为多少的时候,预测框能够跟真实框重合?其做法是将预测框坐标中的 目标检测 - 图92 设置为真实框的坐标,即可求解出t的数值。

令:

目标检测 - 图93

目标检测 - 图94

目标检测 - 图95

目标检测 - 图96

对于

目标检测 - 图97目标检测 - 图98 ,由于Sigmoid的反函数不好计算,我们直接将 目标检测 - 图99目标检测 - 图100 作为回归的目标

目标检测 - 图101

目标检测 - 图102

目标检测 - 图103

目标检测 - 图104

如果

目标检测 - 图105 是网络预测的输出值,将 目标检测 - 图106 作为 目标检测 - 图107 的目标值,以它们之间的差距作为损失函数,则可以建立起一个回归问题,通过学习网络参数,使得 目标检测 - 图108 足够接近 目标检测 - 图109 ,从而能够求解出预测框的位置。

标注锚框包含物体类别的标签

对于objectness=1的锚框,需要确定其具体类别。正如上面所说,objectness标注为1的锚框,会有一个真实框跟它对应,该锚框所属物体类别,即是其所对应的真实框包含的物体类别。这里使用one-hot向量来表示类别标签label。比如一共有10个分类,而真实框里面包含的物体类别是第2类,则label为

目标检测 - 图110

对上述步骤进行总结,标注的流程如 图15 所示。

目标检测 - 图111

图15:标注流程示意图

通过这种方式,我们在每个小方块区域都生成了一系列的锚框作为候选区域,并且根据图片上真实物体的位置,标注出了每个候选区域对应的objectness标签、位置需要调整的幅度以及包含的物体所属的类别。位置需要调整的幅度由4个变量描述

目标检测 - 图112 ,objectness标签需要用一个变量描述 目标检测 - 图113 ,描述所属类别的变量长度等于类别数C。

对于每个锚框,模型需要预测输出

目标检测 - 图114 ,其中 目标检测 - 图115 是锚框是否包含物体的概率, 目标检测 - 图116 则是锚框包含的物体属于每个类别的概率。接下来让我们一起学习如何通过卷积神经网络输出这样的预测值。

标注锚框的具体程序

上面描述了如何对预锚框进行标注,但读者可能仍然对里面的细节不太了解,下面将通过具体的程序完成这一步骤。

  1. # 标注预测框的objectness
  2. def get_objectness_label(img, gt_boxes, gt_labels, iou_threshold = 0.7,
  3. anchors = [116, 90, 156, 198, 373, 326],
  4. num_classes=7, downsample=32):
  5. """
  6. img 是输入的图像数据,形状是[N, C, H, W]
  7. gt_boxes,真实框,维度是[N, 50, 4],其中50是真实框数目的上限,当图片中真实框不足50个时,不足部分的坐标全为0
  8. 真实框坐标格式是xywh,这里使用相对值
  9. gt_labels,真实框所属类别,维度是[N, 50]
  10. iou_threshold,当预测框与真实框的iou大于iou_threshold时不将其看作是负样本
  11. anchors,锚框可选的尺寸
  12. anchor_masks,通过与anchors一起确定本层级的特征图应该选用多大尺寸的锚框
  13. num_classes,类别数目
  14. downsample,特征图相对于输入网络的图片尺寸变化的比例
  15. """
  16. img_shape = img.shape
  17. batchsize = img_shape[0]
  18. num_anchors = len(anchors) // 2
  19. input_h = img_shape[2]
  20. input_w = img_shape[3]
  21. # 将输入图片划分成num_rows x num_cols个小方块区域,每个小方块的边长是 downsample
  22. # 计算一共有多少行小方块
  23. num_rows = input_h // downsample
  24. # 计算一共有多少列小方块
  25. num_cols = input_w // downsample
  26. label_objectness = np.zeros([batchsize, num_anchors, num_rows, num_cols])
  27. label_classification = np.zeros([batchsize, num_anchors, num_classes, num_rows, num_cols])
  28. label_location = np.zeros([batchsize, num_anchors, 4, num_rows, num_cols])
  29. scale_location = np.ones([batchsize, num_anchors, num_rows, num_cols])
  30. # 对batchsize进行循环,依次处理每张图片
  31. for n in range(batchsize):
  32. # 对图片上的真实框进行循环,依次找出跟真实框形状最匹配的锚框
  33. for n_gt in range(len(gt_boxes[n])):
  34. gt = gt_boxes[n][n_gt]
  35. gt_cls = gt_labels[n][n_gt]
  36. gt_center_x = gt[0]
  37. gt_center_y = gt[1]
  38. gt_width = gt[2]
  39. gt_height = gt[3]
  40. if (gt_height < 1e-3) or (gt_height < 1e-3):
  41. continue
  42. i = int(gt_center_y * num_rows)
  43. j = int(gt_center_x * num_cols)
  44. ious = []
  45. for ka in range(num_anchors):
  46. bbox1 = [0., 0., float(gt_width), float(gt_height)]
  47. anchor_w = anchors[ka * 2]
  48. anchor_h = anchors[ka * 2 + 1]
  49. bbox2 = [0., 0., anchor_w/float(input_w), anchor_h/float(input_h)]
  50. # 计算iou
  51. iou = box_iou_xywh(bbox1, bbox2)
  52. ious.append(iou)
  53. ious = np.array(ious)
  54. inds = np.argsort(ious)
  55. k = inds[-1]
  56. label_objectness[n, k, i, j] = 1
  57. c = gt_cls
  58. label_classification[n, k, c, i, j] = 1.
  59. # for those prediction bbox with objectness =1, set label of location
  60. dx_label = gt_center_x * num_cols - j
  61. dy_label = gt_center_y * num_rows - i
  62. dw_label = np.log(gt_width * input_w / anchors[k*2])
  63. dh_label = np.log(gt_height * input_h / anchors[k*2 + 1])
  64. label_location[n, k, 0, i, j] = dx_label
  65. label_location[n, k, 1, i, j] = dy_label
  66. label_location[n, k, 2, i, j] = dw_label
  67. label_location[n, k, 3, i, j] = dh_label
  68. # scale_location用来调节不同尺寸的锚框对损失函数的贡献,作为加权系数和位置损失函数相乘
  69. scale_location[n, k, i, j] = 2.0 - gt_width * gt_height
  70. # 目前根据每张图片上所有出现过的gt box,都标注出了objectness为正的预测框,剩下的预测框则默认objectness为0
  71. # 对于objectness为1的预测框,标出了他们所包含的物体类别,以及位置回归的目标
  72. return label_objectness.astype('float32'), label_location.astype('float32'), label_classification.astype('float32'), \
  73. scale_location.astype('float32')
  1. # 读取数据
  2. reader = multithread_loader('/home/aistudio/work/insects/train', batch_size=2, mode='train')
  3. img, gt_boxes, gt_labels, im_shape = next(reader())
  4. # 计算出锚框对应的标签
  5. label_objectness, label_location, label_classification, scale_location = get_objectness_label(img,
  6. gt_boxes, gt_labels,
  7. iou_threshold = 0.7,
  8. anchors = [116, 90, 156, 198, 373, 326],
  9. num_classes=7, downsample=32)
  1. img.shape, gt_boxes.shape, gt_labels.shape, im_shape.shape
  1. ((2, 3, 480, 480), (2, 50, 4), (2, 50), (2, 2))
  1. label_objectness.shape, label_location.shape, label_classification.shape, scale_location.shape
  1. ((2, 3, 15, 15), (2, 3, 4, 15, 15), (2, 3, 7, 15, 15), (2, 3, 15, 15))

上面的程序实现了对锚框进行标注,对于每个真实框,选出了与它形状最匹配的锚框,将其objectness标注为1,并且将

目标检测 - 图117 作为正样本位置的标签,真实框包含的物体类别作为锚框的类别。而其余的锚框,objectness将被标注为0,无需标注出位置和类别的标签。

  • 注意:这里还遗留一个小问题,前面我们说了对于与真实框IoU较大的那些锚框,需要将其objectness标注为-1,不参与损失函数的计算。我们先将这个问题放一放,等到后面建立损失函数的时候再补上。

卷积神经网络提取特征

在上一节图像分类的课程中,我们已经学习过了通过卷积神经网络提取图像特征。通过连续使用多层卷积和池化等操作,能得到语义含义更加丰富的特征图。在检测问题中,也使用卷积神经网络逐层提取图像特征,通过最终的输出特征图来表征物体位置和类别等信息。

YOLO V3算法使用的骨干网络是Darknet53。Darknet53网络的具体结构如 图16 所示,在ImageNet图像分类任务上取得了很好的成绩。在检测任务中,将图中C0后面的平均池化、全连接层和Softmax去掉,保留从输入到C0部分的网络结构,作为检测模型的基础网络结构,也称为骨干网络。YOLO V3模型会在骨干网络的基础上,再添加检测相关的网络模块。

目标检测 - 图118

图16:Darknet53网络结构

下面的程序是Darknet53骨干网络的实现代码,这里将上图中C0、C1、C2所表示的输出数据取出,并查看它们的形状分别是,

目标检测 - 图119目标检测 - 图120目标检测 - 图121

  • 名词解释:特征图的步幅(stride)

在提取特征的过程中通常会使用步幅大于1的卷积或者池化,导致后面的特征图尺寸越来越小,特征图的步幅等于输入图片尺寸除以特征图尺寸。例如C0的尺寸是

目标检测 - 图122 ,原图尺寸是 目标检测 - 图123 ,则C0的步幅是 目标检测 - 图124 。同理,C1的步幅是16,C2的步幅是8。

  1. import paddle.fluid as fluid
  2. from paddle.fluid.param_attr import ParamAttr
  3. from paddle.fluid.regularizer import L2Decay
  4. from paddle.fluid.dygraph.nn import Conv2D, BatchNorm
  5. from paddle.fluid.dygraph.base import to_variable
  6. # YOLO-V3骨干网络结构Darknet53的实现代码
  7. class ConvBNLayer(fluid.dygraph.Layer):
  8. """
  9. 卷积 + 批归一化,BN层之后激活函数默认用leaky_relu
  10. """
  11. def __init__(self,
  12. ch_in,
  13. ch_out,
  14. filter_size=3,
  15. stride=1,
  16. groups=1,
  17. padding=0,
  18. act="leaky",
  19. is_test=True):
  20. super(ConvBNLayer, self).__init__()
  21. self.conv = Conv2D(
  22. num_channels=ch_in,
  23. num_filters=ch_out,
  24. filter_size=filter_size,
  25. stride=stride,
  26. padding=padding,
  27. groups=groups,
  28. param_attr=ParamAttr(
  29. initializer=fluid.initializer.Normal(0., 0.02)),
  30. bias_attr=False,
  31. act=None)
  32. self.batch_norm = BatchNorm(
  33. num_channels=ch_out,
  34. is_test=is_test,
  35. param_attr=ParamAttr(
  36. initializer=fluid.initializer.Normal(0., 0.02),
  37. regularizer=L2Decay(0.)),
  38. bias_attr=ParamAttr(
  39. initializer=fluid.initializer.Constant(0.0),
  40. regularizer=L2Decay(0.)))
  41. self.act = act
  42. def forward(self, inputs):
  43. out = self.conv(inputs)
  44. out = self.batch_norm(out)
  45. if self.act == 'leaky':
  46. out = fluid.layers.leaky_relu(x=out, alpha=0.1)
  47. return out
  48. class DownSample(fluid.dygraph.Layer):
  49. """
  50. 下采样,图片尺寸减半,具体实现方式是使用stirde=2的卷积
  51. """
  52. def __init__(self,
  53. ch_in,
  54. ch_out,
  55. filter_size=3,
  56. stride=2,
  57. padding=1,
  58. is_test=True):
  59. super(DownSample, self).__init__()
  60. self.conv_bn_layer = ConvBNLayer(
  61. ch_in=ch_in,
  62. ch_out=ch_out,
  63. filter_size=filter_size,
  64. stride=stride,
  65. padding=padding,
  66. is_test=is_test)
  67. self.ch_out = ch_out
  68. def forward(self, inputs):
  69. out = self.conv_bn_layer(inputs)
  70. return out
  71. class BasicBlock(fluid.dygraph.Layer):
  72. """
  73. 基本残差块的定义,输入x经过两层卷积,然后接第二层卷积的输出和输入x相加
  74. """
  75. def __init__(self, ch_in, ch_out, is_test=True):
  76. super(BasicBlock, self).__init__()
  77. self.conv1 = ConvBNLayer(
  78. ch_in=ch_in,
  79. ch_out=ch_out,
  80. filter_size=1,
  81. stride=1,
  82. padding=0,
  83. is_test=is_test
  84. )
  85. self.conv2 = ConvBNLayer(
  86. ch_in=ch_out,
  87. ch_out=ch_out*2,
  88. filter_size=3,
  89. stride=1,
  90. padding=1,
  91. is_test=is_test
  92. )
  93. def forward(self, inputs):
  94. conv1 = self.conv1(inputs)
  95. conv2 = self.conv2(conv1)
  96. out = fluid.layers.elementwise_add(x=inputs, y=conv2, act=None)
  97. return out
  98. class LayerWarp(fluid.dygraph.Layer):
  99. """
  100. 添加多层残差块,组成Darknet53网络的一个层级
  101. """
  102. def __init__(self, ch_in, ch_out, count, is_test=True):
  103. super(LayerWarp,self).__init__()
  104. self.basicblock0 = BasicBlock(ch_in,
  105. ch_out,
  106. is_test=is_test)
  107. self.res_out_list = []
  108. for i in range(1, count):
  109. res_out = self.add_sublayer("basic_block_%d" % (i), #使用add_sublayer添加子层
  110. BasicBlock(ch_out*2,
  111. ch_out,
  112. is_test=is_test))
  113. self.res_out_list.append(res_out)
  114. def forward(self,inputs):
  115. y = self.basicblock0(inputs)
  116. for basic_block_i in self.res_out_list:
  117. y = basic_block_i(y)
  118. return y
  119. DarkNet_cfg = {53: ([1, 2, 8, 8, 4])}
  120. class DarkNet53_conv_body(fluid.dygraph.Layer):
  121. def __init__(self,
  122. name_scope,
  123. is_test=True):
  124. super(DarkNet53_conv_body, self).__init__(name_scope)
  125. self.stages = DarkNet_cfg[53]
  126. self.stages = self.stages[0:5]
  127. # 第一层卷积
  128. self.conv0 = ConvBNLayer(
  129. ch_in=3,
  130. ch_out=32,
  131. filter_size=3,
  132. stride=1,
  133. padding=1,
  134. is_test=is_test)
  135. # 下采样,使用stride=2的卷积来实现
  136. self.downsample0 = DownSample(
  137. ch_in=32,
  138. ch_out=32 * 2,
  139. is_test=is_test)
  140. # 添加各个层级的实现
  141. self.darknet53_conv_block_list = []
  142. self.downsample_list = []
  143. for i, stage in enumerate(self.stages):
  144. conv_block = self.add_sublayer(
  145. "stage_%d" % (i),
  146. LayerWarp(32*(2**(i+1)),
  147. 32*(2**i),
  148. stage,
  149. is_test=is_test))
  150. self.darknet53_conv_block_list.append(conv_block)
  151. # 两个层级之间使用DownSample将尺寸减半
  152. for i in range(len(self.stages) - 1):
  153. downsample = self.add_sublayer(
  154. "stage_%d_downsample" % i,
  155. DownSample(ch_in=32*(2**(i+1)),
  156. ch_out=32*(2**(i+2)),
  157. is_test=is_test))
  158. self.downsample_list.append(downsample)
  159. def forward(self,inputs):
  160. out = self.conv0(inputs)
  161. #print("conv1:",out.numpy())
  162. out = self.downsample0(out)
  163. #print("dy:",out.numpy())
  164. blocks = []
  165. for i, conv_block_i in enumerate(self.darknet53_conv_block_list): #依次将各个层级作用在输入上面
  166. out = conv_block_i(out)
  167. blocks.append(out)
  168. if i < len(self.stages) - 1:
  169. out = self.downsample_list[i](out)
  170. return blocks[-1:-4:-1] # 将C0, C1, C2作为返回值
  1. # 查看Darknet53网络输出特征图
  2. import numpy as np
  3. with fluid.dygraph.guard():
  4. backbone = DarkNet53_conv_body('yolov3_backbone', is_test=False)
  5. x = np.random.randn(1, 3, 640, 640).astype('float32')
  6. x = to_variable(x)
  7. C0, C1, C2 = backbone(x)
  8. print(C0.shape, C1.shape, C2.shape)
  1. [1, 1024, 20, 20] [1, 512, 40, 40] [1, 256, 80, 80]

上面这段示例代码,指定输入数据的形状是

目标检测 - 图125 ,则3个层级的输出特征图的形状分别是 目标检测 - 图126目标检测 - 图127目标检测 - 图128

根据输出特征图计算预测框位置和类别

YOLO-V3中对每个预测框计算逻辑如下:

  • 预测框是否包含物体。也可理解为objectness=1的概率是多少,可以用网络输出一个实数x,可以用Sigmoid(x)表示objectness为正的概率

目标检测 - 图129

  • 预测物体位置和形状。物体位置和形状

目标检测 - 图130 可以用网络输出4个实数来表示 目标检测 - 图131

  • 预测物体类别。预测图像中物体的具体类别是什么,或者说其属于每个类别的概率分别是多少。总的类别数为C,需要预测物体属于每个类别的概率

目标检测 - 图132 ,可以用网络输出C个实数 目标检测 - 图133 ,对每个实数分别求Sigmoid函数,让 目标检测 - 图134 ,则可以表示出物体属于每个类别的概率。

对于一个预测框,网络需要输出

目标检测 - 图135 个实数来表征它是否包含物体、位置和形状尺寸以及属于每个类别的概率。

由于我们在每个小方块区域都生成了K个预测框,则所有预测框一共需要网络输出的预测值数目是:

目标检测 - 图136

还有更重要的一点是网络输出必须要能区分出小方块区域的位置来,不能直接将特征图连接一个输出大小为

目标检测 - 图137 的全连接层。

建立输出特征图与预测框之间的关联

现在观察特征图,经过多次卷积核池化之后,其步幅stride=32,

目标检测 - 图138 大小的输入图片变成了 目标检测 - 图139 的特征图;而小方块区域的数目正好是 目标检测 - 图140 ,也就是说可以让特征图上每个像素点分别跟原图上一个小方块区域对应。这也是为什么我们最开始将小方块区域的尺寸设置为32的原因,这样可以巧妙的将小方块区域跟特征图上的像素点对应起来,解决了空间位置的对应关系。

目标检测 - 图141

图17:特征图C0与小方块区域形状对比

下面需要将像素点

目标检测 - 图142 与第i行第j列的小方块区域所需要的预测值关联起来,每个小方块区域产生K个预测框,每个预测框需要 目标检测 - 图143 个实数预测值,则每个像素点相对应的要有 目标检测 - 图144 个实数。为了解决这一问题,对特征图进行多次卷积,并将最终的输出通道数设置为 目标检测 - 图145 ,即可将生成的特征图与每个预测框所需要的预测值巧妙的对应起来。

骨干网络的输出特征图是C0,下面的程序是对C0进行多次卷积以得到跟预测框相关的特征图P0。

  1. # 从骨干网络输出特征图C0得到跟预测相关的特征图P0
  2. class YoloDetectionBlock(fluid.dygraph.Layer):
  3. # define YOLO-V3 detection head
  4. # 使用多层卷积和BN提取特征
  5. def __init__(self,name_scope,ch_in,ch_out,is_test=True):
  6. super(YoloDetectionBlock, self).__init__(name_scope)
  7. assert ch_out % 2 == 0, \
  8. "channel {} cannot be divided by 2".format(ch_out)
  9. self.conv0 = ConvBNLayer(
  10. ch_in=ch_in,
  11. ch_out=ch_out,
  12. filter_size=1,
  13. stride=1,
  14. padding=0,
  15. is_test=is_test
  16. )
  17. self.conv1 = ConvBNLayer(
  18. ch_in=ch_out,
  19. ch_out=ch_out*2,
  20. filter_size=3,
  21. stride=1,
  22. padding=1,
  23. is_test=is_test
  24. )
  25. self.conv2 = ConvBNLayer(
  26. ch_in=ch_out*2,
  27. ch_out=ch_out,
  28. filter_size=1,
  29. stride=1,
  30. padding=0,
  31. is_test=is_test
  32. )
  33. self.conv3 = ConvBNLayer(
  34. ch_in=ch_out,
  35. ch_out=ch_out*2,
  36. filter_size=3,
  37. stride=1,
  38. padding=1,
  39. is_test=is_test
  40. )
  41. self.route = ConvBNLayer(
  42. ch_in=ch_out*2,
  43. ch_out=ch_out,
  44. filter_size=1,
  45. stride=1,
  46. padding=0,
  47. is_test=is_test
  48. )
  49. self.tip = ConvBNLayer(
  50. ch_in=ch_out,
  51. ch_out=ch_out*2,
  52. filter_size=3,
  53. stride=1,
  54. padding=1,
  55. is_test=is_test
  56. )
  57. def forward(self, inputs):
  58. out = self.conv0(inputs)
  59. out = self.conv1(out)
  60. out = self.conv2(out)
  61. out = self.conv3(out)
  62. route = self.route(out)
  63. tip = self.tip(route)
  64. return route, tip
  1. NUM_ANCHORS = 3
  2. NUM_CLASSES = 7
  3. num_filters=NUM_ANCHORS * (NUM_CLASSES + 5)
  4. with fluid.dygraph.guard():
  5. backbone = DarkNet53_conv_body('yolov3_backbone', is_test=False)
  6. detection = YoloDetectionBlock('detection', ch_in=1024, ch_out=512, is_test=False)
  7. conv2d_pred = Conv2D(num_channels=1024, num_filters=num_filters, filter_size=1)
  8. x = np.random.randn(1, 3, 640, 640).astype('float32')
  9. x = to_variable(x)
  10. C0, C1, C2 = backbone(x)
  11. route, tip = detection(C0)
  12. P0 = conv2d_pred(tip)
  13. print(P0.shape)
  1. [1, 36, 20, 20]

如上面的代码所示,可以由特征图C0生成特征图P0,P0的形状是

目标检测 - 图146 。每个小方块区域生成的锚框或者预测框的数量是3,物体类别数目是7,每个区域需要的预测值个数是 目标检测 - 图147 ,正好等于P0的输出通道数。

目标检测 - 图148

图18:特征图P0与候选区域的关联

目标检测 - 图149 与输入的第t张图片上小方块区域 目标检测 - 图150 第1个预测框所需要的12个预测值对应, 目标检测 - 图151 与输入的第t张图片上小方块区域 目标检测 - 图152 第2个预测框所需要的12个预测值对应, 目标检测 - 图153 与输入的第t张图片上小方块区域 目标检测 - 图154 第3个预测框所需要的12个预测值对应。

目标检测 - 图155 与输入的第t张图片上小方块区域 目标检测 - 图156 第1个预测框的位置对应, 目标检测 - 图157 与输入的第t张图片上小方块区域 目标检测 - 图158 第1个预测框的objectness对应, 目标检测 - 图159 与输入的第t张图片上小方块区域 目标检测 - 图160 第1个预测框的类别对应。

图18 所示,通过这种方式可以巧妙的将网络输出特征图,与每个小方块区域生成的预测框对应起来了。

计算预测框是否包含物体的概率

根据前面的分析,

目标检测 - 图161 与输入的第t张图片上小方块区域 目标检测 - 图162 第1个预测框的objectness对应, 目标检测 - 图163 与第2个预测框的objectness对应,…,则可以使用下面的程序将objectness相关的预测取出,并使用fluid.layers.sigmoid计算输出概率。

  1. NUM_ANCHORS = 3
  2. NUM_CLASSES = 7
  3. num_filters=NUM_ANCHORS * (NUM_CLASSES + 5)
  4. with fluid.dygraph.guard():
  5. backbone = DarkNet53_conv_body('yolov3_backbone', is_test=False)
  6. detection = YoloDetectionBlock('detection', ch_in=1024, ch_out=512, is_test=False)
  7. conv2d_pred = Conv2D(num_channels=1024, num_filters=num_filters, filter_size=1)
  8. x = np.random.randn(1, 3, 640, 640).astype('float32')
  9. x = to_variable(x)
  10. C0, C1, C2 = backbone(x)
  11. route, tip = detection(C0)
  12. P0 = conv2d_pred(tip)
  13. reshaped_p0 = fluid.layers.reshape(P0, [-1, NUM_ANCHORS, NUM_CLASSES + 5, P0.shape[2], P0.shape[3]])
  14. pred_objectness = reshaped_p0[:, :, 4, :, :]
  15. pred_objectness_probability = fluid.layers.sigmoid(pred_objectness)
  16. print(pred_objectness.shape, pred_objectness_probability.shape)
  1. [1, 3, 20, 20] [1, 3, 20, 20]

上面的输出程序显示,预测框是否包含物体的概率pred_objectness_probability,其数据形状是$[1, 3, 20, 20] $,与我们上面提到的预测框个数一致,数据大小在0~1之间,表示预测框为正样本的概率。

计算预测框位置坐标

目标检测 - 图164 与输入的第t张图片上小方块区域 目标检测 - 图165 第1个预测框的位置对应, 目标检测 - 图166 与第2个预测框的位置对应,…,使用下面的程序可以从P0中取出跟预测框位置相关的预测值。

  1. NUM_ANCHORS = 3
  2. NUM_CLASSES = 7
  3. num_filters=NUM_ANCHORS * (NUM_CLASSES + 5)
  4. with fluid.dygraph.guard():
  5. backbone = DarkNet53_conv_body('yolov3_backbone', is_test=False)
  6. detection = YoloDetectionBlock('detection', ch_in=1024, ch_out=512, is_test=False)
  7. conv2d_pred = Conv2D(num_channels=1024, num_filters=num_filters, filter_size=1)
  8. x = np.random.randn(1, 3, 640, 640).astype('float32')
  9. x = to_variable(x)
  10. C0, C1, C2 = backbone(x)
  11. route, tip = detection(C0)
  12. P0 = conv2d_pred(tip)
  13. reshaped_p0 = fluid.layers.reshape(P0, [-1, NUM_ANCHORS, NUM_CLASSES + 5, P0.shape[2], P0.shape[3]])
  14. pred_objectness = reshaped_p0[:, :, 4, :, :]
  15. pred_objectness_probability = fluid.layers.sigmoid(pred_objectness)
  16. pred_location = reshaped_p0[:, :, 0:4, :, :]
  17. print(pred_location.shape)
  1. [1, 3, 4, 20, 20]

网络输出值是

目标检测 - 图167 ,还需要将其转化为 目标检测 - 图168 这种形式的坐标表示。Paddle里面有专门的API fluid.layers.yolo_box直接计算出结果,但为了给读者更清楚的展示算法的实现过程,我们使用Numpy来实现这一过程。

  1. # 定义Sigmoid函数
  2. def sigmoid(x):
  3. return 1./(1.0 + np.exp(-x))
  4. # 将网络特征图输出的[tx, ty, th, tw]转化成预测框的坐标[x1, y1, x2, y2]
  5. def get_yolo_box_xxyy(pred, anchors, num_classes, downsample):
  6. """
  7. pred是网络输出特征图转化成的numpy.ndarray
  8. anchors 是一个list。表示锚框的大小,
  9. 例如 anchors = [116, 90, 156, 198, 373, 326],表示有三个锚框,
  10. 第一个锚框大小[w, h]是[116, 90],第二个锚框大小是[156, 198],第三个锚框大小是[373, 326]
  11. """
  12. batchsize = pred.shape[0]
  13. num_rows = pred.shape[-2]
  14. num_cols = pred.shape[-1]
  15. input_h = num_rows * downsample
  16. input_w = num_cols * downsample
  17. num_anchors = len(anchors) // 2
  18. # pred的形状是[N, C, H, W],其中C = NUM_ANCHORS * (5 + NUM_CLASSES)
  19. # 对pred进行reshape
  20. pred = pred.reshape([-1, num_anchors, 5+num_classes, num_rows, num_cols])
  21. pred_location = pred[:, :, 0:4, :, :]
  22. pred_location = np.transpose(pred_location, (0,3,4,1,2))
  23. anchors_this = []
  24. for ind in range(num_anchors):
  25. anchors_this.append([anchors[ind*2], anchors[ind*2+1]])
  26. anchors_this = np.array(anchors_this).astype('float32')
  27. # 最终输出数据保存在pred_box中,其形状是[N, H, W, NUM_ANCHORS, 4],
  28. # 其中最后一个维度4代表位置的4个坐标
  29. pred_box = np.zeros(pred_location.shape)
  30. for n in range(batchsize):
  31. for i in range(num_rows):
  32. for j in range(num_cols):
  33. for k in range(num_anchors):
  34. pred_box[n, i, j, k, 0] = j
  35. pred_box[n, i, j, k, 1] = i
  36. pred_box[n, i, j, k, 2] = anchors_this[k][0]
  37. pred_box[n, i, j, k, 3] = anchors_this[k][1]
  38. # 这里使用相对坐标,pred_box的输出元素数值在0.~1.0之间
  39. pred_box[:, :, :, :, 0] = (sigmoid(pred_location[:, :, :, :, 0]) + pred_box[:, :, :, :, 0]) / num_cols
  40. pred_box[:, :, :, :, 1] = (sigmoid(pred_location[:, :, :, :, 1]) + pred_box[:, :, :, :, 1]) / num_rows
  41. pred_box[:, :, :, :, 2] = np.exp(pred_location[:, :, :, :, 2]) * pred_box[:, :, :, :, 2] / input_w
  42. pred_box[:, :, :, :, 3] = np.exp(pred_location[:, :, :, :, 3]) * pred_box[:, :, :, :, 3] / input_h
  43. # 将坐标从xywh转化成xyxy
  44. pred_box[:, :, :, :, 0] = pred_box[:, :, :, :, 0] - pred_box[:, :, :, :, 2] / 2.
  45. pred_box[:, :, :, :, 1] = pred_box[:, :, :, :, 1] - pred_box[:, :, :, :, 3] / 2.
  46. pred_box[:, :, :, :, 2] = pred_box[:, :, :, :, 0] + pred_box[:, :, :, :, 2]
  47. pred_box[:, :, :, :, 3] = pred_box[:, :, :, :, 1] + pred_box[:, :, :, :, 3]
  48. pred_box = np.clip(pred_box, 0., 1.0)
  49. return pred_box

通过调用上面定义的get_yolo_box_xxyy函数,可以从P0计算出预测框坐标来,具体程序如下:

  1. NUM_ANCHORS = 3
  2. NUM_CLASSES = 7
  3. num_filters=NUM_ANCHORS * (NUM_CLASSES + 5)
  4. with fluid.dygraph.guard():
  5. backbone = DarkNet53_conv_body('yolov3_backbone', is_test=False)
  6. detection = YoloDetectionBlock('detection', ch_in=1024, ch_out=512, is_test=False)
  7. conv2d_pred = Conv2D(num_channels=1024, num_filters=num_filters, filter_size=1)
  8. x = np.random.randn(1, 3, 640, 640).astype('float32')
  9. x = to_variable(x)
  10. C0, C1, C2 = backbone(x)
  11. route, tip = detection(C0)
  12. P0 = conv2d_pred(tip)
  13. reshaped_p0 = fluid.layers.reshape(P0, [-1, NUM_ANCHORS, NUM_CLASSES + 5, P0.shape[2], P0.shape[3]])
  14. pred_objectness = reshaped_p0[:, :, 4, :, :]
  15. pred_objectness_probability = fluid.layers.sigmoid(pred_objectness)
  16. pred_location = reshaped_p0[:, :, 0:4, :, :]
  17. # anchors包含了预先设定好的锚框尺寸
  18. anchors = [116, 90, 156, 198, 373, 326]
  19. # downsample是特征图P0的步幅
  20. pred_boxes = get_yolo_box_xxyy(P0.numpy(), anchors, num_classes=7, downsample=32) # 由输出特征图P0计算预测框位置坐标
  21. print(pred_boxes.shape)
  1. (1, 20, 20, 3, 4)

上面程序计算出来的pred_boxes的形状是

目标检测 - 图169 ,坐标格式是 目标检测 - 图170 ,数值在0~1之间,表示相对坐标。

计算物体属于每个类别概率

目标检测 - 图171 与输入的第t张图片上小方块区域 目标检测 - 图172 第1个预测框包含物体的类别对应, 目标检测 - 图173 与第2个预测框的类别对应,…,使用下面的程序可以从P0中取出那些跟预测框类别相关的预测值。

  1. NUM_ANCHORS = 3
  2. NUM_CLASSES = 7
  3. num_filters=NUM_ANCHORS * (NUM_CLASSES + 5)
  4. with fluid.dygraph.guard():
  5. backbone = DarkNet53_conv_body('yolov3_backbone', is_test=False)
  6. detection = YoloDetectionBlock('detection', ch_in=1024, ch_out=512, is_test=False)
  7. conv2d_pred = Conv2D(num_channels=1024, num_filters=num_filters, filter_size=1)
  8. x = np.random.randn(1, 3, 640, 640).astype('float32')
  9. x = to_variable(x)
  10. C0, C1, C2 = backbone(x)
  11. route, tip = detection(C0)
  12. P0 = conv2d_pred(tip)
  13. reshaped_p0 = fluid.layers.reshape(P0, [-1, NUM_ANCHORS, NUM_CLASSES + 5, P0.shape[2], P0.shape[3]])
  14. # 取出与objectness相关的预测值
  15. pred_objectness = reshaped_p0[:, :, 4, :, :]
  16. pred_objectness_probability = fluid.layers.sigmoid(pred_objectness)
  17. # 取出与位置相关的预测值
  18. pred_location = reshaped_p0[:, :, 0:4, :, :]
  19. # 取出与类别相关的预测值
  20. pred_classification = reshaped_p0[:, :, 5:5+NUM_CLASSES, :, :]
  21. pred_classification_probability = fluid.layers.sigmoid(pred_classification)
  22. print(pred_classification.shape)
  1. [1, 3, 7, 20, 20]

上面的程序通过P0计算出了预测框包含的物体所属类别的概率,pred_classification_probability的形状是

目标检测 - 图174 ,数值在0~1之间。

损失函数

上面一小节从概念上将输出特征图上的像素点与预测框关联起来了,那么要对神经网络进行求解,还必须从数学上将网络输出和预测框关联起来,也就是要建立起损失函数跟网络输出之间的关系。下面讨论如何建立起YOLO-V3的损失函数。

对于每个预测框,YOLO-V3模型会建立三种类型的损失函数:

  • 表征是否包含目标物体的损失函数,通过pred_objectness和label_objectness计算
  1. loss_obj = fluid.layers.sigmoid_cross_entropy_with_logits(pred_objectness, label_objectness)
  • 表征物体位置的损失函数,通过pred_location和label_location计算
  1. pred_location_x = pred_location[:, :, 0, :, :]
  2. pred_location_y = pred_location[:, :, 1, :, :]
  3. pred_location_w = pred_location[:, :, 2, :, :]
  4. pred_location_h = pred_location[:, :, 3, :, :]
  5. loss_location_x = fluid.layers.sigmoid_cross_entropy_with_logits(pred_location_x, label_location_x)
  6. loss_location_y = fluid.layers.sigmoid_cross_entropy_with_logits(pred_location_y, label_location_y)
  7. loss_location_w = fluid.layers.abs(pred_location_w - label_location_w)
  8. loss_location_h = fluid.layers.abs(pred_location_h - label_location_h)
  9. loss_location = loss_location_x + loss_location_y + loss_location_w + loss_location_h
  • 表征物体类别的损失函数,通过pred_classification和label_classification计算
  1. loss_obj = fluid.layers.sigmoid_cross_entropy_with_logits(pred_classification, label_classification)

在前面几个小节中我们已经知道怎么计算这些预测值和标签了,但是遗留了一个小问题,就是没有标注出哪些锚框的objectness为-1。为了完成这一步,我们需要计算出所有预测框跟真实框之间的IoU,然后把那些IoU大于阈值的真实框挑选出来。实现代码如下:

  1. # 挑选出跟真实框IoU大于阈值的预测框
  2. def get_iou_above_thresh_inds(pred_box, gt_boxes, iou_threshold):
  3. batchsize = pred_box.shape[0]
  4. num_rows = pred_box.shape[1]
  5. num_cols = pred_box.shape[2]
  6. num_anchors = pred_box.shape[3]
  7. ret_inds = np.zeros([batchsize, num_rows, num_cols, num_anchors])
  8. for i in range(batchsize):
  9. pred_box_i = pred_box[i]
  10. gt_boxes_i = gt_boxes[i]
  11. for k in range(len(gt_boxes_i)): #gt in gt_boxes_i:
  12. gt = gt_boxes_i[k]
  13. gtx_min = gt[0] - gt[2] / 2.
  14. gty_min = gt[1] - gt[3] / 2.
  15. gtx_max = gt[0] + gt[2] / 2.
  16. gty_max = gt[1] + gt[3] / 2.
  17. if (gtx_max - gtx_min < 1e-3) or (gty_max - gty_min < 1e-3):
  18. continue
  19. x1 = np.maximum(pred_box_i[:, :, :, 0], gtx_min)
  20. y1 = np.maximum(pred_box_i[:, :, :, 1], gty_min)
  21. x2 = np.minimum(pred_box_i[:, :, :, 2], gtx_max)
  22. y2 = np.minimum(pred_box_i[:, :, :, 3], gty_max)
  23. intersection = np.maximum(x2 - x1, 0.) * np.maximum(y2 - y1, 0.)
  24. s1 = (gty_max - gty_min) * (gtx_max - gtx_min)
  25. s2 = (pred_box_i[:, :, :, 2] - pred_box_i[:, :, :, 0]) * (pred_box_i[:, :, :, 3] - pred_box_i[:, :, :, 1])
  26. union = s2 + s1 - intersection
  27. iou = intersection / union
  28. above_inds = np.where(iou > iou_threshold)
  29. ret_inds[i][above_inds] = 1
  30. ret_inds = np.transpose(ret_inds, (0,3,1,2))
  31. return ret_inds.astype('bool')

上面的函数可以得到哪些锚框的objectness需要被标注为-1,通过下面的程序,对label_objectness进行处理,将IoU大于阈值,但又不是正样本的那些锚框标注为-1。

  1. def label_objectness_ignore(label_objectness, iou_above_thresh_indices):
  2. # 注意:这里不能简单的使用 label_objectness[iou_above_thresh_indices] = -1,
  3. # 这样可能会造成label_objectness为1的那些点被设置为-1了
  4. # 只有将那些被标注为0,且与真实框IoU超过阈值的预测框才被标注为-1
  5. negative_indices = (label_objectness < 0.5)
  6. ignore_indices = negative_indices * iou_above_thresh_indices
  7. label_objectness[ignore_indices] = -1
  8. return label_objectness

下面通过调用这两个函数,实现如何将部分预测框的label_objectness设置为-1。

  1. # 读取数据
  2. reader = multithread_loader('/home/aistudio/work/insects/train', batch_size=2, mode='train')
  3. img, gt_boxes, gt_labels, im_shape = next(reader())
  4. # 计算出锚框对应的标签
  5. label_objectness, label_location, label_classification, scale_location = get_objectness_label(img,
  6. gt_boxes, gt_labels,
  7. iou_threshold = 0.7,
  8. anchors = [116, 90, 156, 198, 373, 326],
  9. num_classes=7, downsample=32)
  10. NUM_ANCHORS = 3
  11. NUM_CLASSES = 7
  12. num_filters=NUM_ANCHORS * (NUM_CLASSES + 5)
  13. with fluid.dygraph.guard():
  14. backbone = DarkNet53_conv_body('yolov3_backbone', is_test=False)
  15. detection = YoloDetectionBlock('detection', ch_in=1024, ch_out=512, is_test=False)
  16. conv2d_pred = Conv2D(num_channels=1024, num_filters=num_filters, filter_size=1)
  17. x = to_variable(img)
  18. C0, C1, C2 = backbone(x)
  19. route, tip = detection(C0)
  20. P0 = conv2d_pred(tip)
  21. # anchors包含了预先设定好的锚框尺寸
  22. anchors = [116, 90, 156, 198, 373, 326]
  23. # downsample是特征图P0的步幅
  24. pred_boxes = get_yolo_box_xxyy(P0.numpy(), anchors, num_classes=7, downsample=32)
  25. iou_above_thresh_indices = get_iou_above_thresh_inds(pred_boxes, gt_boxes, iou_threshold=0.7)
  26. label_objectness = label_objectness_ignore(label_objectness, iou_above_thresh_indices)
  27. print(label_objectness.shape)
  1. (2, 3, 12, 12)

使用这种方式,就可以将那些没有被标注为正样本,但又与真实框IoU比较大的样本objectness标签设置为-1了,不计算其对任何一种损失函数的贡献。

计算总的损失函数的代码如下:

  1. def get_loss(output, label_objectness, label_location, label_classification, scales, num_anchors=3, num_classes=7):
  2. # 将output从[N, C, H, W]变形为[N, NUM_ANCHORS, NUM_CLASSES + 5, H, W]
  3. reshaped_output = fluid.layers.reshape(output, [-1, num_anchors, num_classes + 5, output.shape[2], output.shape[3]])
  4. # 从output中取出跟objectness相关的预测值
  5. pred_objectness = reshaped_output[:, :, 4, :, :]
  6. loss_objectness = fluid.layers.sigmoid_cross_entropy_with_logits(pred_objectness, label_objectness, ignore_index=-1)
  7. ## 对第1,2,3维求和
  8. #loss_objectness = fluid.layers.reduce_sum(loss_objectness, dim=[1,2,3], keep_dim=False)
  9. # pos_samples 只有在正样本的地方取值为1.,其它地方取值全为0.
  10. pos_objectness = label_objectness > 0
  11. pos_samples = fluid.layers.cast(pos_objectness, 'float32')
  12. pos_samples.stop_gradient=True
  13. #从output中取出所有跟位置相关的预测值
  14. tx = reshaped_output[:, :, 0, :, :]
  15. ty = reshaped_output[:, :, 1, :, :]
  16. tw = reshaped_output[:, :, 2, :, :]
  17. th = reshaped_output[:, :, 3, :, :]
  18. # 从label_location中取出各个位置坐标的标签
  19. dx_label = label_location[:, :, 0, :, :]
  20. dy_label = label_location[:, :, 1, :, :]
  21. tw_label = label_location[:, :, 2, :, :]
  22. th_label = label_location[:, :, 3, :, :]
  23. # 构建损失函数
  24. loss_location_x = fluid.layers.sigmoid_cross_entropy_with_logits(tx, dx_label)
  25. loss_location_y = fluid.layers.sigmoid_cross_entropy_with_logits(ty, dy_label)
  26. loss_location_w = fluid.layers.abs(tw - tw_label)
  27. loss_location_h = fluid.layers.abs(th - th_label)
  28. # 计算总的位置损失函数
  29. loss_location = loss_location_x + loss_location_y + loss_location_h + loss_location_w
  30. # 乘以scales
  31. loss_location = loss_location * scales
  32. # 只计算正样本的位置损失函数
  33. loss_location = loss_location * pos_samples
  34. #从ooutput取出所有跟物体类别相关的像素点
  35. pred_classification = reshaped_output[:, :, 5:5+num_classes, :, :]
  36. # 计算分类相关的损失函数
  37. loss_classification = fluid.layers.sigmoid_cross_entropy_with_logits(pred_classification, label_classification)
  38. # 将第2维求和
  39. loss_classification = fluid.layers.reduce_sum(loss_classification, dim=2, keep_dim=False)
  40. # 只计算objectness为正的样本的分类损失函数
  41. loss_classification = loss_classification * pos_samples
  42. total_loss = loss_objectness + loss_location + loss_classification
  43. # 对所有预测框的loss进行求和
  44. total_loss = fluid.layers.reduce_sum(total_loss, dim=[1,2,3], keep_dim=False)
  45. # 对所有样本求平均
  46. total_loss = fluid.layers.reduce_mean(total_loss)
  47. return total_loss
  1. # 计算损失函数
  2. # 读取数据
  3. reader = multithread_loader('/home/aistudio/work/insects/train', batch_size=2, mode='train')
  4. img, gt_boxes, gt_labels, im_shape = next(reader())
  5. # 计算出锚框对应的标签
  6. label_objectness, label_location, label_classification, scale_location = get_objectness_label(img,
  7. gt_boxes, gt_labels,
  8. iou_threshold = 0.7,
  9. anchors = [116, 90, 156, 198, 373, 326],
  10. num_classes=7, downsample=32)
  11. NUM_ANCHORS = 3
  12. NUM_CLASSES = 7
  13. num_filters=NUM_ANCHORS * (NUM_CLASSES + 5)
  14. with fluid.dygraph.guard():
  15. backbone = DarkNet53_conv_body('yolov3_backbone', is_test=False)
  16. detection = YoloDetectionBlock('detection', ch_in=1024, ch_out=512, is_test=False)
  17. conv2d_pred = Conv2D(num_channels=1024, num_filters=num_filters, filter_size=1)
  18. x = to_variable(img)
  19. C0, C1, C2 = backbone(x)
  20. route, tip = detection(C0)
  21. P0 = conv2d_pred(tip)
  22. # anchors包含了预先设定好的锚框尺寸
  23. anchors = [116, 90, 156, 198, 373, 326]
  24. # downsample是特征图P0的步幅
  25. pred_boxes = get_yolo_box_xxyy(P0.numpy(), anchors, num_classes=7, downsample=32)
  26. iou_above_thresh_indices = get_iou_above_thresh_inds(pred_boxes, gt_boxes, iou_threshold=0.7)
  27. label_objectness = label_objectness_ignore(label_objectness, iou_above_thresh_indices)
  28. label_objectness = to_variable(label_objectness)
  29. label_location = to_variable(label_location)
  30. label_classification = to_variable(label_classification)
  31. scales = to_variable(scale_location)
  32. label_objectness.stop_gradient=True
  33. label_location.stop_gradient=True
  34. label_classification.stop_gradient=True
  35. scales.stop_gradient=True
  36. total_loss = get_loss(P0, label_objectness, label_location, label_classification, scales,
  37. num_anchors=NUM_ANCHORS, num_classes=NUM_CLASSES)
  38. total_loss_data = total_loss.numpy()
  39. print(total_loss_data)
  1. [565.0204]

上面的程序计算出了总的损失函数,看到这里,读者已经了解到了YOLO-V3算法的大部分内容,包括如何生成锚框、给锚框打上标签、通过卷积神经网络提取特征、将输出特征图跟预测框相关联、建立起损失函数。

多尺度检测

目前我们计算损失函数是在特征图P0的基础上进行的,它的步幅stride=32。特征图的尺寸比较小,像素点数目比较少,每个像素点的感受野很大,具有非常丰富的高层级语义信息,可能比较容易检测到较大的目标。为了能够检测到尺寸较小的那些目标,需要在尺寸较大的特征图上面建立预测输出。如果我们在C2或者C1这种层级的特征图上直接产生预测输出,可能面临新的问题,它们没有经过充分的特征提取,像素点包含的语义信息不够丰富,有可能难以提取到有效的特征模式。在目标检测中,解决这一问题的方式是,将高层级的特征图尺寸放大之后跟低层级的特征图进行融合,得到的新特征图既能包含丰富的语义信息,又具有较多的像素点,能够描述更加精细的结构。

具体的网络实现方式如 图19 所示:

目标检测 - 图175

图19:生成多层级的输出特征图P0、P1、P2

YOLO-V3在每个区域的中心位置产生3个锚框,在3个层级的特征图上产生锚框的大小分别为P2 [(10×13),(16×30),(33×23)],P1 [(30×61),(62×45),(59× 119)],P0[(116 × 90), (156 × 198), (373 × 326]。越往后的特征图上用到的锚框尺寸也越大,能捕捉到大尺寸目标的信息;越往前的特征图上锚框尺寸越小,能捕捉到小尺寸目标的信息。

因为有多尺度的检测,所以需要对上面的代码进行较大的修改,而且实现过程也略显繁琐,所以推荐大家直接使用Paddle提供的API fluid.layers.yolov3_loss,其具体说明如下:

  • fluid.layers.yolov3_loss(x, gt_box, gt_label, anchors, anchor_mask, class_num, ignore_thresh, downsample_ratio, gt_score=None, use_label_smooth=True, name=None))
    • x: 输入的图像数据
    • gt_box: 真实框
    • gt_label: 真实框标签
    • anchors: 使用到的anchor的尺寸,如[10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326]
    • anchor_mask: 每个层级上使用的anchor的掩码,[[6, 7, 8], [3, 4, 5], [0, 1, 2]]
    • class_num,物体类别数,AI识虫数据集为7
    • ignore_thresh,预测框与真实框IoU阈值超过ignore_thresh时,不作为负样本,YOLO-V3模型里设置为0.7
    • downsample_ratio,特征图P0的下采样比例,使用Darknet53骨干网络时为32
    • gt_score,真实框的置信度,在使用了mixup技巧时会用到
    • use_label_smooth,一种训练技巧,不使用就设置为False
    • name,该层的名字,比如’yolov3_loss’,可以不设置

对于使用了多层级特征图产生预测框的方法,其具体实现代码如下:

  1. # 定义上采样模块
  2. class Upsample(fluid.dygraph.Layer):
  3. def __init__(self, name_scope, scale=2):
  4. super(Upsample,self).__init__(name_scope)
  5. self.scale = scale
  6. def forward(self, inputs):
  7. # get dynamic upsample output shape
  8. shape_nchw = fluid.layers.shape(inputs)
  9. shape_hw = fluid.layers.slice(shape_nchw, axes=[0], starts=[2], ends=[4])
  10. shape_hw.stop_gradient = True
  11. in_shape = fluid.layers.cast(shape_hw, dtype='int32')
  12. out_shape = in_shape * self.scale
  13. out_shape.stop_gradient = True
  14. # reisze by actual_shape
  15. out = fluid.layers.resize_nearest(
  16. input=inputs, scale=self.scale, actual_shape=out_shape)
  17. return out
  18. # 定义YOLO-V3模型
  19. class YOLOv3(fluid.dygraph.Layer):
  20. def __init__(self,name_scope, num_classes=7, is_train=True):
  21. super(YOLOv3,self).__init__(name_scope)
  22. self.is_train = is_train
  23. self.num_classes = num_classes
  24. # 提取图像特征的骨干代码
  25. self.block = DarkNet53_conv_body(self.full_name(),
  26. is_test = not self.is_train)
  27. self.block_outputs = []
  28. self.yolo_blocks = []
  29. self.route_blocks_2 = []
  30. # 生成3个层级的特征图P0, P1, P2
  31. for i in range(3):
  32. # 添加从ci生成ri和ti的模块
  33. yolo_block = self.add_sublayer(
  34. "yolo_detecton_block_%d" % (i),
  35. YoloDetectionBlock(self.full_name(),
  36. ch_in=512//(2**i)*2 if i==0 else 512//(2**i)*2 + 512//(2**i),
  37. ch_out = 512//(2**i),
  38. is_test = not self.is_train))
  39. self.yolo_blocks.append(yolo_block)
  40. num_filters = 3 * (self.num_classes + 5)
  41. # 添加从ti生成pi的模块,这是一个Conv2D操作,输出通道数为3 * (num_classes + 5)
  42. block_out = self.add_sublayer(
  43. "block_out_%d" % (i),
  44. Conv2D(num_channels=512//(2**i)*2,
  45. num_filters=num_filters,
  46. filter_size=1,
  47. stride=1,
  48. padding=0,
  49. act=None,
  50. param_attr=ParamAttr(
  51. initializer=fluid.initializer.Normal(0., 0.02)),
  52. bias_attr=ParamAttr(
  53. initializer=fluid.initializer.Constant(0.0),
  54. regularizer=L2Decay(0.))))
  55. self.block_outputs.append(block_out)
  56. if i < 2:
  57. # 对ri进行卷积
  58. route = self.add_sublayer("route2_%d"%i,
  59. ConvBNLayer(ch_in=512//(2**i),
  60. ch_out=256//(2**i),
  61. filter_size=1,
  62. stride=1,
  63. padding=0,
  64. is_test=(not self.is_train)))
  65. self.route_blocks_2.append(route)
  66. # 将ri放大以便跟c_{i+1}保持同样的尺寸
  67. self.upsample = Upsample(self.full_name())
  68. def forward(self, inputs):
  69. outputs = []
  70. blocks = self.block(inputs)
  71. for i, block in enumerate(blocks):
  72. if i > 0:
  73. # 将r_{i-1}经过卷积和上采样之后得到特征图,与这一级的ci进行拼接
  74. block = fluid.layers.concat(input=[route, block], axis=1)
  75. # 从ci生成ti和ri
  76. route, tip = self.yolo_blocks[i](block)
  77. # 从ti生成pi
  78. block_out = self.block_outputs[i](tip)
  79. # 将pi放入列表
  80. outputs.append(block_out)
  81. if i < 2:
  82. # 对ri进行卷积调整通道数
  83. route = self.route_blocks_2[i](route)
  84. # 对ri进行放大,使其尺寸和c_{i+1}保持一致
  85. route = self.upsample(route)
  86. return outputs
  87. def get_loss(self, outputs, gtbox, gtlabel, gtscore=None,
  88. anchors = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326],
  89. anchor_masks = [[6, 7, 8], [3, 4, 5], [0, 1, 2]],
  90. ignore_thresh=0.7,
  91. use_label_smooth=False):
  92. """
  93. 使用fluid.layers.yolov3_loss,直接计算损失函数,过程更简洁,速度也更快
  94. """
  95. self.losses = []
  96. downsample = 32
  97. for i, out in enumerate(outputs): # 对三个层级分别求损失函数
  98. anchor_mask_i = anchor_masks[i]
  99. loss = fluid.layers.yolov3_loss(
  100. x=out, # out是P0, P1, P2中的一个
  101. gt_box=gtbox, # 真实框坐标
  102. gt_label=gtlabel, # 真实框类别
  103. gt_score=gtscore, # 真实框得分,使用mixup训练技巧时需要,不使用该技巧时直接设置为1,形状与gtlabel相同
  104. anchors=anchors, # 锚框尺寸,包含[w0, h0, w1, h1, ..., w8, h8]共9个锚框的尺寸
  105. anchor_mask=anchor_mask_i, # 筛选锚框的mask,例如anchor_mask_i=[3, 4, 5],将anchors中第3、4、5个锚框挑选出来给该层级使用
  106. class_num=self.num_classes, # 分类类别数
  107. ignore_thresh=ignore_thresh, # 当预测框与真实框IoU > ignore_thresh,标注objectness = -1
  108. downsample_ratio=downsample, # 特征图相对于原图缩小的倍数,例如P0是32, P1是16,P2是8
  109. use_label_smooth=False) # 使用label_smooth训练技巧时会用到,这里没用此技巧,直接设置为False
  110. self.losses.append(fluid.layers.reduce_mean(loss)) #reduce_mean对每张图片求和
  111. downsample = downsample // 2 # 下一级特征图的缩放倍数会减半
  112. return sum(self.losses) # 对每个层级求和

开启端到端训练

训练过程的流程如下图所示,输入图片经过特征提取得到三个层级的输出特征图P0(stride=32)、P1(stride=16)和P2(stride=8),相应的分别使用不同大小的小方块区域去生成对应的锚框和预测框,并对这些锚框进行标注。

  • P0层级特征图,对应着使用

目标检测 - 图176 大小的小方块,在每个区域中心生成大小分别为 目标检测 - 图177 , 目标检测 - 图178 , 目标检测 - 图179 的三种锚框。

  • P1层级特征图,对应着使用

目标检测 - 图180 大小的小方块,在每个区域中心生成大小分别为 目标检测 - 图181 , 目标检测 - 图182 , 目标检测 - 图183 的三种锚框。

  • P2层级特征图,对应着使用

目标检测 - 图184 大小的小方块,在每个区域中心生成大小分别为 目标检测 - 图185 , 目标检测 - 图186 , 目标检测 - 图187 的三种锚框。

将三个层级的特征图与对应锚框之间的标签关联起来,并建立损失函数,总的损失函数等于三个层级的损失函数相加。通过极小化损失函数,可以开启端到端的训练过程。

目标检测 - 图188

图20:端到端训练流程

训练过程的具体实现代码如下:

  1. ############# 这段代码在本地机器上运行请慎重,容易造成死机#######################
  2. import time
  3. import os
  4. import paddle
  5. import paddle.fluid as fluid
  6. ANCHORS = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326]
  7. ANCHOR_MASKS = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
  8. IGNORE_THRESH = .7
  9. NUM_CLASSES = 7
  10. def get_lr(base_lr = 0.0001, lr_decay = 0.1):
  11. bd = [10000, 20000]
  12. lr = [base_lr, base_lr * lr_decay, base_lr * lr_decay * lr_decay]
  13. learning_rate = fluid.layers.piecewise_decay(boundaries=bd, values=lr)
  14. return learning_rate
  15. if __name__ == '__main__':
  16. TRAINDIR = '/home/aistudio/work/insects/train'
  17. TESTDIR = '/home/aistudio/work/insects/test'
  18. VALIDDIR = '/home/aistudio/work/insects/val'
  19. with fluid.dygraph.guard():
  20. model = YOLOv3('yolov3', num_classes = NUM_CLASSES, is_train=True) #创建模型
  21. learning_rate = get_lr()
  22. opt = fluid.optimizer.Momentum(
  23. learning_rate=learning_rate,
  24. momentum=0.9,
  25. regularization=fluid.regularizer.L2Decay(0.0005),
  26. parameter_list=model.parameters()) #创建优化器
  27. train_loader = multithread_loader(TRAINDIR, batch_size= 10, mode='train') #创建训练数据读取器
  28. valid_loader = multithread_loader(VALIDDIR, batch_size= 10, mode='valid') #创建验证数据读取器
  29. MAX_EPOCH = 200
  30. for epoch in range(MAX_EPOCH):
  31. for i, data in enumerate(train_loader()):
  32. img, gt_boxes, gt_labels, img_scale = data
  33. gt_scores = np.ones(gt_labels.shape).astype('float32')
  34. gt_scores = to_variable(gt_scores)
  35. img = to_variable(img)
  36. gt_boxes = to_variable(gt_boxes)
  37. gt_labels = to_variable(gt_labels)
  38. outputs = model(img) #前向传播,输出[P0, P1, P2]
  39. loss = model.get_loss(outputs, gt_boxes, gt_labels, gtscore=gt_scores,
  40. anchors = ANCHORS,
  41. anchor_masks = ANCHOR_MASKS,
  42. ignore_thresh=IGNORE_THRESH,
  43. use_label_smooth=False) # 计算损失函数
  44. loss.backward() # 反向传播计算梯度
  45. opt.minimize(loss) # 更新参数
  46. model.clear_gradients()
  47. if i % 1 == 0:
  48. timestring = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(time.time()))
  49. print('{}[TRAIN]epoch {}, iter {}, output loss: {}'.format(timestring, epoch, i, loss.numpy()))
  50. # save params of model
  51. if (epoch % 5 == 0) or (epoch == MAX_EPOCH -1):
  52. fluid.save_dygraph(model.state_dict(), 'yolo_epoch{}'.format(epoch))
  53. # 每个epoch结束之后在验证集上进行测试
  54. model.eval()
  55. for i, data in enumerate(valid_loader()):
  56. img, gt_boxes, gt_labels, img_scale = data
  57. gt_scores = np.ones(gt_labels.shape).astype('float32')
  58. gt_scores = to_variable(gt_scores)
  59. img = to_variable(img)
  60. gt_boxes = to_variable(gt_boxes)
  61. gt_labels = to_variable(gt_labels)
  62. outputs = model(img)
  63. loss = model.get_loss(outputs, gt_boxes, gt_labels, gtscore=gt_scores,
  64. anchors = ANCHORS,
  65. anchor_masks = ANCHOR_MASKS,
  66. ignore_thresh=IGNORE_THRESH,
  67. use_label_smooth=False)
  68. if i % 1 == 0:
  69. timestring = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(time.time()))
  70. print('{}[VALID]epoch {}, iter {}, output loss: {}'.format(timestring, epoch, i, loss.numpy()))
  71. model.train()

预测

预测过程流程 图21 如下所示:

目标检测 - 图189

图21:端到端训练流程

预测过程可以分为两步:

  • 通过网络输出计算出预测框位置和所属类别的得分。
  • 使用非极大值抑制来消除重叠较大的预测框。

对于第1步,前面我们已经讲过如何通过网络输出值计算pred_objectness_probability, pred_boxes以及pred_classification_probability,这里推荐大家直接使用fluid.layers.yolo_box,其使用方法是:

  • fluid.layers.yolo_box(x, img_size, anchors, class_num, conf_thresh, downsample_ratio, name=None)

    • x,网络输出特征图,例如上面提到的P0或者P1、P2
    • img_size,输入图片尺寸
    • anchors,使用到的anchor的尺寸,如[10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326]
    • anchor_mask: 每个层级上使用的anchor的掩码,[[6, 7, 8], [3, 4, 5], [0, 1, 2]]
    • class_num,物体类别数目
    • conf_thresh, 置信度阈值,得分低于该阈值的预测框位置数值不用计算直接设置为0.0
    • downsample_ratio, 特征图的下采样比例,例如P0是32,P1是16,P2是8
    • name=None,名字,例如’yolo_box’
  • 返回值包括两项,boxes和scores,其中boxes是所有预测框的坐标值,scores是所有预测框的得分。

预测框得分的定义是所属类别的概率乘以其预测框是否包含目标物体的objectness概率,即

目标检测 - 图190

在上面定义的类YOLO-V3下面添加函数,get_pred,通过调用fluid.layers.yolo_box获得P0、P1、P2三个层级的特征图对应的预测框和得分,并将他们拼接在一块,即可得到所有的预测框及其属于各个类别的得分。

  1. # 定义YOLO-V3模型
  2. class YOLOv3(fluid.dygraph.Layer):
  3. def __init__(self,name_scope, num_classes=7, is_train=True):
  4. super(YOLOv3,self).__init__(name_scope)
  5. self.is_train = is_train
  6. self.num_classes = num_classes
  7. # 提取图像特征的骨干代码
  8. self.block = DarkNet53_conv_body(self.full_name(),
  9. is_test = not self.is_train)
  10. self.block_outputs = []
  11. self.yolo_blocks = []
  12. self.route_blocks_2 = []
  13. # 生成3个层级的特征图P0, P1, P2
  14. for i in range(3):
  15. # 添加从ci生成ri和ti的模块
  16. yolo_block = self.add_sublayer(
  17. "yolo_detecton_block_%d" % (i),
  18. YoloDetectionBlock(self.full_name(),
  19. ch_in=512//(2**i)*2 if i==0 else 512//(2**i)*2 + 512//(2**i),
  20. ch_out = 512//(2**i),
  21. is_test = not self.is_train))
  22. self.yolo_blocks.append(yolo_block)
  23. num_filters = 3 * (self.num_classes + 5)
  24. # 添加从ti生成pi的模块,这是一个Conv2D操作,输出通道数为3 * (num_classes + 5)
  25. block_out = self.add_sublayer(
  26. "block_out_%d" % (i),
  27. Conv2D(num_channels=512//(2**i)*2,
  28. num_filters=num_filters,
  29. filter_size=1,
  30. stride=1,
  31. padding=0,
  32. act=None,
  33. param_attr=ParamAttr(
  34. initializer=fluid.initializer.Normal(0., 0.02)),
  35. bias_attr=ParamAttr(
  36. initializer=fluid.initializer.Constant(0.0),
  37. regularizer=L2Decay(0.))))
  38. self.block_outputs.append(block_out)
  39. if i < 2:
  40. # 对ri进行卷积
  41. route = self.add_sublayer("route2_%d"%i,
  42. ConvBNLayer(ch_in=512//(2**i),
  43. ch_out=256//(2**i),
  44. filter_size=1,
  45. stride=1,
  46. padding=0,
  47. is_test=(not self.is_train)))
  48. self.route_blocks_2.append(route)
  49. # 将ri放大以便跟c_{i+1}保持同样的尺寸
  50. self.upsample = Upsample(self.full_name())
  51. def forward(self, inputs):
  52. outputs = []
  53. blocks = self.block(inputs)
  54. for i, block in enumerate(blocks):
  55. if i > 0:
  56. # 将r_{i-1}经过卷积和上采样之后得到特征图,与这一级的ci进行拼接
  57. block = fluid.layers.concat(input=[route, block], axis=1)
  58. # 从ci生成ti和ri
  59. route, tip = self.yolo_blocks[i](block)
  60. # 从ti生成pi
  61. block_out = self.block_outputs[i](tip)
  62. # 将pi放入列表
  63. outputs.append(block_out)
  64. if i < 2:
  65. # 对ri进行卷积调整通道数
  66. route = self.route_blocks_2[i](route)
  67. # 对ri进行放大,使其尺寸和c_{i+1}保持一致
  68. route = self.upsample(route)
  69. return outputs
  70. def get_loss(self, outputs, gtbox, gtlabel, gtscore=None,
  71. anchors = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326],
  72. anchor_masks = [[6, 7, 8], [3, 4, 5], [0, 1, 2]],
  73. ignore_thresh=0.7,
  74. use_label_smooth=False):
  75. self.losses = []
  76. downsample = 32
  77. for i, out in enumerate(outputs):
  78. anchor_mask_i = anchor_masks[i]
  79. loss = fluid.layers.yolov3_loss(
  80. x=out,
  81. gt_box=gtbox,
  82. gt_label=gtlabel,
  83. gt_score=gtscore,
  84. anchors=anchors,
  85. anchor_mask=anchor_mask_i,
  86. class_num=self.num_classes,
  87. ignore_thresh=ignore_thresh,
  88. downsample_ratio=downsample,
  89. use_label_smooth=False)
  90. self.losses.append(fluid.layers.reduce_mean(loss))
  91. downsample = downsample // 2
  92. return sum(self.losses)
  93. def get_pred(self,
  94. outputs,
  95. im_shape=None,
  96. anchors = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326],
  97. anchor_masks = [[6, 7, 8], [3, 4, 5], [0, 1, 2]],
  98. valid_thresh = 0.01):
  99. downsample = 32
  100. total_boxes = []
  101. total_scores = []
  102. for i, out in enumerate(outputs):
  103. anchor_mask = anchor_masks[i]
  104. anchors_this_level = []
  105. for m in anchor_mask:
  106. anchors_this_level.append(anchors[2 * m])
  107. anchors_this_level.append(anchors[2 * m + 1])
  108. boxes, scores = fluid.layers.yolo_box(
  109. x=out,
  110. img_size=im_shape,
  111. anchors=anchors_this_level,
  112. class_num=self.num_classes,
  113. conf_thresh=valid_thresh,
  114. downsample_ratio=downsample,
  115. name="yolo_box" + str(i))
  116. total_boxes.append(boxes)
  117. total_scores.append(
  118. fluid.layers.transpose(
  119. scores, perm=[0, 2, 1]))
  120. downsample = downsample // 2
  121. yolo_boxes = fluid.layers.concat(total_boxes, axis=1)
  122. yolo_scores = fluid.layers.concat(total_scores, axis=2)
  123. return yolo_boxes, yolo_scores

第1步的计算结果会在每个小方块区域都会产生多个预测框,输出预测框中会有很多重合度比较大,需要消除重叠较大的冗余预测框。

下面示例代码中的预测框是使用模型对图片预测之后输出的,这里一共选出了11个预测框,在图上画出预测框如下所示。在每个人像周围,都出现了多个预测框,需要消除冗余的预测框以得到最终的预测结果。

  1. # 画图展示目标物体边界框
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. import matplotlib.patches as patches
  5. from matplotlib.image import imread
  6. import math
  7. # 定义画矩形框的程序
  8. def draw_rectangle(currentAxis, bbox, edgecolor = 'k', facecolor = 'y', fill=False, linestyle='-'):
  9. # currentAxis,坐标轴,通过plt.gca()获取
  10. # bbox,边界框,包含四个数值的list, [x1, y1, x2, y2]
  11. # edgecolor,边框线条颜色
  12. # facecolor,填充颜色
  13. # fill, 是否填充
  14. # linestype,边框线型
  15. # patches.Rectangle需要传入左上角坐标、矩形区域的宽度、高度等参数
  16. rect=patches.Rectangle((bbox[0], bbox[1]), bbox[2]-bbox[0]+1, bbox[3]-bbox[1]+1, linewidth=1,
  17. edgecolor=edgecolor,facecolor=facecolor,fill=fill, linestyle=linestyle)
  18. currentAxis.add_patch(rect)
  19. plt.figure(figsize=(10, 10))
  20. filename = '/home/aistudio/work/images/section3/000000086956.jpg'
  21. im = imread(filename)
  22. plt.imshow(im)
  23. currentAxis=plt.gca()
  24. # 预测框位置
  25. boxes = np.array([[4.21716537e+01, 1.28230896e+02, 2.26547668e+02, 6.00434631e+02],
  26. [3.18562988e+02, 1.23168472e+02, 4.79000000e+02, 6.05688416e+02],
  27. [2.62704697e+01, 1.39430557e+02, 2.20587097e+02, 6.38959656e+02],
  28. [4.24965363e+01, 1.42706665e+02, 2.25955185e+02, 6.35671204e+02],
  29. [2.37462646e+02, 1.35731537e+02, 4.79000000e+02, 6.31451294e+02],
  30. [3.19390472e+02, 1.29295090e+02, 4.79000000e+02, 6.33003845e+02],
  31. [3.28933838e+02, 1.22736115e+02, 4.79000000e+02, 6.39000000e+02],
  32. [4.44292603e+01, 1.70438187e+02, 2.26841858e+02, 6.39000000e+02],
  33. [2.17988785e+02, 3.02472412e+02, 4.06062927e+02, 6.29106628e+02],
  34. [2.00241089e+02, 3.23755096e+02, 3.96929321e+02, 6.36386108e+02],
  35. [2.14310303e+02, 3.23443665e+02, 4.06732849e+02, 6.35775269e+02]])
  36. # 预测框得分
  37. scores = np.array([0.5247661 , 0.51759845, 0.86075854, 0.9910175 , 0.39170712,
  38. 0.9297706 , 0.5115228 , 0.270992 , 0.19087596, 0.64201415, 0.879036])
  39. # 画出所有预测框
  40. for box in boxes:
  41. draw_rectangle(currentAxis, box)

目标检测 - 图191

  1. <Figure size 720x720 with 1 Axes>

这里使用非极大值抑制(non-maximum suppression, nms)来消除冗余框,其基本思想是,如果有多个预测框都对应同一个物体,则只选出得分最高的那个预测框,剩下的预测框被丢弃掉。那么如何判断两个预测框对应的是同一个物体呢,标准该怎么设置?如果两个预测框的类别一样,而且他们的位置重合度比较大,则可以认为他们是在预测同一个目标。非极大值抑制的做法是,选出某个类别得分最高的预测框,然后看哪些预测框跟它的IoU大于阈值,就把这些预测框给丢弃掉。这里IoU的阈值是超参数,需要提前设置,YOLO-V3模型里面设置的是0.5。

比如在上面的程序中,boxes里面一共对应11个预测框,scores给出了它们预测"人"这一类别的得分。

  • Step0 创建选中列表,keep_list = []
  • Step1 对得分进行排序,remain_list = [ 3, 5, 10, 2, 9, 0, 1, 6, 4, 7, 8],
  • Step2 选出boxes[3],此时keep_list为空,不需要计算IoU,直接将其放入keep_list,keep_list = [3], remain_list=[5, 10, 2, 9, 0, 1, 6, 4, 7, 8]
  • Step3 选出boxes[5],此时keep_list中已经存在boxes[3],计算出IoU(boxes[3], boxes[5]) = 0.0,显然小于阈值,则keep_list=[3, 5], remain_list = [10, 2, 9, 0, 1, 6, 4, 7, 8]
  • Step4 选出boxes[10],此时keep_list=[3, 5],计算IoU(boxes[3], boxes[10])=0.0268,IoU(boxes[5], boxes[10])=0.0268 = 0.24,都小于阈值,则keep_list = [3, 5, 10],remain_list=[2, 9, 0, 1, 6, 4, 7, 8]
  • Step5 选出boxes[2],此时keep_list = [3, 5, 10],计算IoU(boxes[3], boxes[2]) = 0.88,超过了阈值,直接将boxes[2]丢弃,keep_list=[3, 5, 10],remain_list=[9, 0, 1, 6, 4, 7, 8]
  • Step6 选出boxes[9],此时keep_list = [3, 5, 10],计算IoU(boxes[3], boxes[9]) = 0.0577,IoU(boxes[5], boxes[9]) = 0.205,IoU(boxes[10], boxes[9]) = 0.88,超过了阈值,将boxes[9]丢弃掉。keep_list=[3, 5, 10],remain_list=[0, 1, 6, 4, 7, 8]
  • Step7 重复上述Step6直到remain_list为空

最终得到keep_list=[3, 5, 10],也就是预测框3、5、10被最终挑选出来了,如下图所示

  1. # 画图展示目标物体边界框
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. import matplotlib.patches as patches
  5. from matplotlib.image import imread
  6. import math
  7. # 定义画矩形框的程序
  8. def draw_rectangle(currentAxis, bbox, edgecolor = 'k', facecolor = 'y', fill=False, linestyle='-'):
  9. # currentAxis,坐标轴,通过plt.gca()获取
  10. # bbox,边界框,包含四个数值的list, [x1, y1, x2, y2]
  11. # edgecolor,边框线条颜色
  12. # facecolor,填充颜色
  13. # fill, 是否填充
  14. # linestype,边框线型
  15. # patches.Rectangle需要传入左上角坐标、矩形区域的宽度、高度等参数
  16. rect=patches.Rectangle((bbox[0], bbox[1]), bbox[2]-bbox[0]+1, bbox[3]-bbox[1]+1, linewidth=1,
  17. edgecolor=edgecolor,facecolor=facecolor,fill=fill, linestyle=linestyle)
  18. currentAxis.add_patch(rect)
  19. plt.figure(figsize=(10, 10))
  20. filename = '/home/aistudio/work/images/section3/000000086956.jpg'
  21. im = imread(filename)
  22. plt.imshow(im)
  23. currentAxis=plt.gca()
  24. boxes = np.array([[4.21716537e+01, 1.28230896e+02, 2.26547668e+02, 6.00434631e+02],
  25. [3.18562988e+02, 1.23168472e+02, 4.79000000e+02, 6.05688416e+02],
  26. [2.62704697e+01, 1.39430557e+02, 2.20587097e+02, 6.38959656e+02],
  27. [4.24965363e+01, 1.42706665e+02, 2.25955185e+02, 6.35671204e+02],
  28. [2.37462646e+02, 1.35731537e+02, 4.79000000e+02, 6.31451294e+02],
  29. [3.19390472e+02, 1.29295090e+02, 4.79000000e+02, 6.33003845e+02],
  30. [3.28933838e+02, 1.22736115e+02, 4.79000000e+02, 6.39000000e+02],
  31. [4.44292603e+01, 1.70438187e+02, 2.26841858e+02, 6.39000000e+02],
  32. [2.17988785e+02, 3.02472412e+02, 4.06062927e+02, 6.29106628e+02],
  33. [2.00241089e+02, 3.23755096e+02, 3.96929321e+02, 6.36386108e+02],
  34. [2.14310303e+02, 3.23443665e+02, 4.06732849e+02, 6.35775269e+02]])
  35. scores = np.array([0.5247661 , 0.51759845, 0.86075854, 0.9910175 , 0.39170712,
  36. 0.9297706 , 0.5115228 , 0.270992 , 0.19087596, 0.64201415, 0.879036])
  37. left_ind = np.where((boxes[:, 0]<60) * (boxes[:, 0]>20))
  38. left_boxes = boxes[left_ind]
  39. left_scores = scores[left_ind]
  40. colors = ['r', 'g', 'b', 'k']
  41. # 画出最终保留的预测框
  42. inds = [3, 5, 10]
  43. for i in range(3):
  44. box = boxes[inds[i]]
  45. draw_rectangle(currentAxis, box, edgecolor=colors[i])

目标检测 - 图192

  1. <Figure size 720x720 with 1 Axes>

非极大值抑制的具体实现代码如下面nms函数的定义,需要说明的是数据集中含有多个类别的物体,所以这里需要做多分类非极大值抑制,其实现原理与非极大值抑制相同,区别在于需要对每个类别都做非极大值抑制,实现代码如下面的multiclass_nms所示。

  1. # 非极大值抑制
  2. def nms(bboxes, scores, score_thresh, nms_thresh, pre_nms_topk, i=0, c=0):
  3. """
  4. nms
  5. """
  6. inds = np.argsort(scores)
  7. inds = inds[::-1]
  8. keep_inds = []
  9. while(len(inds) > 0):
  10. cur_ind = inds[0]
  11. cur_score = scores[cur_ind]
  12. # if score of the box is less than score_thresh, just drop it
  13. if cur_score < score_thresh:
  14. break
  15. keep = True
  16. for ind in keep_inds:
  17. current_box = bboxes[cur_ind]
  18. remain_box = bboxes[ind]
  19. iou = box_iou_xyxy(current_box, remain_box)
  20. if iou > nms_thresh:
  21. keep = False
  22. break
  23. if i == 0 and c == 4 and cur_ind == 951:
  24. print('suppressed, ', keep, i, c, cur_ind, ind, iou)
  25. if keep:
  26. keep_inds.append(cur_ind)
  27. inds = inds[1:]
  28. return np.array(keep_inds)
  29. # 多分类非极大值抑制
  30. def multiclass_nms(bboxes, scores, score_thresh=0.01, nms_thresh=0.45, pre_nms_topk=1000, pos_nms_topk=100):
  31. """
  32. This is for multiclass_nms
  33. """
  34. batch_size = bboxes.shape[0]
  35. class_num = scores.shape[1]
  36. rets = []
  37. for i in range(batch_size):
  38. bboxes_i = bboxes[i]
  39. scores_i = scores[i]
  40. ret = []
  41. for c in range(class_num):
  42. scores_i_c = scores_i[c]
  43. keep_inds = nms(bboxes_i, scores_i_c, score_thresh, nms_thresh, pre_nms_topk, i=i, c=c)
  44. if len(keep_inds) < 1:
  45. continue
  46. keep_bboxes = bboxes_i[keep_inds]
  47. keep_scores = scores_i_c[keep_inds]
  48. keep_results = np.zeros([keep_scores.shape[0], 6])
  49. keep_results[:, 0] = c
  50. keep_results[:, 1] = keep_scores[:]
  51. keep_results[:, 2:6] = keep_bboxes[:, :]
  52. ret.append(keep_results)
  53. if len(ret) < 1:
  54. rets.append(ret)
  55. continue
  56. ret_i = np.concatenate(ret, axis=0)
  57. scores_i = ret_i[:, 1]
  58. if len(scores_i) > pos_nms_topk:
  59. inds = np.argsort(scores_i)[::-1]
  60. inds = inds[:pos_nms_topk]
  61. ret_i = ret_i[inds]
  62. rets.append(ret_i)
  63. return rets

下面是完整的测试程序,在测试数据集上的输出结果将会被保存在pred_results.json文件中。

  1. import json
  2. ANCHORS = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326]
  3. ANCHOR_MASKS = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
  4. VALID_THRESH = 0.01
  5. NMS_TOPK = 400
  6. NMS_POSK = 100
  7. NMS_THRESH = 0.45
  8. NUM_CLASSES = 7
  9. if __name__ == '__main__':
  10. TRAINDIR = '/home/aistudio/work/insects/train/images'
  11. TESTDIR = '/home/aistudio/work/insects/test/images'
  12. VALIDDIR = '/home/aistudio/work/insects/val'
  13. with fluid.dygraph.guard():
  14. model = YOLOv3('yolov3', num_classes=NUM_CLASSES, is_train=False)
  15. params_file_path = '/home/aistudio/work/yolo_epoch50'
  16. model_state_dict, _ = fluid.load_dygraph(params_file_path)
  17. model.load_dict(model_state_dict)
  18. model.eval()
  19. total_results = []
  20. test_loader = test_data_loader(TESTDIR, batch_size= 1, mode='test')
  21. for i, data in enumerate(test_loader()):
  22. img_name, img_data, img_scale_data = data
  23. img = to_variable(img_data)
  24. img_scale = to_variable(img_scale_data)
  25. outputs = model.forward(img)
  26. bboxes, scores = model.get_pred(outputs,
  27. im_shape=img_scale,
  28. anchors=ANCHORS,
  29. anchor_masks=ANCHOR_MASKS,
  30. valid_thresh = VALID_THRESH)
  31. bboxes_data = bboxes.numpy()
  32. scores_data = scores.numpy()
  33. result = multiclass_nms(bboxes_data, scores_data,
  34. score_thresh=VALID_THRESH,
  35. nms_thresh=NMS_THRESH,
  36. pre_nms_topk=NMS_TOPK,
  37. pos_nms_topk=NMS_POSK)
  38. for j in range(len(result)):
  39. result_j = result[j]
  40. img_name_j = img_name[j]
  41. total_results.append([img_name_j, result_j.tolist()])
  42. print('processed {} pictures'.format(len(total_results)))
  43. print('')
  44. json.dump(total_results, open('pred_results.json', 'w'))

json文件中保存着测试结果,是包含所有图片预测结果的list,其构成如下:

  1. [[img_name, [[label, score, x1, x2, y1, y2], ..., [label, score, x1, x2, y1, y2]]],
  2. [img_name, [[label, score, x1, x2, y1, y2], ..., [label, score, x1, x2, y1, y2]]],
  3. ...
  4. [img_name, [[label, score, x1, x2, y1, y2],..., [label, score, x1, x2, y1, y2]]]]

list中的每一个元素是一张图片的预测结果,list的总长度等于图片的数目,每张图片预测结果的格式是:

  1. [img_name, [[label, score, x1, x2, y1, y2],..., [label, score, x1, x2, y1, y2]]]

其中第一个元素是图片名称image_name,第二个元素是包含该图片所有预测框的list, 预测框列表:

  1. [[label, score, x1, x2, y1, y2],..., [label, score, x1, x2, y1, y2]]

预测框列表中每个元素[label, score, x1, x2, y1, y2]描述了一个预测框,label是预测框所属类别标签,score是预测框的得分;x1, x2, y1, y2对应预测框左上角坐标(x1, y1),右下角坐标(x2, y2)。每张图片可能有很多个预测框,则将其全部放在预测框列表中。

在AI识虫比赛的基础版本中,老师提供了MAP指标计算代码,使用此pred_results.json文件即可计算出最终的评估指标。

模型效果及可视化展示

上面的程序展示了如何读取测试数据集的读片,并将最终结果保存在json格式的文件中。为了更直观的给读者展示模型效果,下面的程序添加了如何读取单张图片,并画出其产生的预测框。

  • 创建数据读取器以读取单张图片的数据
  1. # 读取单张测试图片
  2. def single_image_data_loader(filename, test_image_size=608, mode='test'):
  3. """
  4. 加载测试用的图片,测试数据没有groundtruth标签
  5. """
  6. batch_size= 1
  7. def reader():
  8. batch_data = []
  9. img_size = test_image_size
  10. file_path = os.path.join(filename)
  11. img = cv2.imread(file_path)
  12. img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  13. H = img.shape[0]
  14. W = img.shape[1]
  15. img = cv2.resize(img, (img_size, img_size))
  16. mean = [0.485, 0.456, 0.406]
  17. std = [0.229, 0.224, 0.225]
  18. mean = np.array(mean).reshape((1, 1, -1))
  19. std = np.array(std).reshape((1, 1, -1))
  20. out_img = (img / 255.0 - mean) / std
  21. out_img = out_img.astype('float32').transpose((2, 0, 1))
  22. img = out_img #np.transpose(out_img, (2,0,1))
  23. im_shape = [H, W]
  24. batch_data.append((image_name.split('.')[0], img, im_shape))
  25. if len(batch_data) == batch_size:
  26. yield make_test_array(batch_data)
  27. batch_data = []
  28. return reader
  • 定义绘制预测框的画图函数,代码如下。
  1. # 定义画图函数
  2. INSECT_NAMES = ['Boerner', 'Leconte', 'Linnaeus',
  3. 'acuminatus', 'armandi', 'coleoptera', 'linnaeus']
  4. # 定义画矩形框的函数
  5. def draw_rectangle(currentAxis, bbox, edgecolor = 'k', facecolor = 'y', fill=False, linestyle='-'):
  6. # currentAxis,坐标轴,通过plt.gca()获取
  7. # bbox,边界框,包含四个数值的list, [x1, y1, x2, y2]
  8. # edgecolor,边框线条颜色
  9. # facecolor,填充颜色
  10. # fill, 是否填充
  11. # linestype,边框线型
  12. # patches.Rectangle需要传入左上角坐标、矩形区域的宽度、高度等参数
  13. rect=patches.Rectangle((bbox[0], bbox[1]), bbox[2]-bbox[0]+1, bbox[3]-bbox[1]+1, linewidth=1,
  14. edgecolor=edgecolor,facecolor=facecolor,fill=fill, linestyle=linestyle)
  15. currentAxis.add_patch(rect)
  16. # 定义绘制预测结果的函数
  17. def draw_results(result, filename, draw_thresh=0.5):
  18. plt.figure(figsize=(10, 10))
  19. im = imread(filename)
  20. plt.imshow(im)
  21. currentAxis=plt.gca()
  22. colors = ['r', 'g', 'b', 'k', 'y', 'c', 'purple']
  23. for item in result:
  24. box = item[2:6]
  25. label = int(item[0])
  26. name = INSECT_NAMES[label]
  27. if item[1] > draw_thresh:
  28. draw_rectangle(currentAxis, box, edgecolor = colors[label])
  29. plt.text(box[0], box[1], name, fontsize=12, color=colors[label])
  • 使用上面定义的single_image_data_loader函数读取指定的图片,输入网络并计算出预测框和得分,然后使用多分类非极大值抑制消除冗余的框。将最终结果画图展示出来。
  1. import json
  2. import paddle
  3. import paddle.fluid as fluid
  4. ANCHORS = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326]
  5. ANCHOR_MASKS = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
  6. VALID_THRESH = 0.01
  7. NMS_TOPK = 400
  8. NMS_POSK = 100
  9. NMS_THRESH = 0.45
  10. NUM_CLASSES = 7
  11. if __name__ == '__main__':
  12. image_name = '/home/aistudio/work/insects/test/images/2599.jpeg'
  13. params_file_path = '/home/aistudio/work/yolo_epoch50'
  14. with fluid.dygraph.guard():
  15. model = YOLOv3('yolov3', num_classes=NUM_CLASSES, is_train=False)
  16. model_state_dict, _ = fluid.load_dygraph(params_file_path)
  17. model.load_dict(model_state_dict)
  18. model.eval()
  19. total_results = []
  20. test_loader = single_image_data_loader(image_name, mode='test')
  21. for i, data in enumerate(test_loader()):
  22. img_name, img_data, img_scale_data = data
  23. img = to_variable(img_data)
  24. img_scale = to_variable(img_scale_data)
  25. outputs = model.forward(img)
  26. bboxes, scores = model.get_pred(outputs,
  27. im_shape=img_scale,
  28. anchors=ANCHORS,
  29. anchor_masks=ANCHOR_MASKS,
  30. valid_thresh = VALID_THRESH)
  31. bboxes_data = bboxes.numpy()
  32. scores_data = scores.numpy()
  33. results = multiclass_nms(bboxes_data, scores_data,
  34. score_thresh=VALID_THRESH,
  35. nms_thresh=NMS_THRESH,
  36. pre_nms_topk=NMS_TOPK,
  37. pos_nms_topk=NMS_POSK)
  38. result = results[0]
  39. draw_results(result, image_name, draw_thresh=0.5)

通过上面的程序,清晰的给读者展示了如何使用训练好的权重,对图片进行预测并将结果可视化。最终输出的图片上,检测出了每个昆虫,标出了它们的边界框和具体类别。