Tensorflow的图像操作(三)

Tensorflow的图像操作(二)

这里我们重点来看一下这个train方法,在训练的部分有一个非常重要的点就是如何去进行样本的选择。如果使用triplet loss训练我们的网络结构,会存在一个非常严重的问题,就是正负样本的样本对的数量存在很大的差异。这个时候会进行难样本的挖掘,在FaceNet中的策略,我们不能将其称为OHEM,不能称为严格意义上的难例挖掘,但有其核心思想在里面。如果要想使我们的模型训练的更好,此处可以对样本选择的部分进行优化。

def train(args, sess, dataset, epoch, image_paths_placeholder, labels_placeholder, labels_batch,
          batch_size_placeholder, learning_rate_placeholder, phase_train_placeholder, enqueue_op, input_queue, global_step, 
          embeddings, loss, train_op, summary_op, summary_writer, learning_rate_schedule_file,
          embedding_size, anchor, positive, negative, triplet_loss):
    batch_number = 0
    # 获取学习率
    if args.learning_rate > 0.0:
        lr = args.learning_rate
    else:
        lr = facenet.get_learning_rate_from_file(learning_rate_schedule_file, epoch)
    # 这里的epoch_size对应了batch_size的数据
    # 比如说总样本量为100000,每个batch_size是100,那么epoch_size
    # 为100000 / 100 = 1000
    while batch_number < args.epoch_size:
        # 从数据源随机获取样本量
        # 在进行训练的时候每次取出的样本量由args.people_per_batch和args.images_per_person来定义
        # args.people_per_batch表示在当前的batch_size中一共有多少个人
        # args.images_per_person表示每个人有多少张图片
        image_paths, num_per_class = sample_people(dataset, args.people_per_batch, args.images_per_person)
        
        print('Running forward pass on sampled images: ', end='')
        start_time = time.time()
        # 获取整体的样本图片数量
        nrof_examples = args.people_per_batch * args.images_per_person
        labels_array = np.reshape(np.arange(nrof_examples), (-1, 3))
        image_paths_array = np.reshape(np.expand_dims(np.array(image_paths), 1), (-1, 3))
        sess.run(enqueue_op, {image_paths_placeholder: image_paths_array, labels_placeholder: labels_array})
        emb_array = np.zeros((nrof_examples, embedding_size))
        # 获取每一批次的图片数量
        nrof_batches = int(np.ceil(nrof_examples / args.batch_size))
        # 对每一批次的图像来进行数据的提取和特征的提取,获取这一批次的特征向量和标签
        for i in range(nrof_batches):
            batch_size = min(nrof_examples-i*args.batch_size, args.batch_size)
            emb, lab = sess.run([embeddings, labels_batch], feed_dict={batch_size_placeholder: batch_size, 
                learning_rate_placeholder: lr, phase_train_placeholder: True})
            emb_array[lab, :] = emb
        print('%.3f' % (time.time()-start_time))

        # 拿到这一批次的特征向量和标签之后并不是马上进行损失函数的计算,而是通过select_triplets
        # 来进行选择,拿到真实计算loss所对应的目标
        print('Selecting suitable triplets for training')
        triplets, nrof_random_negs, nrof_triplets = select_triplets(emb_array, num_per_class, 
            image_paths, args.people_per_batch, args.alpha)
        selection_time = time.time() - start_time
        print('(nrof_random_negs, nrof_triplets) = (%d, %d): time=%.3f seconds' % 
            (nrof_random_negs, nrof_triplets, selection_time))

        # 对选择出来的目标重新计算每批次的图片数量
        nrof_batches = int(np.ceil(nrof_triplets*3/args.batch_size))
        triplet_paths = list(itertools.chain(*triplets))
        labels_array = np.reshape(np.arange(len(triplet_paths)),(-1,3))
        triplet_paths_array = np.reshape(np.expand_dims(np.array(triplet_paths), 1), (-1, 3))
        sess.run(enqueue_op, {image_paths_placeholder: triplet_paths_array, labels_placeholder: labels_array})
        nrof_examples = len(triplet_paths)
        train_time = 0
        i = 0
        emb_array = np.zeros((nrof_examples, embedding_size))
        loss_array = np.zeros((nrof_triplets,))
        summary = tf.Summary()
        step = 0
        # 经过循环去提取数据
        while i < nrof_batches:
            start_time = time.time()
            batch_size = min(nrof_examples-i*args.batch_size, args.batch_size)
            feed_dict = {batch_size_placeholder: batch_size, learning_rate_placeholder: lr, phase_train_placeholder: True}
            # 拿到数据之后再进行loss的计算,这个loss就是输出结果中的每一个batch_size中的loss
            err, _, step, emb, lab = sess.run([loss, train_op, global_step, embeddings, labels_batch], feed_dict=feed_dict)
            emb_array[lab,:] = emb
            loss_array[i] = err
            duration = time.time() - start_time
            print('Epoch: [%d][%d/%d]\tTime %.3f\tLoss %2.3f' %
                  (epoch, batch_number+1, args.epoch_size, duration, err))
            batch_number += 1
            i += 1
            train_time += duration
            summary.value.add(tag='loss', simple_value=err)
            
        # Add validation loss and accuracy to summary
        #pylint: disable=maybe-no-member
        summary.value.add(tag='time/selection', simple_value=selection_time)
        summary_writer.add_summary(summary, step)
    return step

所以这里的loss并不是在参数中定义的batch_size的数量的loss,而是定义的有多少个人以及每个人有多少个图片这样一个数量,在这个基础上再进行样本筛选之后的样本所对应的loss。我们来看一下样本筛选select_triplets的实现。

def select_triplets(embeddings, nrof_images_per_class, image_paths, people_per_batch, alpha):
    """ Select the triplets for training
    """
    trip_idx = 0
    emb_start_idx = 0
    num_trips = 0
    triplets = []
    
    # VGG Face: Choosing good triplets is crucial and should strike a balance between
    #  selecting informative (i.e. challenging) examples and swamping training with examples that
    #  are too hard. This is achieve by extending each pair (a, p) to a triplet (a, p, n) by sampling
    #  the image n at random, but only between the ones that violate the triplet loss margin. The
    #  latter is a form of hard-negative mining, but it is not as aggressive (and much cheaper) than
    #  choosing the maximally violating example, as often done in structured output learning.
    # 遍历有多少个人
    for i in xrange(people_per_batch):
        nrof_images = int(nrof_images_per_class[i])
        # 遍历每一个人所对应的图片
        for j in xrange(1,nrof_images):
            a_idx = emb_start_idx + j - 1
            neg_dists_sqr = np.sum(np.square(embeddings[a_idx] - embeddings), 1)
            # 构造样本对
            for pair in xrange(j, nrof_images): # For every possible positive pair.
                p_idx = emb_start_idx + pair
                # 对于同一个人的不同图片构造正样本对
                pos_dist_sqr = np.sum(np.square(embeddings[a_idx]-embeddings[p_idx]))
                # 对于不同人的不同图片构造负样本对
                neg_dists_sqr[emb_start_idx:emb_start_idx+nrof_images] = np.NaN
                #all_neg = np.where(np.logical_and(neg_dists_sqr-pos_dist_sqr<alpha, pos_dist_sqr<neg_dists_sqr))[0]  # FaceNet selection
                # 对正负样本对进行筛选,筛选的标准是负样本对与正样本对的差小于alpha
                # 这里就是满足了triplet loss的约束,因为负样本的数量是远远大于正样本的,所以要挑选出满足要求
                # 的负样本,如果负样本已经达到了loss约束的情况,此时这些样本就是一些已经分类正确的样本,对于这些
                # 样本就舍弃掉,我们需要挑选那些不满足triplet loss约束的样本,所以这里是一个<,这个公式刚好和loss
                # 计算的公式是相反的
                all_neg = np.where(neg_dists_sqr-pos_dist_sqr < alpha)[0] # VGG Face selecction
                # 获取最终计算loss的样本序列
                nrof_random_negs = all_neg.shape[0]
                if nrof_random_negs > 0:
                    rnd_idx = np.random.randint(nrof_random_negs)
                    n_idx = all_neg[rnd_idx]
                    triplets.append((image_paths[a_idx], image_paths[p_idx], image_paths[n_idx]))
                    #print('Triplet %d: (%d, %d, %d), pos_dist=%2.6f, neg_dist=%2.6f (%d, %d, %d, %d, %d)' % 
                    #    (trip_idx, a_idx, p_idx, n_idx, pos_dist_sqr, neg_dists_sqr[n_idx], nrof_random_negs, rnd_idx, i, j, emb_start_idx))
                    trip_idx += 1

                num_trips += 1

        emb_start_idx += nrof_images
    # 再对该序列进行乱序
    np.random.shuffle(triplets)
    return triplets, num_trips, len(triplets)

这里对负样本的筛选还是过于简单粗暴,如果想对网络进行优化的话,可以尝试在以下的地方修改对负样本的选择方式

# 构造样本对
for pair in xrange(j, nrof_images): # For every possible positive pair.
    p_idx = emb_start_idx + pair
    # 对于同一个人的不同图片构造正样本对
    pos_dist_sqr = np.sum(np.square(embeddings[a_idx]-embeddings[p_idx]))
    # 对于不同人的不同图片构造负样本对
    neg_dists_sqr[emb_start_idx:emb_start_idx+nrof_images] = np.NaN
    #all_neg = np.where(np.logical_and(neg_dists_sqr-pos_dist_sqr<alpha, pos_dist_sqr<neg_dists_sqr))[0]  # FaceNet selection
    # 对正负样本对进行筛选,筛选的标准是负样本对与正样本对的差小于alpha
    # 这里就是满足了triplet loss的约束,因为负样本的数量是远远大于正样本的,所以要挑选出满足要求
    # 的负样本,如果负样本已经达到了loss约束的情况,此时这些样本就是一些已经分类正确的样本,对于这些
    # 样本就舍弃掉,我们需要挑选那些不满足triplet loss约束的样本,所以这里是一个<,这个公式刚好和loss
    # 计算的公式是相反的
    all_neg = np.where(neg_dists_sqr-pos_dist_sqr < alpha)[0] # VGG Face selecction

这里并没有对正样本进行处理,而是把所有的正样本都挑选了出来。这里也可以对正样本完成筛选来完成对网络的优化。总结来看对于整个FaceNet,我们可以优化的点有三个地方,一个就是在负样本和正样本对筛选的时候,如何去做更好的筛选策略;然后就是数据增强的部分,如何去添加更加丰富的数据增强的策略来保证模型的鲁棒性会更好;再有就是主干网络的定义,主干网络的定义只要去采用一些现成的网络结构,比如SENet、DenseNet等等这些卷积神经网络结构来提取更加好的网络特征,从这个角度来提高模型的鲁棒性。

FaceNet模型测试

在facenet/src目录下有一个validate_on_lfw.py脚本文件可以帮助我们对模型进行测试。我们来看看它里面的代码,这里同样需要将

import tensorflow as tf

修改成

import tensorflow.compat.v1 as tf

从main()方法开始

def main(args):
  
    with tf.Graph().as_default():
      
        with tf.Session() as sess:
            
            # 读取测试样本队
            pairs = lfw.read_pairs(os.path.expanduser(args.lfw_pairs))

            # 获取测试图片路径
            paths, actual_issame = lfw.get_paths(os.path.expanduser(args.lfw_dir), pairs)
            # 图片路径
            image_paths_placeholder = tf.placeholder(tf.string, shape=(None, 1), name='image_paths')
            # 图片标签,标签是指图片是相同样本还是不同样本
            labels_placeholder = tf.placeholder(tf.int32, shape=(None, 1), name='labels')
            batch_size_placeholder = tf.placeholder(tf.int32, name='batch_size')
            control_placeholder = tf.placeholder(tf.int32, shape=(None, 1), name='control')
            phase_train_placeholder = tf.placeholder(tf.bool, name='phase_train')
 
            nrof_preprocess_threads = 4
            image_size = (args.image_size, args.image_size)
            # 创建先进先出数据队列,通过数据队列完成对数据的读取
            eval_input_queue = data_flow_ops.FIFOQueue(capacity=2000000,
                                        dtypes=[tf.string, tf.int32, tf.int32],
                                        shapes=[(1,), (1,), (1,)],
                                        shared_name=None, name=None)
            eval_enqueue_op = eval_input_queue.enqueue_many([image_paths_placeholder, labels_placeholder, control_placeholder], name='eval_enqueue_op')
            image_batch, label_batch = facenet.create_input_pipeline(eval_input_queue, image_size, nrof_preprocess_threads, batch_size_placeholder)
     
            # 加载模型
            input_map = {'image_batch': image_batch, 'label_batch': label_batch, 'phase_train': phase_train_placeholder}
            facenet.load_model(args.model, input_map=input_map)

            # 通过前向计算来获取特征矩阵
            embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0")
#              
            coord = tf.train.Coordinator()
            tf.train.start_queue_runners(coord=coord, sess=sess)
            # 检测度量,度量函数就是args.distance_metric,此时就可以对特征矩阵进行相似度衡量
            # 同类样本之间的相似度和不同类样本之间的相似度,判断样本的正负性,设定一个阈值,根据
            # 不同的阈值来判断当前的样本对预测的结果是否是同一个样本,如果这两个样本是同一个样本,
            # 即这两个样本之间的相似度满足一个阈值,此时我们认为这是同一个样本。如果两个样本取自同
            # 一个人的不同人脸,且相似度满足阈值,此时我们认为预测正确,否则就是预测错误。对于不同
            # 人的人脸,相似度超出了阈值,此时我们认为这不是同一个人脸。如果这两个样本确实来自不同
            # 的人,我们就能够通过模型正确的预测出这两个类别不是同一类。此时我们也认为预测正确
            evaluate(sess, eval_enqueue_op, image_paths_placeholder, labels_placeholder, phase_train_placeholder, batch_size_placeholder, control_placeholder,
                embeddings, label_batch, paths, actual_issame, args.lfw_batch_size, args.lfw_nrof_folds, args.distance_metric, args.subtract_mean,
                args.use_flipped_images, args.use_fixed_image_standardization)

我们再来看一下之前训练的模型

cd /Users/admin/models/facenet/20211211-082127/
ls

可以看到

checkpoint
model-20211211-082127.ckpt-1007.data-00000-of-00001
model-20211211-082127.ckpt-1007.index
model-20211211-082127.ckpt-2028.data-00000-of-00001
model-20211211-082127.ckpt-2028.index
model-20211211-082127.ckpt-3040.data-00000-of-00001
model-20211211-082127.ckpt-3040.index
model-20211211-082127.meta

现在我们来开始进行测试,进入facenet文件夹,运行

python src/validate_on_lfw.py /Users/admin/Downloads/lfw_160 /Users/admin/models/facenet/20211211-082127

这里第一个参数是图片路径,第二个参数是模型路径,测试日志

Runnning forward pass on LFW images
............
Accuracy: 0.85600+-0.01982
Validation rate: 0.15433+-0.03649 @ FAR=0.00100
Area Under Curve (AUC): 0.933
Equal Error Rate (EER): 0.147
2021-12-13 08:15:01.242145: W tensorflow/core/kernels/queue_base.cc:285] _0_FIFOQueueV2: Skipping cancelled dequeue attempt with queue not closed
2021-12-13 08:15:01.242201: W tensorflow/core/kernels/queue_base.cc:285] _0_FIFOQueueV2: Skipping cancelled dequeue attempt with queue not closed
2021-12-13 08:15:01.242231: W tensorflow/core/kernels/queue_base.cc:285] _0_FIFOQueueV2: Skipping cancelled dequeue attempt with queue not closed
2021-12-13 08:15:01.242257: W tensorflow/core/kernels/queue_base.cc:285] _0_FIFOQueueV2: Skipping cancelled dequeue attempt with queue not closed
2021-12-13 08:15:01.242288: W tensorflow/core/kernels/queue_base.cc:285] _0_FIFOQueueV2: Skipping cancelled dequeue attempt with queue not closed
2021-12-13 08:15:01.242315: W tensorflow/core/kernels/queue_base.cc:285] _0_FIFOQueueV2: Skipping cancelled dequeue attempt with queue not closed
2021-12-13 08:15:01.242344: W tensorflow/core/kernels/queue_base.cc:285] _0_FIFOQueueV2: Skipping cancelled dequeue attempt with queue not closed
2021-12-13 08:15:01.242375: W tensorflow/core/kernels/queue_base.cc:285] _0_FIFOQueueV2: Skipping cancelled dequeue attempt with queue not closed

由于我这里是使用相同的数据训练和测试,实际上这是不应该的,不过训练比较耗时,就暂且不计较这些,我们可以看到测试得到的Accuracy是0.85,AUC是0.93。当然如果是不同的图像数据集分开训练和测试的话,它的模型精度不会有这么高,通常有一个专门研究跨域学习的领域叫做openset domain transfer learning,可以提升此类问题的模型精度。

训练模型转pb文件,模型固化

在facenet/src目录下有一个freeze_graph.py的脚本文件,这里同样需要将

import tensorflow as tf

修改成

import tensorflow.compat.v1 as tf

我们同样来看一下main()方法

def main(args):
    with tf.Graph().as_default():
        with tf.Session() as sess:
            # Load the model metagraph and checkpoint
            print('Model directory: %s' % args.model_dir)
            # 获取训练好的模型文件
            meta_file, ckpt_file = facenet.get_model_filenames(os.path.expanduser(args.model_dir))
            
            print('Metagraph file: %s' % meta_file)
            print('Checkpoint file: %s' % ckpt_file)

            model_dir_exp = os.path.expanduser(args.model_dir)
            saver = tf.train.import_meta_graph(os.path.join(model_dir_exp, meta_file), clear_devices=True)
            tf.get_default_session().run(tf.global_variables_initializer())
            tf.get_default_session().run(tf.local_variables_initializer())
            # 恢复模型到当前会话
            saver.restore(tf.get_default_session(), os.path.join(model_dir_exp, ckpt_file))
            
            # Retrieve the protobuf graph definition and fix the batch norm nodes
            input_graph_def = sess.graph.as_graph_def()
            
            # 对当前会话的图模型进行保存
            output_graph_def = freeze_graph_def(sess, input_graph_def, 'embeddings,label_batch')

        # Serialize and dump the output graph to the filesystem
        with tf.gfile.GFile(args.output_file, 'wb') as f:
            f.write(output_graph_def.SerializeToString())
        print("%d ops in the final graph: %s" % (len(output_graph_def.node), args.output_file))

在freeze_graph_def方法中主要就是下面的代码来进行保存模型

# 获取需要保存的节点列表
whitelist_names = []
for node in input_graph_def.node:
    if (node.name.startswith('InceptionResnet') or node.name.startswith('embeddings') or 
            node.name.startswith('image_batch') or node.name.startswith('label_batch') or
            node.name.startswith('phase_train') or node.name.startswith('Logits')):
        whitelist_names.append(node.name)

# 将变量转化成常量,就是固化的过程
output_graph_def = graph_util.convert_variables_to_constants(
    sess, input_graph_def, output_node_names.split(","),
    variable_names_whitelist=whitelist_names)

现在我们同样进入facenet文件夹,执行

python src/freeze_graph.py /Users/admin/models/facenet/20211211-082127 /Users/admin/models/facenet/20211211-082127/graph.pb

此时我们进入/Users/admin/models/facenet/20211211-082127文件夹下,可以看到graph.pb这个固化的模型。

计算两张图片的相似度

在facenet/src目录下有一个compare.py的脚本文件,它是专门用来对两种图片相似度进行比较的。这里同样需要将

import tensorflow as tf

修改成

import tensorflow.compat.v1 as tf

from scipy import misc

替换成

import imageio
from PIL import Image

修改123行

parser.add_argument('--margin', type=int,
    help='Margin for the crop around the bounding box (height, width) in pixels.', default=32)

修改97行

img = imageio.imread(os.path.expanduser(image))

修改111行

im = Image.fromarray(cropped)
size = (image_size, image_size)
aligned = np.array(im.resize(size, Image.BILINEAR))

现在我们来看一下main方法,首先

# 导入图像,该图像是带背景图的图像,这里会使用mtcnn来完成人脸检测和人脸定位
images = load_and_align_data(args.image_files, args.image_size, args.margin, args.gpu_memory_fraction)

这里我们可以看一下load_and_align_data方法

def load_and_align_data(image_paths, image_size, margin, gpu_memory_fraction):

    minsize = 20  # minimum size of face
    threshold = [0.6, 0.7, 0.7]  # three steps's threshold
    factor = 0.709  # scale factor
    
    print('Creating networks and loading parameters')
    with tf.Graph().as_default():
        gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=gpu_memory_fraction)
        sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options, log_device_placement=False))
        with sess.as_default():
            # 使用mtcnn来进行人脸检测,使用pnet、rnet、onet集联网络来完成人脸定位
            pnet, rnet, onet = align.detect_face.create_mtcnn(sess, None)
  
    tmp_image_paths = copy.copy(image_paths)
    img_list = []
    for image in tmp_image_paths:
        img = imageio.imread(os.path.expanduser(image), mode='RGB')
        img_size = np.asarray(img.shape)[0:2]
        # 获取人脸框区域
        bounding_boxes, _ = align.detect_face.detect_face(img, minsize, pnet, rnet, onet, threshold, factor)
        if len(bounding_boxes) < 1:
          image_paths.remove(image)
          print("can't detect face, remove ", image)
          continue
        det = np.squeeze(bounding_boxes[0, 0:4])
        bb = np.zeros(4, dtype=np.int32)
        # 将检测出来的人脸框进行扩充
        bb[0] = np.maximum(det[0]-margin/2, 0)
        bb[1] = np.maximum(det[1]-margin/2, 0)
        bb[2] = np.minimum(det[2]+margin/2, img_size[1])
        bb[3] = np.minimum(det[3]+margin/2, img_size[0])
        # 对人脸框进行裁剪
        cropped = img[bb[1]:bb[3], bb[0]:bb[2], :]
        im = Image.fromarray(cropped)
        size = (image_size, image_size)
        # resize到前向运算所用到的尺寸
        aligned = np.array(im.resize(size, Image.BILINEAR))
        # aligned = misc.imresize(cropped, (image_size, image_size), interp='bilinear')
        # 对数据进行预处理
        prewhitened = facenet.prewhiten(aligned)
        img_list.append(prewhitened)
    images = np.stack(img_list)
    return images

这里有一个预处理的过程prewhitened = facenet.prewhiten(aligned),我们来看一下prewhiten方法,在facenet.py文件中

def prewhiten(x):
    '''
    图片标准化
    '''
    # 均值
    mean = np.mean(x)
    # 方差
    std = np.std(x)
    std_adj = np.maximum(std, 1.0/np.sqrt(x.size))
    y = np.multiply(np.subtract(x, mean), 1/std_adj)
    return y  

现在我们回到main方法中

with tf.Graph().as_default():

    with tf.Session() as sess:
  
        # 加载模型
        facenet.load_model(args.model)

        # 获取输入的张量
        images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0")
        embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0")
        phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0")

        # 通过前向运算来获取特征向量
        feed_dict = {images_placeholder: images, phase_train_placeholder: False}
        emb = sess.run(embeddings, feed_dict=feed_dict)
        
        nrof_images = len(args.image_files)

        print('Images:')
        for i in range(nrof_images):
            print('%1d: %s' % (i, args.image_files[i]))
        print('')
        
        # Print distance matrix
        print('Distance matrix')
        print('    ', end='')
        for i in range(nrof_images):
            print('    %1d     ' % i, end='')
        print('')
        for i in range(nrof_images):
            print('%1d  ' % i, end='')
            for j in range(nrof_images):
                # 计算两个特征向量之间的欧式距离
                dist = np.sqrt(np.sum(np.square(np.subtract(emb[i, :], emb[j, :]))))
                print('  %1.4f  ' % dist, end='')
            print('')

现在我们来比较一下两张图片的相似度,我就使用自己的两张照片

在facenet目录下执行

python src/compare.py /Users/admin/models/facenet/20211211-082127/graph.pb /Users/admin/Documents/1234.jpeg /Users/admin/Documents/2123.png

这里第一个参数是我们之前训练的模型,后面两个参数分别是两个图片的地址

运行结果

Distance matrix
        0         1     
0    0.0000    0.6887  
1    0.6887    0.0000

可以看到这两张图片的相似度为0.68,其实这里指的是欧式距离,距离越小说明相似度越高,现在我们换成不同的人的图片。

在facenet目录下执行

python src/compare.py /Users/admin/models/facenet/20211211-082127/graph.pb /Users/admin/Documents/1314.jpeg /Users/admin/Documents/2123.png 

运行结果

Distance matrix
        0         1     
0    0.0000    1.2073  
1    1.2073    0.0000

这里我们可以看到相似度为1.2073,由于是不同的人脸,说明距离增大了,相似度降低。

阈值判定——PR曲线

这里我们会使用PR曲线来判断阈值到底选择多少,PR曲线又称为精准率-召回率曲线,有关详细内容可以参考机器学习算法整理(三) ,在我们进行人脸匹配的时候,精准率Precision一定要高,如果将PR曲线绘制出来,如果模型收敛的比较好的话,在保证精准率的前提下,召回率Recall的值也会有一个比较好的保证。我们在选择的时候在确保精准率的情况下,去找到一个召回率足够大的一个值来作为我们的阈值。在上图中,如果要保证精准率在0.9,此时召回率大概在0.5几,也就说每十张图片有可能会有一张出现错误。通常在人脸匹配的时候的精准率在0.99+,也就是说每隔1000张图,会错几张。假设召回率是0.5的话,就意味着大概两张图片就会有一张真实图片。如果精准率很高的情况下,召回率可能会比较低,就意味着需要采集多张图片来进行确认。关于阈值的确定,就是使用PR曲线来进行确定,保证精准率,一定是优先考虑精准率,在精准率精度足够的情况下,尽可能选择召回率比较高的,这时候人脸匹配的效率就会比较高。

68点人脸关键点定位

人脸对齐基本概念

  • 什么是人脸对齐/人脸关键点?

根据输入的人脸图像,自动定位出面部关键特征点,如眼睛、鼻尖、嘴角点、眉毛以及人脸各部件轮廓点等。

如上图中,我们会去勾勒出人脸的形状,形状是在解决计算机视觉任务中,一个非常重要的概念。比如说当前的形状是一个圆形,一个矩形,一个正方形,一个三角形等,这样一些可以定义的形状,此时就可以通过形状的定义来进行表示,例如圆形可以通过中心点和半径来进行表示,对于矩形可以通过长宽、左上角、右下角、中心点这些点位来进行表示。当我们想要去表示一个轮廓信息的时候,此时会通过点的集合来进行表示。通过点的集合来勾勒出形状信息。人脸关键点实际上就是对人脸形状的完美刻画。对于人脸形状到底用多少个点去进行刻画呢?

其实想要刻画一个人脸有非常多的表示方式,如2D人脸、3D人脸,5点刻画(嘴巴、嘴角、鼻尖、眼睛、瞳孔),21点、29点、68点、96点、192点、1000点、8000点……

上图中右图就是一个8000点的稠密人脸关键点的位置,左图中是1000点的人脸关键点的位置。可以看出8000点的人脸关键点可以描述出一个3D曲面的信息。实际上对于人脸对齐可以分为2D人脸和3D人脸。上面的从5点到1000点都是2D人脸定位的内容,而8000点属于3D人脸。对于2D人脸主要是定位出当前人脸中一些关键点位的x、y座标信息。而3D人脸需要考虑到的就是深度学习,通过人脸关键点定位,能够找到在人脸中一些非常重要的五官、轮廓信息,利用这些五官和轮廓信息就可以去完成后续的功能,比如说表情识别、人脸编辑、人脸美妆。有了非常稠密的人脸关键点之后就可以对人脸进行3维重建。人脸本身是一个立体图形,在使用相机拍摄的时候,会将立体图变成平面图。如果有了3D的MARK就可以利用这张图重构出3D的人脸模型。

人脸对齐算法评价指标

点的集合(向量),人脸关键点算法通常会将输出结果表示成点的集合,而这个集合很难在模型中进行运算,所以会进一步将其表示成向量。比如说当前关键点为5个关键点,此时会表示成10维的向量(x1,y1,x2,y2,x3,y3,x4,y4,x5,y5)。对于这样一个十维的向量,我们就可以在后续进行回归loss的计算。对于人脸关键点预测的结果之后,对其好坏的评价方法有:

  • NME: Normalized mean error,标准化的平均误差
  1. 两外眼角间距离
  2. 人脸外接矩形对角线长度

NME计算了当前预测的点的集合与真实值之间的标准的偏差。

上面的公式中表示预测的点,表示基准点。表示点和点之间的欧式距离,N表达了归一化系数,也就是说对N个点取平均值。表达了标准化的方法,为了去掉因为脸的大小不同,所带来的影响。这里会通过人脸框的信息来对点的误差来进行标准化。通常会采用两个外眼角之间的距离或者是人脸的外接矩形对角线的长度来作为的值。通过该公式可以得到一个偏差值e,度量了预测出来的点同真实值之间的误差,根据这个误差就能评价当前算法的好坏。

  • CED: Cumulative Errors Distribution (CED) curve,根据NME得到的结果

CED实际上是结合NME绘制出来的这样一个曲线,横座标表达了当前的偏差值,纵座标表达了满足当前偏差值的图片的数量。CED曲线是一个上升的曲线,伴随着偏差值不断的提高,满足当前偏差的图片的数量也会增多。最终就可以绘制出一个CED曲线。

人脸对齐传统方法

对于人脸关键点的方法做一个大致的分类,可分为传统方法、深度学习方法。传统的方法有:

  • 基于形状学习的模型:ASM、AAM

对于形状学习的人脸关键点,它主要是去勾勒出基本的轮廓,而这个轮廓我们就可以认为属于一种特定的形状,根据形状就可以去构造出学习的方法。首先会对人脸的特征点进行建模,人脸模型构造首先会选择基准的图片,利用这个基准的图片来作为一个参照。有了参照之后,其他在训练集中的人脸图片会进行相应的变换,将训练集中的图片按照基准的形状来进行变换,这个变换的过程包括了尺度上的放缩、旋转等操作。经过简单的操作会拿到训练集中所有的人脸图像处理过之后的集合,这些集合中的人脸图像都是同基准的图像大体上是相似的。根据在训练集中的人脸关键点标定的位置,对每一个点来进行特征提取。以当前的关键点来作为中心,提取局部图像区域,然后对局部图像区域来进行特征提取。此时就能够利用点的邻域信息对当前的点来进行表示。具体可以采用颜色、纹理等等特征工程来提取出对于当前的一些关键点能够更好的进行鲁棒特征表示的特征。经过特征提取之后,假如当前人脸图像有5个点,此时就可以拿到5个向量,这5个向量就是从训练集中学到的每一个人脸关键点所对应的特征向量,一个特征表示。

有了对于点的特征描述之后,给定一个测试集中的图片,拿到这些图片之后首先会进行关键点的搜索,在初步的搜索之后,会选择这5个点中的某几个点来作为初始搜索的对象。比如说对于5个点的人脸关键点定位的问题,通常会选择眼睛或者是眼睛和鼻子这几个点来作为最开始点搜索的目标。我们对于每一个点都有了特征向量的描述,对于输入的测试集中的图片,同样也可以对其每一个点都进行特征表示。此时在测试集中人脸中的每一个点都会有一个特征向量。而在训练之后每一个点都会有标准的特征向量,利用这两个特征向量去计算相似度就能够去找到一个最佳匹配的点,这个时候就认为这些最佳匹配的点就是我们想要去定位到的人脸的关键点。

最开始搜索的时候会只考虑其中的几个点,这几个点的位置确定好了之后,此时就需要对人脸进行对齐。因为在训练的时候有一个人脸基准的形状,在拿到了人脸关键点的某几个点之后,就可以利用这几个点来对测试集中的人脸进行简单的调整,这个调整就是使得当前的人脸同标准人脸的形状保持尽可能的相似。这个调整的过程同样包含了旋转、放缩等一系列的操作。这个过程被称为人脸对齐的过程。对于人脸对齐和人脸关键点其实属于一类方法。对于人脸关键点有时候也会称为人脸对齐或者人脸对齐也称为人脸关键点,主要是人脸对齐是依赖于点的,拿到人脸关键点之后就能够对人脸的形状进行调整,通过仿射变换就能够完成这个操作。我们想要解决人脸对齐主要就是解决了人脸关键点就能够完成人脸对齐的工作。在传统的人脸关键点定位方法中,在进行形状学习的时候,其中有一个重要的步骤也是人脸对齐,包括在进行关键点特征提取,也就是在模型训练的时候,也提到了同基准模型进行对齐的过程。

在对初识的几个点对齐之后再对其他的点进行定位。其他的点进行定位的时候,因为已经进行了对齐,所以就可以去以基准的人脸来作为参照,根据基准人脸就能估算出在测试集的人脸中,想要去定位的其他的点的大致的位置。有了大致位置之后,在邻域周边去寻找想要拿到的最终的关键点的位置。此时就能通过形状学习来完成人脸关键点座标的定位。

  • 基于级联回归学习的模型:CPR

级联回归模型算法的思想和形状学习算法的思想存在很大的区别。对于形状学习对于人脸关键点定位会用到人脸对齐,会根据估算出来的点对人脸进行调整,最终从基准人脸形状上保持一致。而级联回归模型的重点在回归的部分,属于一个迭代的算法,在这个迭代算法中包括了几个流程,首先初始化人脸关键点的座标,利用平均人脸就可以初始化出最开始的图像中人脸关键点的座标。平均人脸就是对训练集中的人脸关键点的座标求平均值。拿到的平均值就是初始化的一个值Si。然后对当前的人脸图像进行特征提取,同样会用到特征工程提取颜色特征、纹理特征、高层的语义特征等等。利用提取出来的特征来回归出最终的人脸关键点的位置同计算出来的人脸关键点位置的偏差∆S。然后得到新的人脸关键点位置Si+1=Si+∆S,此为下一次迭代时候的位置信息。Si+1会更接近我们想要的真实的人脸关键点的位置。拿到Si+1之后会将初始化的座标值进行修改。有了这个更新的过程,再重新计算特征,再次计算偏差,直到∆S不再发生变化。通过这样不停的迭代,最终计算出人脸关键点的座标。

在这里比较重要的点Si如何表示,提取什么样的特征,如何利用这个特征来得到∆S。这是级联回归模型中涉及到的三个重要的概念。Si的表示依然表示成一个向量,将所有座标的点进行拼接。特征会采用一些传统计算机视觉中的表示方法,如LBP,HOG等人脸特征表示的方法。∆S的计算实际上是通过一个回归函数来进行计算,回归函数的输入就是提取出来的特征。

人脸对齐算法深度学习算法

人脸对齐的深度学习算法分为多级回归和多任务两个类别,多级回归最早使用在DCNN算法中,DCNN使用粗到细,用不同的CNN模型来预测关键点中的部分或者是全部的关键点,并且在这个基础上进行平均得到最终预测的结果。DCNN最大的贡献点就是提出了从粗粒度到细粒度的这样的一个级联的关键点定位算法的思想。先粗略的估计一下关键点的位置,然后再根据粗略的估计出的关键点的位置进行图片裁剪,裁剪过之后再进一步对于裁剪过之后的图片再进行精细的关键点位置的预测。通过这样一个级联的过程,我们能够对人脸关键点位置进行进一步的修订。DCNN后续也被DCNN-Face团队进行了进一步优化,发表在了2013年的ICCV图像识别的会议上。计算机视觉的三大顶会——CVPR、ECCV和ICCV。

除去这种级联的回归网络以外,再就是多任务的网络结构,其中比较具有代表性的就是TCDCN和MTCNN。这一类算法的核心思想就是在预测关键点的时候,不仅去仅仅预测关键点,同时会预测一些其他相关的任务。比如在MTCNN中会通过三个级联的网络同时完成人脸检测和关键点定位。而在TCDCN中则是完成一些人脸属性和人脸关键点问题这样几个不同的任务。通过多任务网络,我们能够利用几个不同任务之间的关联来提高最终模型的准确性,同时利用同一个网络解决多个任务,这样也有利于最终算法执行的效率对资源的消耗。

除去级联回归和多任务网络之外,再有就是采用直接回归的模型,其中比较具有代表性的就是Vanlilla CNN,它也是对输入的图像进行特征提取,在进行特征提取之后会对网络的特征进行分析,进行分析之后会发现提取出来的特征包含几个类别(类簇),网络结构具有一定的层次关系。经过卷积提取出来的特征会进行分类,将不同的特征划分到不同的分支上去。每一个不同的特征再去分别预测不同的关键点。也就是说针对不同的姿势训练出了不同的回归模型。在解决关键点定位问题时,把任务做的更加细粒度。我们使用的SENet就是使用Vanilla Net来实现关键点定位的任务。

再有一种就是热力图的方式,其中具有代表性的网络就是DAN。热力图就是对每一个关键点去做一个变换,对于给定的一副图像以及关键点座标,如果想要生成一个热力图,一个比较简单的做法就是图像中的关键点的位置定义为1,其他的位置定义成0。但是如果只是5个点的热力图,0的数量会比1的数量多很多,那么1几本上就会被湮没在热力图中,所以在DAN中将每个点都变成了一个区域。假设这个区域的大小为m*n,关键点的位置就是区域的中心点。中心点定义成1,边缘定义成0,0和1之间定义成均匀分布或者正态分布。反映在热力图中就是中心点的位置就是图中最亮的位置。0的位置是图像中最暗的位置。而区域则是一个从亮到暗渐变的过程。利用这个热力图,就可以采用分割编解码的网络去完成关键点的定位。其实就是去回归这个热力图。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章