保姆级 Keras 实现 Faster R-CNN 三

上一篇文章 中我们讲了如何生成 anchor box, 但是只是生成 anchor box 并不能直接开始训练, 因为还缺少标签, 所以本篇重点就放在如何为这些生成的 anchor box 打标签

一. 从 xml 或者 json 文件中读出标注信息

VOC 数据集 中的标签文件是 xml 格式, 上一篇文章 中大概看了一下, 而 json 格式的标注在 语义分割之 json 文件分析 中也分析了一下, 所以下面就直接上代码

# 从 xml 或 json 文件中读出 ground_truth
# data_set: get_data_set 函数返回的列表
# categories: 类别列表
# file_type: 标注文件类型
# 返回 ground_truth 坐标与类别
def get_ground_truth(label_path, file_type, categories):
    ground_truth = []
    with open(label_path, 'r', encoding = "utf-8") as f:
        if "json" == file_type:
            jsn = f.read()
            js_dict = json.loads(jsn)        
            shapes = js_dict["shapes"] # 取出所有图形

            for shape in shapes:
                if shape["label"] in categories:                
                    pts = shape["points"]
                    x1 = round(pts[0][0])
                    x2 = round(pts[1][0])
                    y1 = round(pts[0][1])
                    y2 = round(pts[1][1])

                    # 防止有些人标注的时候喜欢从右下角拉到左上角
                    if x1 > x2:
                        x1, x2 = x2, x1
                    if y1 > y2:
                        y1, y2 = y2, y1
                        
                    bnd_box = [x1, y1, x2, y2]
                    cls_id = categories.index(shape["label"])

                    # 把 bnd_box 和 cls_id 组合在一起, 后面可有会用得上
                    ground_truth.append([bnd_box, cls_id])
        elif "xml" == file_type:
            tree = et.parse(f)
            root = tree.getroot()
            for obj in root.iter("object"):

                cls_id = obj.find("name").text
                cls_id = categories.index(cls_id) # 类别 id

                bnd_box = obj.find("bndbox")
                bnd_box = [int(bnd_box.find("xmin").text),
                           int(bnd_box.find("ymin").text),
                           int(bnd_box.find("xmax").text),
                           int(bnd_box.find("ymax").text)]

                # 把 bnd_box 和 cls_id 组合在一起, 后面可有会用得上
                ground_truth.append([bnd_box, cls_id])
            
    return ground_truth

测试 get_ground_truth 函数

# 测试 get_ground_truth
label_data = train_set[idx] # idx 上面已经定义过了
gts = get_ground_truth(label_data[1], label_data[2], CATEGORIES)

img_copy = image.copy()
# 图像缩放之后, ground_truth 也要做相应的缩放, scale 由 new_size_image 返回
for gt in gts:
    gt[0][0] = round(gt[0][0] * scale)
    gt[0][1] = round(gt[0][1] * scale)
    gt[0][2] = round(gt[0][2] * scale)
    gt[0][3] = round(gt[0][3] * scale)
    
    print(gt, "class:", CATEGORIES[gt[1]])
    
    cv.rectangle(img_copy, (gt[0][0], gt[0][1]), (gt[0][2], gt[0][3]),
                 (0, random.randint(128, 256), 0), 2)
    
plt.figure("label_box", figsize = (8, 4))
plt.imshow(img_copy[..., : : -1]) # 这里的通道要反过来显示才正常
plt.show()
[[25, 40, 82, 101], 1] class: aeroplane
[[42, 136, 100, 190], 1] class: aeroplane
[[58, 215, 114, 269], 1] class: aeroplane
[[170, 128, 229, 184], 1] class: aeroplane
[[147, 26, 205, 86], 1] class: aeroplane
[[357, 13, 415, 75], 1] class: aeroplane

6 planes
嗯. 证明了代码还是没有问题的~~ 只是要注意 图像缩放之后, ground_truth 也要做相应的缩放
这里要说明一点, 在 VOC 数据集中有一些图是漏标注的, 所以当你测试的图像没有标注框的时候, 不一定是代码错了, 你要检查一下是不是真的漏标注了

二. 为 anchor box 打标签

1. 如何区分正负样本与中立样本

既然我们这么能干, 已经可以读出标签信息了, 那接下来肯定要为每一个 anchor box 打类别标签了. 这里的类别其实只有两类, 一类是背景, 一类是目标. 还没有到真正细分类别的时候, 只是替代 Selective Search 作为候选区域. 因为我们把激活函数替换成了 sigmoid, 所以标签就只是 0 或 1 了. 又因为我们把 RPN 网络中的回归部分也去掉了, 所以暂时就不用为回归打标签. 不用担心, 到时讲回归的时候会补回来的

标签为正样本还是负样本在 上一篇文章 中已经讲过规则了, 这里再重复一下

  • 与任意的 ground truth IoU ≥ 0.7 是目标, IoU ≤ 0.3 是背景, 0.3 < IoU < 0.7 这部分不用管, 不参加训练
  • 如果其中一个 ground truth 没有任何一个 anchor box 与之 IoU ≥ 0.7, 那与之 IoU 最大的那个 anchor box 也算目标, 也就是正样本

问: 正负样本的阈值分别是 0.3 和 0.7, 那 0.3 < IoU < 0.7 的样本怎么办, 又如何区分?

IoU 介于 0.3 到 0.7 的样本我们姑且叫作 中立 样本, 这样的样本是不参加训练的. 说不参加训练是假的, 因为都在一个网络里面走, 而真正的意思在计算 Loss 的时候不考虑它, 忽略它

至于如何区分中立样本, 数字 1 作为正样本, 0 作为负样本是比较符合我们习惯的, 那我们就把 -1 作为中立样本, 在计算 Loss 的时候只计算标签为 0 或者 1 的样本

2. 代码实现

用代码怎么实现上面的两条规则呢? 我们先用一个表来理解

a \ g g 1 g_1 g1 g 2 g_2 g2 g 3 g_3 g3 g 4 g_4 g4
a 1 a_1 a10.10.80.30.9
a 2 a_2 a20.10.80.10.5
a 3 a_3 a30.60.00.30.4
a 4 a_4 a40.10.20.80.4
a 5 a_5 a50.20.20.00.1

表格中 a i a_i ai g i g_i gi 分别表示 anchor box 和 ground truth

先看行数据, 第 1 行中 Iou( a 1 a_1 a1, g 4 g_4 g4) = 0.9, 所以 a 1 a_1 a1 是正样本, 同时 IoU( a 1 a_1 a1, g 2 g_2 g2) = 0.8, 虽然也大于了正样本阈值 0.7, 但是 a 1 a_1 a1 所对应的 ground truth 应该 g 4 g_4 g4, 算回归偏移的时候要以 g 4 g_4 g4 为准. 第 2 行 IoU( a 2 a_2 a2, g 2 g_2 g2) = 0.8, 所以 a 2 a_2 a2 也是正样本. 第 3 行 0.3 < Iou( a 3 a_3 a3, g 1 g_1 g1) < 0.6, 暂时算中立样本. 同理, 第 4 行 a 4 a_4 a4 也是正样本, 第 5 行 max IoU < 0.3, 所以 a 5 a_5 a5 是负样本

为什么说 a 3 a_3 a3 暂时算中立样本呢? 因为 g 1 g_1 g1 没有一个 anchor box 和它的 IoU ≥ 0.7, 也就是说 g 1 g_1 g1 没有与之对应的正样本, 所以我们这时要从列方向来看数据. g 1 g_1 g1 所在列与之 IoU 最大的是 a 3 a_3 a3, 按照规则, 要将 a 3 a_3 a3 算作正样本

到这里是否终于明白了那两条规则了, 那就可以上代码了

# 为每一个 anchor box 打类别标签
# anchors: create_train_anchors 生成的 anchor_box
# train_num: 每一张图中参加训练的样本数量
# 返回每一个 anchor box 的标签类型 1: 正, 0: 负: -1: 中立

POS_VAL = 1 # 正样本
NEG_VAL = 0 # 负样本
NEUTRAL = -1 # 其他不参与计算 loss 的样本

def get_rpn_cls_label(img_shape, anchors, ground_truth,
                      pos_thres = 0.7, neg_thres = 0.3, train_num = TRAIN_NUM):
    
    cls_labels = [] # 存放每个 anchor_box 的标签值和对应的 gt 坐标
    iou_matrix = [] # 暂时用来存放每个 anchor_box 与 每个 gt 的 iou, 后面用来判断是正样本还是负样本
                    # anchor_box 为列, ground_truth 为行, 组合成一个二维列表
                    # 交点就是 第 i 个 anchor_box 与 第 j 个 gt 的 iou
                    # 这样做的目录是方便为 anchor_box 分配一个与之 iou 最大的 ground_truth box
    for a in anchors:
        row_iou = [] # 行, 一个 anchor_box 与 所有 gt 的 iou        
        for gt in ground_truth:
            '''
            # 截断代码-------------------------
            a[0] = max(a[0], 0)
            a[1] = max(a[1], 0)
            a[2] = min(a[2], img_shape[1] - 1)
            a[3] = min(a[3], img_shape[0] - 1)
            
            iou = get_iou(a, gt[0]) # gt[0] 表示 ground_true box, gt[1] 表示类别 
            # 截断代码-------------------------
            '''
            
            # 舍去代码-------------------------
            iou = get_iou(a, gt[0]) # gt[0] 表示 ground_true box, gt[1] 表示类别
            
            if a[0] < 0 or a[1] < 0 or a[2] >= img_shape[1] or a[3] >= img_shape[0]:
                iou = -1.0 # 没有赋值为 0, 是因为这样的样本也不参加训练
            # 舍去代码-------------------------
            
            row_iou.append(iou)
        iou_matrix.append(row_iou)
        
    for r in iou_matrix:
        # 每一行的最大 iou, 意思就是: 计算某一个 anchor_box 与 每个 gt 的 iou 后取最大值
        max_iou = max(r)
        # 如果与其中一个 gt 的 iou >= pos_thres, 则为正样本
        if (max_iou >= pos_thres):
            gt = ground_truth[r.index(max_iou)][0]     # 这样做不担心有两个一样大的 IoU 吗? 你自己想一下
            cls_labels.append((POS_VAL, gt))           # 把 gt 加进去方便后面的回归标签生成
            
        elif (0 <= max_iou <= neg_thres):              # 这里要判断 0 <= max_iou 是排除超边界的框
                                                       # 在 batch_size > 1 训练时会做 padding, 所以不排除的话
                                                       # padding 的黑边就会参与训练
            cls_labels.append((NEG_VAL, [0, 0, 0, 0])) # 负样本的 gt 直接赋值为 0, 保持数据的格式一致      
            
        else:
            cls_labels.append((NEUTRAL, [0, 0, 0, 0]))
            
    # 如果某一个 gt 没有一个 anchor box 与它的 iou >= pos_thres
    # 那就要找出与之最大 iou 的那个 anchor box 为正样本
    # 但是这个 iou 还是要大于 0
    for g in range(len(ground_truth)):
        max_iou = 0;
        for a in range(len(anchors)):
            # a 行 g 列
            if (iou_matrix[a][g] > max_iou):
                max_iou = iou_matrix[a][g]
                
        if 0 < max_iou < pos_thres:
            # 当 anchor_box 与 gt box IoU < pos_thres 时
            # 一个 gt box 可能有多个与之 IoU 一样大的 anchor_box, 比如 anchor_box 在 gt 的内部
            # 所有这些 anchor_box 与 gt IoU 一样大的都要设置成正样本
            for a in range(len(anchors)):
                 if iou_matrix[a][g] >= max_iou:
                    cls_labels[a] = (POS_VAL, ground_truth[g][0])
                    
    # 取出所有正样本与负样本的序号, 方便计数与打乱处理
    positives = [i for i, x in enumerate(cls_labels) if POS_VAL == x[0]]
    negatives = [i for i, x in enumerate(cls_labels) if NEG_VAL == x[0]]
    
    shuffle(positives) # 打乱
    shuffle(negatives)
    
    # 如果正样本数量超过 train_num // 2, 随机选 train_num // 2 个,
    # 上面打乱后直接取前 train_num // 2 个
    pos_num = min(train_num // 2, len(positives))
    for p in positives[pos_num: ]:
        cls_labels[p] = (NEUTRAL, [0, 0, 0, 0]) # 去掉多余的正样本
            
    # 参加训练的负样本的数量
    train_negs = train_num - pos_num
    for n in negatives[train_negs: ]:
        cls_labels[n] = (NEUTRAL, [0, 0, 0, 0]) # 去掉多余的负样本
            
    cls_ids = []  # 每个 anchor 标签, POS_VAL, NEG_VAL 或 NEUTRAL
    gt_boxes = [] # 每个 anchor 对应的 gt 坐标
    
    for label in cls_labels:
        cls_ids.append(label[0])
        gt_boxes.append(label[1])
        
    return cls_ids, gt_boxes

上面注释的截断代码和舍去代码, 你选一种你喜欢的方式就可以了, 接下来测试

# 测试 get_rpn_cls_label, 将其画到图像上, 这里 train_num 设置为 32, 方便显示
rpn_cls_label, gt_boxes = get_rpn_cls_label(image.shape, anchors, gts, train_num = 32)

print("positive boxes: ", rpn_cls_label.count(POS_VAL))
print("negative boxes: ", rpn_cls_label.count(NEG_VAL))

img_copy = image.copy()

for i, a in enumerate(anchors):
    if POS_VAL == rpn_cls_label[i]:
        gt = gt_boxes[i]
        # 测试 get_rpn_cls_label 带出来的 gt 是否正确
        cv.rectangle(img_copy, (gt[0], gt[1]), (gt[2], gt[3]), (255, 55, 55), 2)
        cv.rectangle(img_copy, (a[0], a[1]), (a[2], a[3]), (0, 255, 0), 2)
    elif NEG_VAL == rpn_cls_label[i]:
        cv.rectangle(img_copy, (a[0], a[1]), (a[2], a[3]), (0, 0, random.randint(128, 256)), 1)
        
plt.figure("anchor_box", figsize = (8, 4))
plt.imshow(img_copy[..., : : -1]) # 这里的通道要反过来显示才正常
plt.show()
positive boxes:  7
negative boxes:  25

rpn_class
测试图中, 红框为负样本, 绿框为正样本, 蓝色框是对应的 ground truth. 到这里标签的生成就算完成了

三. 代码下载

示例代码可下载 Jupyter Notebook 示例代码

上一篇: 保姆级 Keras 实现 Faster R-CNN 二
下一篇: 保姆级 Keras 实现 Faster R-CNN 四

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr-MegRob

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值