img_dir = r"D:\datasets\CelebA\Img\img_celeba.7z\img_celeba"
anno_src = r"D:\datasets\CelebA\Anno\list_bbox_celeba.txt"
anno_landmarks_src = r"D:\datasets\CelebA\Anno\list_landmarks_celeba.txt"
save_dir = r".\test_data\MTCNN"
先看一下我们需要处理的数据集从哪里调用的
r"D:\datasets\CelebA\Img\img_celeba.7z\img_celeba"
:这个数据集中是人脸头像所在的所有的野生图像(人脸所处位置不一,图片shape不一)r"D:\datasets\CelebA\Anno\list_bbox_celeba.txt"
:在野生图像上人脸所处的目标框的坐标(左上角横纵坐标和宽高)r"D:\datasets\CelebA\Anno\list_landmarks_celeba.txt"
:在野生图像上人脸五个关键点所处的横纵坐标
以上的三种信息都可以通过labelme来直接标注。
def gen_sample(face_size, stop_value):
# 创建保存样本的目录
positive_img_dir = os.path.join(save_dir, str(face_size), "positive")
negative_img_dir = os.path.join(save_dir, str(face_size), "negative")
part_img_dir = os.path.join(save_dir, str(face_size), "part")
for dir_path in [positive_img_dir, negative_img_dir, part_img_dir]:
if not os.path.exists(dir_path):
os.makedirs(dir_path)
faca_size用来规定我们从野生图片中截取多大的图片,有12,24,48的选择,stop_value表示我们截取的图片的张数,也就是我们输入到PRO网络中需要数据集的个数,由于三个网络都是独立的,所以数量可以不一样。
我们假设目前我们创建的是24大小的训练数据集,首先创建了三个文件夹,分别用来存放24大小的正样本,负样本,偏样本。
# 创建保存标签的文件,并打开文件
anno_positive_filename = os.path.join(save_dir, str(face_size), "positive.txt")
anno_negative_filename = os.path.join(save_dir, str(face_size), "negative.txt")
anno_part_filename = os.path.join(save_dir, str(face_size), "part.txt")
创建三个txt文件,用来保存我们24大小的图片下正负样本的标签(至于有哪些标签待会儿再说 )
try:
with open(anno_landmarks_src) as f:
landmarks_list = f.readlines()
# 读取CelebA的标签文件
with open(anno_src) as f:
anno_list = f.readlines()
# 样本计数
positive_count = 0
negative_count = 0
part_count = 0
打开野生数据集的目标框和关键点坐标文件,按照每一行为单位打开
# 开打人脸框的标签,循环读取每一行
for i, (anno_line, landmarks) in enumerate(zip(anno_list, landmarks_list)):
# 跳过表头
if i < 2:
continue
anno_list和landmarks_list分别表示在野生图片下脸所在的目标框坐标和关键点坐标,利用zip来输出同一张野生图片的目标框坐标和关键点坐标
表头是一些说明性文字,有效的目标框坐标和关键点坐标的形式如下:↓
'''
000001.jpg 95 71 226 313
000002.jpg 72 94 221 306
'''
'''
000001.jpg 165 184 244 176 196 249 194 271 266 260
000002.jpg 140 204 220 204 168 254 146 289 226 289
'''
# 5个关键点
landmarks = landmarks.split()
# 定位框
strs = anno_line.split()
# 解析文件名字
img_name = strs[0].strip()
# 读取图像
img = Image.open(os.path.join(img_dir, img_name))
# 解析出宽度和高度
img_w, img_h = img.size
将二者全部用空格隔开
拿出该图片的名称img_name
利用img_name读取该野生图片的像素信息img
由于我们使用的Image读取图片,所以img是一个[h,w,3]的ndarray数组,利用.size来得到该野生图片的宽高以便于后面的归一化
# 转换框坐标的类型
x, y, w, h = float(strs[1].strip()), float(strs[2].strip()), float(strs[3].strip()), float(strs[4].strip())
# 标签矫正
x1 = int(x + w * 0.12)
y1 = int(y + h * 0.1)
x2 = int(x + w * 0.9)
y2 = int(y + h * 0.85)
将定位框的左上横纵坐标和宽高提取出来,转成浮点类型(再一次证明就算是给定的标签,它的数据也确实是字符串类型)
然后计算出该目标框的左上和右下的坐标(这里做了一个标签的矫正,相当于目标框往中心位置靠拢然后缩小了,换句话说目标框里面尽可能地只充满脸)
# 计算新的宽度和高度
w, h = x2 - x1, y2 - y1
# 判断坐标是否符合要求
if max(w, h) < 40 or x1 < 0 or x2 < 0 or y1 < 0 or y2 < 0:
continue
box = [x1, y1, x2, y2]
然后计算新的目标框的宽高。同时判断,如果过滤掉标注错误的图像,并且规定宽高至少有一个不能小于40,换句话说脸的范围不能太小了,我个人认为这个40只是一个自定义的数字,可以自己更改。
由此,我们就获得了目标框修正后在野生图像上的左上和右下的坐标(未归一化)
# 求出中心点和边长,偏移中心点和边长得到样本,每张图偏移5次
cx = x1 + w / 2
cy = y1 + h / 2
# 最大边长
max_side = max(w, h)
找到每个目标框中心点所在的坐标,并计算该目标宽高的最大值,以备后面制作正负样本,偏样本做准备。↑
# 记录5个关键点的坐标
px1 = float(landmarks[1].strip())
py1 = float(landmarks[2].strip())
px2 = float(landmarks[3].strip())
py2 = float(landmarks[4].strip())
px3 = float(landmarks[5].strip())
py3 = float(landmarks[6].strip())
px4 = float(landmarks[7].strip())
py4 = float(landmarks[8].strip())
px5 = float(landmarks[9].strip())
py5 = float(landmarks[10].strip())
得到该图片下关键点的位置坐标,准备进入样本的制作↑
float_num = [0.1, 0.1, 0.3, 0.5, 0.95, 0.95, 0.99, 0.99, 0.99, 0.99]
for _ in range(5):
# 随机偏移中心点坐标以及边长
seed = float_num[np.random.randint(0, len(float_num))]
# 中心点x坐标随机偏移
_cx = cx + np.random.randint(int(-cx * seed), int(cx * seed))
# 中心点y坐标随机偏移
_cy = cy + np.random.randint(int(-cy * seed), int(cy * seed))
# 最大边长随机偏移
_max_side = max_side + np.random.randint(int(-max_side * seed), int(max_side * seed))
从fload_num中随机选择一个偏移种子seed,seed的范围是0到1双开区间
先看看中心点偏移的特点,以x为例:int(-cx * seed), int(cx * seed)
在(-cx,cx)之间,那么_cx的范围就在(0,2cx)之间,y同理。
同时偏移后的目标框的长度也需要重新规定范围,其范围在(0,2max_size)之间。
# 得到偏移后的坐标值(方框)
_x1 = _cx - _max_side / 2
_y1 = _cy - _max_side / 2
_x2 = _x1 + _max_side
_y2 = _y1 + _max_side
# 偏移过大,偏出图像了,此时,不能用,应该再次尝试偏移
if _x1 < 0 or _y1 < 0 or _x2 > img_w or _y2 > img_h:
continue
利用偏移后的要素得到偏移后的目标框所在的左上和右下的下标(此时偏移后的目标框被强制转换成了方形,也就是等宽等高)。
# 偏移过大,偏出图像了,此时,不能用,应该再次尝试偏移
if _x1 < 0 or _y1 < 0 or _x2 > img_w or _y2 > img_h:
continue
# 记录偏移后的坐标
cbox = [_x1, _y1, _x2, _y2]
如果偏移后的左上角坐标在0以下,或者右下角坐标超出了野生图片的宽高范围,那么就停止本次循环,重新再随机挑选一个种子进行偏移
满足条件之后记录下偏移框在野生图像上的左上角和右下角坐标
# --------------
offset_x1 = (x1 - _x1) / _max_side
offset_y1 = (y1 - _y1) / _max_side
offset_x2 = (x2 - _x2) / _max_side
offset_y2 = (y2 - _y2) / _max_side
# 计算两个坐标点和5个关键点的偏移率
offset_px1 = (px1 - _x1) / _max_side
offset_py1 = (py1 - _y1) / _max_side
offset_px2 = (px2 - _x1) / _max_side
offset_py2 = (py2 - _y1) / _max_side
offset_px3 = (px3 - _x1) / _max_side
offset_py3 = (py3 - _y1) / _max_side
offset_px4 = (px4 - _x1) / _max_side
offset_py4 = (py4 - _y1) / _max_side
offset_px5 = (px5 - _x1) / _max_side
offset_py5 = (py5 - _y1) / _max_side
上面是这个网络的重中之重。上面的14个数就是我们要输入到网络中的确定边框位置的四要素和确定五个关键点坐标的偏移率(终于现身了)
# 根据偏移后的坐标截图图片,并缩放成要训练的大小
img_crop = img.crop(cbox)
img_crop = img_crop.resize((face_size, face_size))
将偏移框里面的内容从图片中截出来,然后resize成指定的大小,由于偏移框内本来就是方形的,所以resize图片并不会出现失真。
# 对偏移框和真实框做iou, 根据偏离程度划分样本
iou = tool.iou(box, np.array([cbox]))[0]
if iou > 0.7:
img_crop.save(os.path.join(positive_img_dir, "{0}.jpg".format(positive_count)))
anno_positive_file.write(
"positive/{0}.jpg {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15}\n".format(
positive_count, 1,
offset_x1, offset_y1, offset_x2, offset_y2, offset_px1, offset_py1, offset_px2, offset_py2,
offset_px3, offset_py3, offset_px4, offset_py4, offset_px5, offset_py5))
anno_positive_file.flush()
positive_count += 1
然后计算该偏移框与真实目标框之间的iou,如果大于0.7则认为偏移框里面的是正样本,并且将该偏移框的信息填写进positive.txt里面
elif 0.4 < iou < 0.6:
img_crop.save(os.path.join(part_img_dir, "{0}.jpg".format(part_count)))
anno_part_file.write(
"part/{0}.jpg {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15}\n".format(
part_count, 2,
offset_x1, offset_y1, offset_x2, offset_y2, offset_px1, offset_py1, offset_px2, offset_py2,
offset_px3, offset_py3, offset_px4, offset_py4, offset_px5, offset_py5))
anno_part_file.flush()
part_count += 1
elif iou < 0.2:
img_crop.save(os.path.join(negative_img_dir, "{0}.jpg".format(negative_count)))
anno_negative_file.write("negative/{0}.jpg 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n".format(negative_count))
anno_negative_file.flush()
negative_count += 1
count = positive_count + negative_count + part_count
if count > stop_value:
break
如果iou在0.4和0.6之间,则认为是偏样本,小于0.2则认为是负样本,依次将这些偏移框的信心填写进对应的文本中就可以了。
这里还有一个细节,也就是seed是从:
float_num = [0.1, 0.1, 0.3, 0.5, 0.95, 0.95, 0.99, 0.99, 0.99, 0.99]
其中0.1对应的正样本实现的方式,0.3和0.5对应偏样本实现的方式,0.95和0.99对应负样本实现的方式,也就是说最后形成的指定大小的训练集图片中,正样本,偏样本,负样本之间的比值是[1,1,3].