前面几个章节我们实现了用于PNet网络训练的若干种训练数据的生成办法。首先是构建了三种数据,分别为neg, part, pos,每种数据都是规格为12*12的图片,其中第一种图片不包含人脸,或者人脸占据的比率不超过30,第二种包含部分人脸,其比率不超过45%,第三种包含人脸的比率超过了65%,这三种图片的目的由于训练网络识别出给定的图片内是否有人脸出现。

然而网络训练的目的不仅仅是要判断出图片中是否有人脸,而且还要能准确的找出人脸在图片中的准确位置,为了实现这点,算法还需要训练网络识别人脸五个关键点所在的坐标,这五个关键点分别对应两个眼睛,中间鼻子和两边嘴角,一旦网络能准确找到这五个关键点的坐标,说明网络能准确把握住人脸的根本特征。

由于PNet的能力是用于识别12*\12规格的图片中是否存在人脸,因此我们需要把图片缩放到给定规格后才能输入网络。这样就带来一个问题,那就是用于训练网络的相关数据会发生改变,例如在原图中,左眼所在坐标是(100,150),然后经过缩放后坐标自然会发生改变,而且在处理图片时,为了增强训练效果,我们还会对图片进行旋转,翻转等操作,于是原来给定关键点数据坐标也会发生改变。

处理这种情况的做法是,将具体坐标转换为相对位置的偏移。例如假设原图大小为100*100,左眼坐标为(100,150),那么我们计算该坐标相对于图片左上角的偏移比率,于是左眼x坐标相对偏移比率是100/200=0.5, y坐标相对偏移比率是150/200 = 0.75,于是左眼坐标转换为(0.5, 0.75),于是当该图片缩放为规格12*12后,我们可以很容易恢复出缩放后左眼坐标,那就是(120.5, 120.75)=(6,0.765),因此通过计算相对偏移比率,我们可以避免训练数据在图片经过转换后产生的错误,下面就是实现计算坐标偏移比率的代码:

class BBox:
    def  __init__(self, box):#box是人脸区域
        self.left = box[0]
        self.top = box[1]
        self.right = box[2]
        self.bottom = box[3]

        self.x = box[0]
        self.y = box[1]
        self.w = box[2] - box[0]
        self.h = box[3] - box[1]
    def  project(self, point):
      #point对应人脸区域内的一点,要将它的绝对坐标转换为相对于左上角的偏移比率
      x = (point[0] - self.x) / self.w
      y = (point[1] - self.y) / self.h
      return np.asarray([x,y])
    def  reproject(self, point):
      #将相对偏移比率改为绝对坐标值
      x = self.x + self.w * point[0]
      y = self.y + self.h * point[1]
      return np.asarray([x,y])
    def  reprojectLandmark(self, landmark):
      #将特征点对应的偏移比率转为绝对坐标值
      p = np.zeros((len(landmark), 2))
      for i in range(len(landmark)):
          p[i] = self.reproject(landmark[i])
      return p 
    def  projectLandmark(self, landmark):
      #将特征点对应的坐标值改为相对偏移比率
      p = np.zeros((len(landmark), 2))
      for i in range(len(landmark)):
          p[i] = self.project(landmark[i])
      return p

接下来我们就需要针对特定的训练数据集做相关操作。在人脸识别应用中,最常用的数据集叫LFW,前面章节我们也提到过,其下载链接为:
链接: https://pan.baidu.com/s/1ODdzlMM_t_36-ldVOl7ySg 密码: f2cq
从链接下载数据解压后可以发现,它除了包含很多人脸图片外,还包含了一个非常重要的说明文件叫trainImageList.txt,该文件包含了图片数据的重要说明,其中的一些记录条目如下:

lfw_5590\Aaron_Eckhart_0001.jpg 84 161 92 169 106.250000 107.750000 146.750000 112.250000 125.250000 142.750000 105.250000 157.750000 139.750000 161.750000

上面是一个记录的内容,可以看到信息可以通过空格分隔开。其中第一个空格前面的信息也就是“lfw_5590\Aaron_Eckhart_0001.jpg”它对应的就是图片目录和名称,接下来四个数据“84 161 92 169”对应的是图片中人脸的左上角和右下角坐标,这四个数据就是我们需要进行偏移比率计算的对象,最后是10个数据“106.250000 107.750000 146.750000 112.250000 125.250000 142.750000 105.250000 157.750000 139.750000 161.750000”,其中每两个数据为一组,对应的就是一个关键点的坐标,后面我们需要训练网络能在读取人脸图片后,将这些关键点坐标计算出来。

根据一条记录的组成特点,我们实现下面函数用于读取数据集对应的记录文件:

def  getDataFromTxt(txt, data_path, with_landmark = True):
    with open(txt, 'r') as f:
      lines = f.readlines() #读取lwf数据集对应描述文件中的每一行
    result = []
    for line in lines:
        line = line.strip()
        components = line.split(' ')
        img_path = os.path.join(data_path, components[0]).replace("\\", '/')
        box = (components[1], components[3], components[2], components[4]) #人脸区域
        box = [float(_) for _ in box]
        box = list(map(int, box))
        #print("getDataFromTxt->path:{}, box: {}".format(img_path, box))
        if not with_landmark:
            result.append((img_path, BBox(box)))
            continue 
        landmark = np.zeros((5, 2)) #5个关键点坐标值
        for index in range(5):
            rv = (float(components[5+ 2*index]), float(components[5 + 2*index+1]))
            landmark[index] = rv 
        result.append((img_path, BBox(box), landmark))
    return result

上面函数从给定文件trainImageList.txt中读取出每条记录,然后根据记录的组成规律,分别读取图片,人脸的坐标,以及五个关键点坐标,接下来我们需要做得是,通过每条记录读取图片,将图片中的人脸专门截取出来形成一个单独的图片文件,然后将人脸坐标转换成偏移比率,同时也要讲五个关键点坐标转换成偏移比率,最后将截取出来的人脸图片所在路径,转换后的人脸坐标以及关键点坐标组成一条记录信息,然后将记录信息写到一个专门文件中,形成类似trainImageList.txt这样的特征说明文件,代码实现如下:

def  gen_landmark_aug(size):
    #总共分三种情况,size=12对应pnet,size == 24对应rnet,size == 48对应onet
    argument = True
    if size == 12:
        net = 'PNet'
    elif size == 24:
        net = 'RNet'
    elif size == 48:
        net = 'ONet'
    image_id = 0
    data_dir = '/content/'
    OUTPUT = os.path.join(data_dir, str(size))
    if  not os.path.exists(OUTPUT):
        os.mkdir(OUTPUT)
    dstdir = os.path.join(OUTPUT, 'train_%s_landmark_aug' % (net))
    if not os.path.exists(dstdir):
        os.mkdir(dstdir)
    ftxt = os.path.join(data_dir, "trainImageList.txt") #读取含有关键点数据的文档
    f = open(os.path.join(OUTPUT, 'landmark_%d_aug.txt'%(size)), 'w')
    data = getDataFromTxt(ftxt, data_dir)
    idx = 0
    for (img_path, box, landmarkGt) in tqdm(data):
        F_imgs = []
        F_landmarks = []
        img = cv2.imread(img_path)
        img_h, img_w, img_c = img.shape
        gt_box = np.array([box.left, box.top, box.right, box.bottom])
        try:
          f_face = img[box.top:box.bottom+1, box.left:box.right+1, :] #将人脸截取出来
          f_face = cv2.resize(f_face, (size, size), interpolation = cv2.INTER_LINEAR) #伸缩成给定大小,此时原来关键点坐标会失效,需要使用相对偏移
        except Exception as e:
          print("resize err with path: ", img_path)
          print("resize err with shape: ", np.shape(img))
          continue

        landmark = np.zeros((5, 2))
        for index, one in enumerate(landmarkGt):
            rv = box.project(one)  #注意这里的逻辑
            landmark[index] = rv #这里获得关键点相对于人脸区域的偏移比例
        F_imgs.append(f_face) #伸缩后的人脸
        F_landmarks.append(landmark.reshape(10)) #关键点偏移比率
        landmark = np.zeros((5, 2))
        if argument: #这里会对人脸区域进行拉伸,旋转等变换,因此人脸区域和关键点也要在变换后重新计算
            idx = idx + 1
            x1, y1, x2, y2 = gt_box
            gt_w = x2 - x1 + 1
            gt_h = y2 - y1 + 1
            if max(gt_w, gt_h) < 40 or x1 < 0 or y1 < 0:  #忽略尺寸太小的人脸
                continue
            for i in range(20): 
                '''
                根据人脸区域随机裁剪,其做法与前面获取neg, pos, part等图像的方法是一样的
                '''
                box_size = npr.randint(int(min(gt_w, gt_h)*0.8), np.ceil(1.25 * max(gt_w, gt_h)))
                delta_x = npr.randint(-gt_w * 0.2, gt_h * 0.2)
                delta_y = npr.randint(-gt_h * 0.2, gt_h * 0.2)
                nx1 = int(max(x1 + gt_w /2 - box_size / 2 + delta_x, 0))
                ny1 = int(max(y1 + gt_h / 2 - box_size / 2 + delta_y, 0))
                nx2 = nx1 + box_size
                ny2 = nx2 + box_size
                if nx2 > img_w or ny2 > img_h:
                    continue
                crop_box = np.array([nx1, ny1, nx2, ny2])
                cropped_im = img[ny1:ny2+1, nx1:nx2+1, :]
                try:
                    resized_im = cv2.resize(cropped_im, (size, size), interpolation = cv2.INTER_LINEAR)
                except Exception as e:
                    print("resize error with resized_im shape: ", np.shape(resized_im))
                    print("resize error with cropped_im shape: ", np.shape(cropped_im))
                    print("resize error for size: ", nx1, ny1, nx2, ny2)
                    return
                iou = IOU(crop_box, np.expand_dims(gt_box, 0))
                if iou > 0.65: #如果区域含有人脸
                    print("crop pos face")
                    F_imgs.append(resized_im)
                    for index, one in enumerate(landmarkGt):
                        box = BBox([nx1, ny1, nx2, ny2])
                        landmark[index] = box.project(one)
                    F_landmarks.append(landmark.reshape(10))
                    landmark = np.zeros((5,2))
                    landmark_ = F_landmarks[-1].reshape(-1, 2)
                    box = BBox([nx1, ny1, nx2, ny2])
                    if random.choice([0,1]) > 0:
                        face_flipped, landmark_flipped = flip(resized_im, landmark_) #翻转操作
                        face_flipped = cv2.resize(face_flipped, (size, size), interpolation = cv2.INTER_LINEAR)
                        F_imgs.append(face_flipped)
                        F_landmarks.append(landmark_flipped.reshape(10))
                        print("flip landmark: ", landmark_flipped)
                    if  random.choice([0,1]) > 0: #旋转和翻转操作
                        #逆时针旋转
                        face_rotated_by_alpha, landmark_rotated = rotate(img, box, box.reprojectLandmark(landmark_), 5)
                        landmark_rotated = box.projectLandmark(landmark_rotated)
                        print("rotate 5 landmark: ", landmark_rotated)
                        face_rotated_by_alpha = cv2.resize(face_rotated_by_alpha, (size, size), interpolation = cv2.INTER_LINEAR) #注意到由于关键点坐标转换成了相对于左上角的偏移比率,因此人脸图像缩放不会影响关键点坐标比率
                        F_imgs.append(face_rotated_by_alpha)
                        F_landmarks.append(landmark_rotated.reshape(10))
                        #对旋转后的人脸接着做左右翻转
                        face_flipped, landmark_flipped = flip(face_rotated_by_alpha, landmark_rotated)
                        face_flipped = cv2.resize(face_flipped, (size, size), interpolation = cv2.INTER_LINEAR)
                        F_imgs.append(face_flipped)
                        F_landmarks.append(landmark_flipped.reshape(10))
                    if  random.choice([0, 1]) > 0:  #顺时针翻转
                        face_rotated_by_alpha, landmark_rotated = rotate(img, box, box.reprojectLandmark(landmark_), -5)
                        landmark_rotated = box.projectLandmark(landmark_rotated)
                        print("rotate -5 landmark: ", landmark_rotated)
                        face_rotated_by_alpha = cv2.resize(face_rotated_by_alpha, (size, size), interpolation = cv2.INTER_LINEAR)
                        F_imgs.append(face_rotated_by_alpha)
                        F_landmarks.append(landmark_rotated.reshape(10))
                        #在顺时针翻转的图像基础上再进行左右翻转
                        face_flipped, landmark_flipped = flip(face_rotated_by_alpha, landmark_rotated)
                        face_flipped = cv2.resize(face_flipped, (size, size), interpolation = cv2.INTER_LINEAR)
                        F_imgs.append(face_flipped)
                        F_landmarks.append(landmark_flipped.reshape(10))
        F_imgs, F_landmarks = np.asarray(F_imgs), np.asarray(F_landmarks)
        for i in range(len(F_imgs)):
            '''
            数据清理,将那些关键点偏移比率小于0或者大于1的数据排除掉,产生这种情况时往往
            是原来数据有问题
            '''
            if  np.sum(np.where(F_landmarks[i] <= 0, 1, 0)) > 0:
                print("landmark <= 0: ", F_landmarks[i])
                continue
            if  np.sum(np.where(np.where(F_landmarks[i] >= 1, 1, 0))) > 0:
                print("landmark >= 1: ", F_landmarks[i])
                continue
            #将旋转和翻转后的人脸数据写成文件
            cv2.imwrite(os.path.join(dstdir, '%d.jpg'%(image_id)), F_imgs[i])
            landmarks = list(map(str, list(F_landmarks[i])))
            print("write landmark to file")
            #将人脸图像的路径和对应的关键点坐标写入到文件中
            f.write(os.path.join(dstdir, '%d.jpg' % (image_id)) + ' -2 ' + ' '.join(landmarks) + '\n')
            image_id += 1
    f.close()
    return F_imgs, F_landmarks

经过上面处理以及前面章节描述的数据处理,现在我们总共有了4个数据说明文件,分别是neg_12.txt,其中记录了不包含人脸或人脸占比小于30%的图片目录,part_12.txt,其中记录了包含人脸比率不超过45%的图片位置,最后是pos_12.txt,其中记录了人脸比率超过65%的图片目录,最后是landmark_12_aug.txt,其中包含了人脸图片路径以及人脸坐标偏移比率和关键点坐标偏移比率,接下来我们要把四个文件的内容合成一个文件:

pos_save_dir = "/content/drive/MyDrive/my_mtcnn/12/positive/pos_12.txt"
part_save_dir  = "/content/drive/MyDrive/my_mtcnn/12/part/part_12.txt"
neg_save_dir = "/content/drive/MyDrive/my_mtcnn/12/neg/neg_12.txt"
landmark_file_dir = '/content/12/landmark_12_aug.txt'
data_dir = '/content/drive/MyDrive/my_mtcnn/data'
def  combine_files():
    size = 12
    with open(pos_save_dir, 'r') as f:
        pos = f.readlines()
    with open(part_save_dir, 'r') as f:
        part = f.readlines()
    with open(neg_save_dir, 'r') as f:
        neg = f.readlines()
    with open(landmark_file_dir, 'r') as f:
        landmark = f.readlines()
    if not os.path.exists(data_dir):
        os.makedir(data_dir)
    with open(os.path.join(data_dir, 'train_pnent_landmark.txt'), 'w') as f:
        nums = [len(neg), len(pos), len(part)]
        base_num = 250000
        print('neg count:{}, pos count:{}, part cont:{}, base_num: {}'.format(len(neg),
                                                            len(pos), len(part), base_num))
        if len(neg) > base_num * 3:
            neg_keep = npr.choice(len(neg), size = base_num * 3, replace = True)
        else:
            neg_keep = npr.choice(len(neg), size = len(neg), replace = True)
        sum_p = len(neg_keep) // 3 
        pos_keep = npr.choice(len(pos), sum_p, replace = True)
        part_keep = npr.choice(len(part), sum_p, replace = True)
        print('neg count:{}, pos count:{}, part cont:{}'.format(len(neg_keep), len(pos_keep), len(part_keep)))
        for i in pos_keep:
            f.write(pos[i])
        for i in neg_keep:
            f.write(neg[i])
        for i in part_keep:
            f.write(part[i])
        for i in landmark:
            f.write(i)
        
 combine_files()

以上就是本节的基本内容,不得不承认,这几节关于训练数据处理的内容稍显枯燥和无聊,在深度学习算法应用的项目中,80%的工作内容都有关于训练数据的预处理。到这里有关数据处理的工作依然没有完成,下一节我们需要将这几节处理的数据转换成Tensorflow训练框架下特定的数据结构TFRecord。