Face recognition với keras, dlib và OpenCV

cnn
python
deep-learning
tutorial

#1

Nhân dịp cuộc thi số 2 của Aivivn về nhận diện khuôn mặt người nổi tiếng, mình xin chia sẻ một phương pháp nhận diện khuôn mặt sử dụng mạng CNN. Ý tưởng chính mình tham khảo từ chia sẻ baseline cho cuộc thi Aivivn của bác Khôi Tuấn Nguyễn. Còn phần mạng CNN mình sử dụng là mạng nn4.small2 từ project Keras-OpenFace.

Github project: https://github.com/habom2310/face-recognition-with-keras-and-dlib

Project có thể chạy trên CPU

Các bước thực hiện

  • Clone project từ github
  • Cài thư viện: Opencv 3.4.5, keras 2.2.4, tqdm 4.31.1, pandas 0.23.4, scipy 1.2.0
  • Dlib 19.4.0 (wheel file cho windows)
  • Download shape_predictor_68_face_landmarks.dat tại đây và để vào trong folder chứa project.
  • Chạy python face_detect_and_save.py
  • Chạy python main.py

Cấu trúc CNN

Phần cuối của mạng là một lớp Fully Connected 128 phần tử và một lớp L2 normalization, cho phép thu được một embedding vector. Bằng việc so sánh khoảng cách Euclide của các embedding vector của các khuôn mặt, ta có thể xác định được các khuôn mặt đó giống hay khác, cụ thể, hai khuôn mặt giống nhau có khoảng cách Euclide nhỏ và hai khuôn mặt khác biệt thì có khoảng cách Euclide lớn. Cấu trúc chi tiết của mạng CNN được định nghĩa trong model.py.

OpenFace cung cấp pre-trained model được train với bộ dữ liệu FacescrubCASIA-Webface (pre-trained model nằm trong folder weights).

from model import create_model

nn4_small2 = create_model()
nn4_small2.load_weights('weights/nn4.small2.v1.h5')

Tạo dữ liệu

Ta có thể sử dụng dữ liệu của riêng mình để train. Tuy nhiên đây không phải là train mạng lại từ đầu mà chính xác hơn là trích xuất các embedding vector để phục vụ cho việc so sánh sau này. Do đó, số lượng ảnh của mỗi người không cần nhiều, khoảng 5 đến 10 ảnh một người là đã cho một kết quả khá tốt. Cấu trúc của folder dữ liệu như sau:

├───image
│   ├───adam_levine
│   │     ├───1.jpg
│   │     ├───2.jpg
│   │     ├───3.jpg
│   │     ├───4.jpg
│   │     ├───5.jpg
│   ├───adele
│   │     ├───1.jpg
│   │     ├───2.jpg
│   │     ├───3.jpg
│   │     ├───4.jpg
│   │     ├───5.jpg
│   ├───ed_sheeran
│   │     ├───1.jpg
│   │     ├───2.jpg
│   │     ├───3.jpg
│   │     ├───4.jpg
│   │     ├───5.jpg
│   ├──taylor_swift
│   │     ├───1.jpg
│   │     ├───2.jpg
│   │     ├───3.jpg
│   │     ├───4.jpg
│   │     ├───5.jpg

Ta có thể thêm bất kì ai vào trong bộ dữ liệu của mình. Lưu ý, ảnh sử dụng trong folder này chỉ có duy nhất khuôn mặt của người tương ứng. Sau đó, chạy file:

python face_detect_and_save.py

File trên có nhiệm vụ tìm khuôn mặt trong tất cả các ảnh trong folder image, sau đó cắt khuôn mặt và lưu đè lên chính file tương ứng.

Sau đó ta load dữ liệu đường dẫn, tên người để chuẩn bị cho việc train.

train_paths = glob.glob("image/*")
nb_classes = len(train_paths)
df_train = pd.DataFrame(columns=['image', 'label', 'name'])

for i,train_path in enumerate(train_paths):
    name = train_path.split("\\")[-1]
    images = glob.glob(train_path + "/*")
    for image in images:
        df_train.loc[len(df_train)]=[image,i,name]
print(df_train.head())

                                                image label          name
0                   image\adam_levine\adam-levine.jpg     0   adam_levine
1         image\adam_levine\adam-levine_editedjpg.jpg     0   adam_levine
2                       image\adam_levine\BBQakzy.jpg     0   adam_levine
3                  image\adam_levine\MI0004052827.jpg     0   adam_levine
4   image\adam_levine\rs_634x951-171107072148-634....     0   adam_levine
5         image\adele\adele-karriere-aus-abschied.jpg     1         adele
6                             image\adele\adele-t.jpg     1         adele
7                               image\adele\adele.jpg     1         adele
8                        image\adele\MI0003568106.jpg     1         adele
9   image\adele\rs_1024x759-180124143107-1024-Adel...     1         adele
10                      image\ed_sheeran\4e9fe179.jpg     2    ed_sheeran
11                       image\ed_sheeran\asdvs23.jpg     2    ed_sheeran
12                    image\ed_sheeran\ed-sheeran.jpg     2    ed_sheeran
13  image\ed_sheeran\ed-sheeran_glamour_16mar17_re...     2    ed_sheeran
14  image\ed_sheeran\GettyImages-800834188-920x584...     2    ed_sheeran
15  image\taylor_swift\0c2f93cb-4151-4c08-be2e-a85...     3  taylor_swift
16                     image\taylor_swift\416x416.jpg     3  taylor_swift
17                     image\taylor_swift\BBL3h40.jpg     3  taylor_swift
18                      image\taylor_swift\csdaf3.jpg     3  taylor_swift
19  image\taylor_swift\taylor-swift-2016-crop-1523...     3  taylor_swift

Xoay khuôn mặt

nn4.small2 được train trên tập dữ liệu các khuôn mặt thẳng, do đó các khuôn mặt cần phải được xoay thẳng trước khi sử dụng. Ta sử dụng thư viện Dlib để xác định các điểm đặc trưng trên mặt (facial landmarks) và dùng OpenCV để xoay và cắt khuôn mặt thành kích thước 96x96. Phần tìm điểm đặc trưng và xoay được định nghĩa trong align.py. Model shape_predictor_68_face_landmarks được sử dụng cho tìm đặc trưng có thể download ở đây và phải được để vào trong folder chứa project.

from align import AlignDlib
alignment = AlignDlib('shape_predictor_68_face_landmarks.dat')

def align_face(face):
    (h,w,c) = face.shape
    bb = dlib.rectangle(0, 0, w, h)
    return alignment.align(96, face, bb,landmarkIndices=AlignDlib.OUTER_EYES_AND_NOSE)

Train

Mục tiêu của train để tìm ra embedding vector của các khuôn mặt. Sau đó, các vector này được lưu lại dưới file train_embs.npy để sau này có thể dùng.

def load_and_align_images(filepaths):
    aligned_images = []
    for filepath in filepaths:
        #print(filepath)
        img = cv2.imread(filepath)
        aligned = align_face(img)
        aligned = (aligned / 255.).astype(np.float32)
        aligned = np.expand_dims(aligned, axis=0)
        aligned_images.append(aligned)
            
    return np.array(aligned_images)

def calc_embs(filepaths, batch_size=64):
    pd = []
    for start in tqdm(range(0, len(filepaths), batch_size)):
        aligned_images = load_and_align_images(filepaths[start:start+batch_size])
        pd.append(nn4_small2.predict_on_batch(np.squeeze(aligned_images)))
    embs = np.array(pd)

    return np.array(embs)

train_embs = calc_embs(df_train.image)
np.save("train_embs.npy", train_embs)

Phân tích

Để có thể xác định được 2 khuôn mặt là giống hay khác, ta dựa vào khoảng cách Euclide giữa 2 embedding vector của 2 khuôn mặt. Khoảng cách bé có nghĩa 2 khuôn mặt là giống nhau và ngược lại. Do đó ta phải tìm một ngưỡng phù hợp để quyết định khoảng cách như thế nào là bé và như thế nào là lớn. Ở đây ta dùng hàm distance.euclidean của thư viện scipy, hoặc ta cũng có thể tự định nghĩa một phương pháp tính khoảng cách Euclide.

Cụ thể, ta sẽ tính khoảng cách giữa các khuôn mặt giống nhau (match_distances), và giữa các khuôn mặt khác nhau (unmatch_distances), từ đó xác định khoảng ngưỡng hợp lý để phân biệt giống và khác.

label2idx = []

for i in tqdm(range(len(train_paths))):
    label2idx.append(np.asarray(df_train[df_train.label == i].index))

match_distances = []
for i in range(nb_classes):
    ids = label2idx[i]
    distances = []
    for j in range(len(ids) - 1):
        for k in range(j + 1, len(ids)):
            distances.append(distance.euclidean(train_embs[ids[j]].reshape(-1), train_embs[ids[k]].reshape(-1)))
    match_distances.extend(distances)
    
unmatch_distances = []
for i in range(nb_classes):
    ids = label2idx[i]
    distances = []
    for j in range(10):
        idx = np.random.randint(train_embs.shape[0])
        while idx in label2idx[i]:
            idx = np.random.randint(train_embs.shape[0])
        distances.append(distance.euclidean(train_embs[ids[np.random.randint(len(ids))]].reshape(-1), train_embs[idx].reshape(-1)))
    unmatch_distances.extend(distances)
import matplotlib.pyplot as plt

_,_,_=plt.hist(match_distances,bins=100)
_,_,_=plt.hist(unmatch_distances,bins=100,fc=(1, 0, 0, 0.5))

plt.show()

Ta có thể thấy được khoảng cách của những khuôn mặt giống nhau (màu xanh) chủ yếu phân bố trong khoảng 0.4-1, trong khi khoảng cách của những khuôn mặt khác nhau (màu đỏ) là lớn hơn 1.2. Do đó ta chọn ngưỡng threshold = 1 là hợp lý.

Test

Với mỗi bức ảnh để test, ta phải tìm khuôn mặt trong ảnh đó

test_paths = glob.glob("test_image/*.jpg")
for path in test_paths:
    test_image = cv2.imread(path)
    show_image = test_image.copy()

    hogFaceDetector = dlib.get_frontal_face_detector()
    faceRects = hogFaceDetector(test_image, 0)
    
    faces = []
    for faceRect in faceRects:
        x1 = faceRect.left()
        y1 = faceRect.top()
        x2 = faceRect.right()
        y2 = faceRect.bottom()
        face = test_image[y1:y2,x1:x2]
        
        faces.append(face)

sau đó tính embedding vector cho từng khuôn mặt

    print("len(faces) = {0}".format(len(faces)))
    if(len(faces)==0):
        print("no face detected!")
        continue
    else:    
        test_embs = calc_emb_test(faces)

    test_embs = np.concatenate(test_embs)

sau đó tính khoảng cách Euclide. Với những khuôn mặt có khoảng cách lớn hơn ngưỡng, ta xác định đó là 1 người unknown, tức là chưa có trong tập dữ liệu train. Những khuôn mặt có khoảng cách nhỏ hơn threshold sẽ được xác định label tương ứng.

    people = []
    for i in range(test_embs.shape[0]):
        distances = []
        for j in range(len(train_paths)):
            distances.append(np.min([distance.euclidean(test_embs[i].reshape(-1), train_embs[k].reshape(-1)) for k in label2idx[j]]))
        if np.min(distances)>threshold:
            people.append("unknown")
        else:
            res = np.argsort(distances)[:1]
            people.append(res)

và cuối cùng là hiển thị kết quả lên bức ảnh.

    for i,faceRect in enumerate(faceRects):
        x1 = faceRect.left()
        y1 = faceRect.top()
        x2 = faceRect.right()
        y2 = faceRect.bottom()
        cv2.rectangle(show_image,(x1,y1),(x2,y2),(255,0,0),3)
        cv2.putText(show_image,names[i],(x1,y1-5), cv2.FONT_HERSHEY_SIMPLEX, 2,(255,0,0),3,cv2.LINE_AA)

Kết quả

Tài liệu tham khảo


Cách phát hiện mặt người từ đầu bằng deep learning
#2

Em thử test với ảnh Beyonce và Adele thì cả 2 đều ra là Adele. Anh xem có đúng như thế không ạ