概念
精度高,最小可检测 12×12 的物体。
计算力需求低,大多数笔记本可以训练,手机可以运行。
算法思路
- 用小卷积核卷多次代替大卷积核卷一次。
- 把单目标侦测网络当成卷积核,通过图像金字塔缩放图片(最小边长不小于 12),多次扫描来侦测多个目标。
- IOU:衡量框的相似度,交并(交叉的框),交小(嵌套的框)。
- NMS(非极大值抑制):按照置信度给框从大到小排序,保留置信度最大的框,然后用这个框和其它框做 IOU,留下相似度小于阈值的框,再从剩下的框里找到置信度最大的保留,和其它框继续做 IOU 比较相似度。
数据处理
使用 CelebA 数据集,生成正样本、部分样本、负样本。(正样本 : 部分样本 : 负样本 = 1 : 1 : 3,因为图上大部分区域都是负样本)
设计标签
标签:地址 编号 x1偏移量 y1偏移量 x2偏移量 y2偏移量
positive/0.jpg 1 0.022935779816513763 0.4334862385321101 -0.013761467889908258 -0.0022935779816513763
增样
CelebA 数据集都是欧洲明星脸,要检测亚洲普通人脸,需要增样。
import os
from PIL import Image
import numpy as np
import utils
img_path = r"D:\class\CelebA_test\img"
label_path = r"D:\class\CelebA_test\label.txt"
save_path = r"D:\class\CelebA_"
# 3 个尺寸(12,24,48),3 种样本(positive-正样本,negative-负样本,part-部分样本)
for img_size in [12, 24, 48]:
img_path_positive = os.path.join(save_path, str(img_size), "positive")
img_path_part = os.path.join(save_path, str(img_size), "part")
img_path_negative = os.path.join(save_path, str(img_size), "negative")
for each in [img_path_positive, img_path_part, img_path_negative]:
if not os.path.exists(each):
os.makedirs(each)
# 3 个标签(positive-正样本,negative-负样本,part-部分样本)
label_path_positive = os.path.join(save_path, str(img_size), "positive.txt")
label_path_part = os.path.join(save_path, str(img_size), "part.txt")
label_path_negative = os.path.join(save_path, str(img_size), "negative.txt")
label_positive = open(label_path_positive, "w")
label_part = open(label_path_part, "w")
label_negative = open(label_path_negative, "w")
positive_count = 0
part_count = 0
negative_count = 0
# 读取标签
for i, info in enumerate(open(label_path).readlines()):
if i < 2:
continue
# 000001.jpg 95 71 226 313
info = info.strip().split(" ")
info = list(filter(bool, info))
# 打开图片
with Image.open(os.path.join(img_path, info[0])) as img:
x1 = float(info[1])
y1 = float(info[2])
w = float(info[3])
h = float(info[4])
# 排除奇异样本
if min(w, h) < 40 or x1 < 0 or y1 < 0:
continue
x2 = x1 + w
y2 = y1 + h
r_box = np.array([[x1, y1, x2, y2]])
# 标签中心点
cx = x1 + w / 2
cy = y1 + h / 2
# 每个尺寸生成 5 个样本
for _ in range(5):
# 建议框中心点(标签框中心点偏移 0.2 倍)
cx_ = cx + np.random.randint(-w * 0.2, w * 0.2)
cy_ = cy + np.random.randint(-h * 0.2, h * 0.2)
# 建议框边长(原图宽的 0.8 倍,原图长的 1.2 倍)
side = np.random.randint(min(w, h) * 0.8, max(w, h) * 1.2)
# 建议框坐标
x1_ = cx_ - side / 2
y1_ = cy_ - side / 2
x2_ = x1_ + side
y2_ = y1_ + side
# 左上角坐标超出图片
x1_ = np.maximum(0, cx_ - side / 2)
y1_ = np.maximum(0, cy_ - side / 2)
# 剪切建议框,设置尺寸
s_box = np.array([x1_, y1_, x2_, y2_])
s_img = img.crop(s_box)
s_img = s_img.resize((img_size, img_size))
# 新样本的标签:标签框相对于建议框的偏移量
# (标签坐标 - 建议框坐标) / 建议框边长
x1_offset = (x1 - x1_) / side
y1_offset = (y1 - y1_) / side
x2_offset = (x2 - x2_) / side
y2_offset = (y2 - y2_) / side
# 建议框和标签框做 IOU
iou = utils.iou(s_box, r_box)
if iou > 0.6:
# 正样本
label_positive.write("positive/{}.jpg 1 {} {} {} {}\n".format(positive_count, x1_offset, y1_offset, x2_offset, y2_offset))
s_img.save(os.path.join(img_path_positive, "{}.jpg".format(positive_count)))
positive_count += 1
elif iou > 0.4:
# 部分样本
label_part.write("part/{}.jpg 2 {} {} {} {}\n".format(part_count, x1_offset, y1_offset, x2_offset, y2_offset))
s_img.save(os.path.join(img_path_part, "{}.jpg".format(part_count)))
part_count += 1
elif iou < 0.3:
# 负样本
label_negative.write("negative/{}.jpg 0 0 0 0 0\n".format(negative_count))
s_img.save(os.path.join(img_path_negative, "{}.jpg".format(negative_count)))
negative_count += 1
# 用中心点偏移生成的样本,几乎没有负样本,所以取标签框外的部分作为负样本
img_w, img_h = img.size
# 左
if img_size < min(x1, y1):
side = np.random.randint(img_size, min(x1, y1))
x1_ = np.random.randint(0, x1 - side)
y1_ = np.random.randint(0, img_h - side)
x2_ = x1_ + side
y2_ = y1_ + side
cut_box = np.array([x1_, y1_, x2_, y2_])
crop_img = img.crop(cut_box)
crop_img = crop_img.resize((img_size, img_size))
label_negative.write("negative/{}.jpg 0 0 0 0\n".format(negative_count))
label_negative.flush()
crop_img.save(os.path.join(img_path_negative, "{}.jpg".format(negative_count)))
negative_count += 1
# 上
if img_size < min(x1, y1):
side = np.random.randint(img_size, min(x1, y1))
x1_ = np.random.randint(0, img_w - side)
y1_ = np.random.randint(0, y1 - side)
x2_ = x1_ + side
y2_ = y1_ + side
cut_box = np.array([x1_, y1_, x2_, y2_])
crop_img = img.crop(cut_box)
crop_img = crop_img.resize