要训练一个有监督模型, 我们需要输入x 和标签 y, 还要定义损失函数. x 和 y 从哪里来? 如果你有时间和精力的话, 你自己标注, 标注方法前面已经讲过了. 如果你想用现成的数据集, 那我们就以 VOC2007 为例, 这个比较小, 下载也快, 打开的网站上也有 VOC2012, 使用方法也和 VOC2007 一样
下载完成解压后, Train/Validation Data (439 MB) 里面有 5 个文件夹
这些文件夹是什么意思呢?
<annotation>
<folder>VOC2007folder>
<filename>000012.jpgfilename>
<source>
<database>The VOC2007 Databasedatabase>
<annotation>PASCAL VOC2007annotation>
<image>flickrimage>
<flickrid>207539885flickrid>
source>
<owner>
<flickrid>KevBowflickrid>
<name>?name>
owner>
<size>
<width>500width>
<height>333height>
<depth>3depth>
size>
<segmented>0segmented>
<object>
<name>carname>
<pose>Rearpose>
<truncated>0truncated>
<difficult>0difficult>
<bndbox>
<xmin>156xmin>
<ymin>97ymin>
<xmax>351xmax>
<ymax>270ymax>
bndbox>
object>
annotation>
上面的标注有用的信息是 filename, object 中的 name 和 bndbox 4 个坐标值 (xmin, ymin, xmax, ymax), 有多个物体的话, 上面的几个有用信息就会重复出现. 标注的文件是 xml 文件, 和 json 大同小异
如何把这些数据输入网络里进行训练或者预测呢? 需要把图像和标签转换成网络能接受的格式, 一个是训练图像, 一个是标签, 这两个是成对输入的, 这里我新建一个文件夹, 取名为 data_set, 再将 Annotations 中标签文件和 JPEGImages 中的图像复制到 data_set 文件夹中, 方便处理. 我的目录是 data_set, 在工程目录中, 所以 上一篇文章 中的配置参数中的 data_set 用的就是相对路径
开始之前我们先来搞个函数, 方便后面调用. 功能就是把 data_set 中的文件列出来, 划分成训练集, 验证集与测试集, 划分方式按我们的思路来
# 取得图像和标注文件路径
# data_set_path: 数据集所在路径
# split_rate: 这些文件中用于训练, 验证, 测试所占的比例
# 如果为 None, 则不区分, 直接返回全部
# 如果只写一个小数, 如 0.8, 则表示 80% 为训练集, 20% 为验证集, 没有测试集
# 如果是一个 tuple 或 list, 只有一个元素的话, 同上面的一个小数的情况
# shuffle_enable: 是否要打乱顺序
# 返回训练集, 验证集和验证集路径列表
def get_data_set(data_set_path, split_rate = (0.7, 0.2, 0.1), shuffle_enable = True):
data_set = []
files = os.listdir(data_set_path)
for f in files:
ext = osp.splitext(f)[1]
if ext in (".jpg", ".png", ".bmp"):
img_path = osp.join(data_set_path, f)
ann_type = "" # 标注文件类型
ann_path = img_path.replace(ext, ".json")
if osp.exists(ann_path):
ann_type = "json"
else:
ann_path = img_path.replace(ext, ".xml")
if osp.exists(ann_path):
ann_type = "xml"
if "" == ann_type:
continue
data_set.append((img_path, ann_path, ann_type))
if shuffle_enable:
shuffle(data_set)
if None == split_rate:
return data_set
total_num = len(data_set)
if isinstance(split_rate, float) or 1 == len(split_rate):
if isinstance(split_rate, float):
split_rate = [split_rate]
train_pos = int(total_num * split_rate[0])
train_set = data_set[: train_pos]
valid_set = data_set[train_pos: ]
return train_set, valid_set
elif isinstance(split_rate, tuple) or isinstance(split_rate, list):
list_len = len(split_rate)
assert(list_len > 1)
train_pos = int(total_num * split_rate[0])
valid_pos = int(total_num * (split_rate[0] + split_rate[1]))
train_set = data_set[0: train_pos]
valid_set = data_set[train_pos: valid_pos]
test_set = data_set[valid_pos: ]
return train_set, valid_set, test_set
get_data_set 语法不复杂, 就没有过多的注释. 函数的作用就是把每个图像和标注文件路径变成一个 tuple, 放到一个 list 中, 顺便划分训练集, 验证集和测试集. 标签的文件类型可以是 xml 或者 json, 调用方式如下
# 取得目录
# DATA_PATH 在配置参数中
train_set, valid_set, test_set = get_data_set(DATA_PATH, split_rate = (0.8, 0.1, 0.1))
print("Total number:", len(train_set) + len(valid_set) + len(test_set),
" Train number:", len(train_set),
" Valid number:", len(valid_set),
" Test number:", len(test_set))
# 输出第一个元素
print("First element:", train_set[0])
输出如下
Total number: 5011 Train number: 4008 Valid number: 501 Test number: 502
First element: ('data_set\\005773.jpg', 'data_set\\005773.xml', 'xml')
RPN 的作用是自动选出一些可能是目标的区域, 是一个有监督型模型, 所以我们要有数据和标签, 输入数据简单, 就是图像. 那标签是什么? 标签就当然是标注的数据了, 只是你现在困惑的是怎么把标签和图像对应起来. 因为一个图像里面可能会有多个目标, 而且有分类的标签和回归的标签(暂时不讲, 因为我们的重点现在是分类). 作为入门的一些教程都只有一个标签, 多个标签一下子就让你找不着北
这里就要用到 IoU 的概念了. 还记得在特征图上的一个点对应 k 个 anchor 吗? 这些 anchor 会对应原图上的一个矩形: anchor box, 把 anchor box 和 ground truth 进行比较, 再用一些指标去判断是不是目标. 不用猜你都知道这个指示就是 IoU. 定义如下, 就是绿色面积 / 蓝色面积
在上图中, 特征图上的一个点的 anchor 映射回原图有一个矩形位置, 如橙色和紫色两个矩形, 绿色是标注的 ground truth. 橙色的矩形和绿色的矩形重叠部分比较小, 所以认为这个 anchor 是背景. 而紫色矩形和绿色矩形重叠分部较多, 可以认为是目标. 那重叠多少来区分呢? 有两个规则
你要知道 Feature Map 上的点如何 映射 回原图上, 在 VGG16 中, 卷积层有做 Padding, 所以不改变大小, 改变大小的是 Pooling, 一共使用了 4 次, 因为最后一个 Pooling 没有使用. 所以图像行数和列数都变为原来的 1 / 16. 又因为特征图上的点是 anchor box 的中心坐标, 映射回原图就指向一个原图的 感受野. 这个感受野有多大呢? 16×16. 所以特征图上的 (0, 0) 就映射到原图的 (0, 0, 15, 15) 这个区域. 现在我们分别把 k 个 anchor box 依次套上去, anchor box 中心就是感受野的中心. 靠边的 anchor box 会有一个问题, 当坐标 (0, 0) 映射到原图 (0, 0, 15, 15) 区域时, anchor box 坐标就会出现负数. 比如 128×128 时坐标就是 (-56, -56, 71, 71), 那这样的 anchor box 是超过了图像范围的, 有两个处理方式, 一是直接舍去, 二是截断成 (0, 0, 71, 71). 论文中讲的是训练阶段舍去不用, 预测时才截断
好了, 又差不多该上代码了
# 计算 IoU
# anchor_box 坐标格式为 (x1, y1, x2, y2)
# 交集
# a1: anchor_box1 a2: anchor_box2
def intersection(a1, a2):
x = max(a1[0], a2[0])
y = max(a1[1], a2[1])
w = min(a1[2], a2[2]) - x
h = min(a1[3], a2[3]) - y
if w < 0 or h < 0:
return 0
return w * h
# 并集 a1: anchor_box1 a2: anchor_box2
def union(a1, a2):
area_1 = (a1[2] - a1[0]) * (a1[3] - a1[1])
area_2 = (a2[2] - a2[0]) * (a2[3] - a2[1])
area_union = area_1 + area_2 - intersection(a1, a2)
return area_union
# IoU
def get_iou(a1, a2):
# 防止 left < right 或者 top < bottom
if a1[2] < a1[0]:
a1[2], a1[0] = a1[0], a1[2]
if a1[3] < a1[1]:
a1[3], a1[1] = a1[1], a1[3]
if a2[2] < a2[0]:
a2[2], a2[0] = a2[0], a2[2]
if a2[3] < a2[1]:
a2[3], a2[1] = a2[1], a2[3]
area_i = float(intersection(a1, a2))
area_u = float(union(a1, a2))
if area_u <= 0:
return 0
return area_i / area_u
随便测试一下
# 测试 IoU
a = (8, 8, 32, 64)
b = (3, 3, 32, 65)
print("iou(a, b) =", get_iou(a, b))
iou(a, b) = 0.7474972187166311
这是 Faster R-CNN 难点之一, Feature Map 假设是 m×n, 那就会产生 m×n×k 个 anchor box, 所以 anchor box 生成函数需要的参数就有 Feature Map 的尺寸, 还有原图到 Feature Map 缩小的倍数, 当然还有 anchor box 的尺寸和长宽比例. 分两步走, 第一步先生成 k 个基础的 anchor box
# 生成基础的 k 个 anchor box
def create_base_anchors(size = ANCHOR_SIZE, ratios = ANCHOR_RATIO):
anchors = []
for r in ratios:
# 各种比例下的边长
side_1 = [round((x * x * r) ** 0.5) for x in size]
side_2 = [round(s / r) for s in side_1]
# print(side_1, side_2)
# 组合各种边长
for i in range(len(size)):
anchors.append((-side_1[i] // 2, -side_2[i] // 2, side_1[i] // 2, side_2[i] // 2))
return anchors
打印出来看一下
# 测试基础 anchor box
base_anchors = create_base_anchors()
for a in base_anchors:
print(a, " w =", a[2] - a[0], "h =", a[3] - a[1])
打印结果如下, 正好满足各种比例和尺寸
(-23, -45, 22, 45) w = 45 h = 90
(-46, -91, 45, 91) w = 91 h = 182
(-91, -181, 90, 181) w = 181 h = 362
(-32, -32, 32, 32) w = 64 h = 64
(-64, -64, 64, 64) w = 128 h = 128
(-128, -128, 128, 128) w = 256 h = 256
(-46, -23, 45, 23) w = 91 h = 46
(-91, -45, 90, 45) w = 181 h = 90
(-181, -91, 181, 90) w = 362 h = 181
那这 k 种组合的 anchor box 怎么用呢, 我们还要把这些 box 套到原图上去, 为每一个 anchor 位置生成对应的 k 个 anchor box, 只需要将上面的基础 anchor box 的坐标加上感受野中心坐标就可以了
不过还差一个函数, 用于缩放图像, 因为训练集中的图像大小不一, 所以要将其最短边都统一到相同的尺寸, 论文中是 600. 这样比较能符合各种 anchor box 的尺度, 而我设置的配置参中这个参数是 300, 所以与之配套的 anchor box 的尺寸分别 64, 128, 256
# 图像缩放函数
# 返回缩放后的图像和缩放比例
def new_size_image(image, short_size = SHORT_SIZE):
img_shape = list(image.shape)
scale = 1.0
if img_shape[0] < img_shape[1]:
scale = short_size / img_shape[0]
img_shape[0] = short_size
img_shape[1] = round(img_shape[1] * scale)
else:
scale = short_size / img_shape[1]
img_shape[1] = short_size
img_shape[0] = round(img_shape[0] * scale)
new_image = cv.resize(image, (img_shape[1], img_shape[0]), interpolation = cv.INTER_LINEAR)
return new_image, scale
现在就可以在原图上生成 anchor box 了
# 在原图上生成训练的 anchor box
# feature_size: 特征图尺寸
# anchors: k 个基础 anchor box 坐标
# stride: 图像到特征图缩小倍数
def create_train_anchors(feature_size, base_anchors, stride = FEATURE_STRIDE):
anchors = []
for r in range(feature_size[0]): # 行
for c in range(feature_size[1]): # 列
for a in base_anchors:
anchors.append([c * stride + stride // 2 + a[0],
r * stride + stride // 2 + a[1],
c * stride + stride // 2 + a[2],
r * stride + stride // 2 + a[3]])
return anchors
接下来测试函数是否正确, 我只选了一个中心点来画, 要不是整个图都画满了. 注意 idx 是随机显示的序号, 后面的函数还会用到
# 测试 create_train_anchors 并画到图像上
idx = random.randint(0, len(train_set)) # 随机显示序号, 这个序号后面还会用到
print("test image index:", idx)
print("test image info:", train_set[idx])
image = cv.imread(train_set[idx][0]) # train_path 由 get_data_set 函数得来的
image, scale = new_size_image(image, SHORT_SIZE) # 缩放到统一尺寸
feature_size = (image.shape[0] // FEATURE_STRIDE, image.shape[1] // FEATURE_STRIDE)
print("image_size:", image.shape, "feature_size:", feature_size)
# 取得每一个 anchor box
anchors = create_train_anchors(feature_size, base_anchors, FEATURE_STRIDE)
print("anchor num:", len(anchors))
# 选一个靠中心点的位置画 k 个 anchor box
center = ((feature_size[0] // 2) * feature_size[1] + feature_size[1] // 2) * len(base_anchors)
# 画框颜色
colors = ((0, 0, 255), (0, 255, 0), (255, 0, 0))
img_copy = image.copy()
for i, a in enumerate(anchors[center: center + len(base_anchors)]):
cv.rectangle(img_copy, (a[0], a[1]), (a[2], a[3]), colors[i % 3], 2)
plt.figure("anchor_box", figsize = (8, 4))
plt.imshow(img_copy[..., : : -1]) # 这里的通道要反过来显示才正常
plt.show()
test image index: 1024
test image info: ('data_set\\007152.jpg', 'data_set\\007152.xml', 'xml')
image_size: (300, 449, 3) feature_size: (18, 28)
anchor num: 4536
示例代码可下载 Jupyter Notebook 示例代码
上一篇: 保姆级 Keras 实现 Faster R-CNN 一
下一篇: 保姆级 Keras 实现 Faster R-CNN 三