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的位置是圖像中最暗的位置。而區域則是一個從亮到暗漸變的過程。利用這個熱力圖,就可以採用分割編解碼的網絡去完成關鍵點的定位。其實就是去迴歸這個熱力圖。

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