Распознавания людей с помощью сиамской нейронной сети. Python

Про нейронные сети мы уже писали в наших статья. Про идею сиамкой нейронной сети для распознавания людей мы тоже уже писали. Хотелось бы теперь попробовать этот подход в полевых условиях, на практике. Реализовывать мы будем все это дело на Python с помощью библиотеки Keras.

Я набросал не большой план действий :

  • Данные. Первым делом на нужно собрать данные для обучения нейросети.
  • Архитектура. Нужно разработать архитектуру сети и написать ее на Keras.
  • Проверка. После обучения сети и получения сухих цифр по ошибкам, проверим распознавание на видео записи.

Создание базы данных.

Мы можем конечно поискать уже готовы дата-сеты в интернете. Я решил создать своею базу данных. Собственно что бы распознать человека на нужно его лицо. Для поиска лица будем использовать библиотеку OpenCV. Сиамской нейросети нужны как лица одного и того же человека, так и лицу других людей. Вопрос где их взять много ?

На самом деле мы можем достаточно легко решить этот вопрос с помощью веб камеры или видео файла. По сути любое видео это набор картинок где мы будем искать лица, приводить к нужному размеру, маркировать и сохранять в базу. Библиотека OpenCV на позволяет как работать с веб-камерой, так работать и с видео и даже с картинками. Поехали :

# Пример кода для веб-камеры
import cv2
import random
import os
# Прописываем классификатор для лица 
faceCascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml') 
# Прописываем классификатор для глаз
eyeCascade = cv2.CascadeClassifier('haarcascade_eye.xml')
cap = cv2.VideoCapture(0)
# Путь куда сохраняем картинки
PATCH_SAVE = 's0/'
# Метка
METKA = 'I_'

while True:
    ret, img = cap.read()
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # Ищем лица
    faces = faceCascade.detectMultiScale(
        gray,               
        scaleFactor=1.2,    
        minNeighbors=3,     
        minSize=(100, 100)    
    )        
    for (x, y, w, h) in faces:
        roi_gray = gray[y:y + h, x:x + w] # Вырезаем область с лицами
        # Ищем глаза в области с лицом для увеличения точности обноружения.
        eyes = eyeCascade.detectMultiScale(
        roi_gray,             
        scaleFactor=1.2,       
        minNeighbors=4,
        minSize=(10, 10),
    )
        if len(eyes) > 0:
        # Генерируем имя картинки
           name = METKA+str(random.randint(1,1000000))+'.jpg'
           size = (100, 100)
        # Изменяем размер картинки на 100х100. Нам не нужно заботиться о пропорциях,
так как OpenCV выделяет прямоугольные области с лицом.
           output = cv2.resize(roi_gray, size, interpolation=cv2.INTER_AREA)
        # Сохраняем лицо
           cv2.imwrite(os.path.join(PATCH_SAVE, name), output)
        # Рисуем контур лица на видео для удобства.
           cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)
    
    cv2.imshow("camera", img)
    if cv2.waitKey(10) == 27: # Клавиша Esc
        break
cap.release()
cv2.destroyAllWindows()

Для поиска лиц в видео мы немого изменим код :

PATCH = '333.mp4'
cap = cv2.VideoCapture(PATCH)
while (cap.isOpened()):
# Читаем очередной кадр из видео.
    ret, frame = cap.read()
# Преобразуем кадр в оттенок серого.
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

Для качественно обучения нейронной сети нужны большие объем данных. По этому правило для датасетов простое: чем они разнообразней и больше тем лучше.

Архитектура.

Ну вот. Картинки мы собрали, привели к одному размеру, разложили по папкам. Пришло теперь время определить как будет выглядеть наша сеть. Люди которые во всем этом разбираются уже определили что для работы с изображение лучше всего подходят сверточные сети. Можно конечно самому придумать архитектуру такой сети, но я точно не эксперт в этом. По этому возьмем уже готовую, например AlexNet .

AlexNet была первой свёрточной нейросетью, выигравшей соревнование по классификации ImageNet в 2012 году.

Её архитектура состоит из пяти свёрточных слоёв, между которыми располагаются pooling-слои и слои нормализации, а завершают нейросеть три полносвязных слоя. Изначально на вход подаётся фотография размером 227×227×3, и размер свёрточных фильтров первого слоя — 11×11. Всего применяется 96 фильтров с шагом 4.

В 2014 году победила уже другая архитектура VGG. VGG архитектур — использование большего числа слоёв с фильтрами меньшего размера. Существуют версии VGG-16 и VGG-19 с 16 и 19 слоями соответственно.

Для начало импортируем все что нам потребуется.

import numpy as np
import random
import os
import cv2
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Flatten, Input, Lambda, MaxPooling2D, Dropout
from keras.models import Model
from keras.optimizers import RMSprop
from keras import backend as K

num_classes = 5
epochs = 10

У нас изображение размером меньше, разделим Alex пополам ))

def create_base_network(input_shape):
    '''Создаем модель нейросети.
    '''
    models = Sequential()
    # Слой свертки
    models.add(Conv2D(48, kernel_size=11, activation='relu', input_shape=input_shape))
    # Слой свертки
    models.add(Conv2D(128, kernel_size=5, activation='relu'))
    # Слой пулинга
    models.add(MaxPooling2D(pool_size=3))
    # Слой нормализации
    models.add(Dropout(.25))
    # Слой свертки
    models.add(Conv2D(192, kernel_size=3, activation='relu'))
    models.add(MaxPooling2D(pool_size=3))
    models.add(Dropout(.25))
    models.add(Conv2D(192, kernel_size=3, activation='relu'))
    models.add(Conv2D(128, kernel_size=3, activation='relu'))
    models.add(Flatten())
    # Полносвязный слой
    models.add(Dense(2048, activation='relu'))
    models.add(Dense(2048, activation='relu'))
    models.add(Dense(500, activation='relu'))
    return models

Суть сиамской нейронной сети в том что через одну сеть прогоняют два изображения, на выходи получают вектор (набор чисел). Потом эти два вектора передают в функцию энергии, которая оценивает сходство.

Давайте напишем функцию загрузки наших данных

# PATCH - константа пути 
def data_set_read():
    index = 0
    index_foto = []
    foto = []
    list = os.listdir(PATCH)
    for i in list:
        if os.path.isdir(PATCH + i):
            tmp = os.listdir(PATCH + i)
            for a in tmp:
                image = cv2.imread(PATCH + i + '/' + a)
                foto.append(image)
                index_foto.append(index)
            index += 1
    index_foto = np.array(index_foto)
    foto = np.array(foto)
    foto = np.resize(foto, (len(foto), 100, 100, 1))
    return foto, index_foto

Теперь на надо разбить фотографии по парам. Пара одного лица и пара разных лиц :

def create_pairs(x, digit_indices):
    '''Создаем позитивные и негативные пары.'''
    pairs = []
    labels = []
    n = min([len(digit_indices[d]) for d in range(num_classes)]) - 1
    for d in range(num_classes):
        for i in range(n):
            z1, z2 = digit_indices[d][i], digit_indices[d][i + 1]
            pairs += [[x[z1], x[z2]]]
            inc = random.randrange(1, num_classes)
            dn = (d + inc) % num_classes
            z1, z2 = digit_indices[d][i], digit_indices[dn][i]
            pairs += [[x[z1], x[z2]]]
            labels += [1, 0]
    return np.array(pairs), np.array(labels)

# Загружаем и подготавливаем дата сет.
x_train, y_train = data_set_read()
x_train = x_train.astype('float32')
x_train /= 255
input_shape = x_train.shape[1:]

# Создаем пары картинок
digit_indices = [np.where(y_train == i)[0] for i in range(num_classes)]
tr_pairs, tr_y = create_pairs(x_train, digit_indices)
# Создаем тестовый и обучающий набор
te_pairs = tr_pairs[0:500]
te_y = tr_y[0:500]
tr_pairs = tr_pairs[500::]
tr_y = tr_y[500::]

Теперь собираем нашу сеть :

base_network = create_base_network(input_shape)

input_a = Input(shape=input_shape)
input_b = Input(shape=input_shape)


processed_a = base_network(input_a)
processed_b = base_network(input_b)

distance = Lambda(euclidean_distance,
                  output_shape=eucl_dist_output_shape)([processed_a, processed_b])

model = Model([input_a, input_b], distance)

Теперь надо сделать немного магии(ибо в этом я нифига не понимаю), напишем функции расчета энергии:

def euclidean_distance(vects):
    x, y = vects
    sum_square = K.sum(K.square(x - y), axis=1, keepdims=True)
    return K.sqrt(K.maximum(sum_square, K.epsilon()))


def eucl_dist_output_shape(shapes):
    shape1, shape2 = shapes
    return (shape1[0], 1)

Следующий шаг, пишем функцию ошибки для обучения сети :

def contrastive_loss(y_true, y_pred):
    '''Contrastive loss from Hadsell-et-al.'06
    http://yann.lecun.com/exdb/publis/pdf/hadsell-chopra-lecun-06.pdf
    '''
    margin = 1
    square_pred = K.square(y_pred)
    margin_square = K.square(K.maximum(margin - y_pred, 0))
    return K.mean(y_true * square_pred + (1 - y_true) * margin_square)

Еще нам нужно добавить функцию Метрики. Метрика-это функция, которая используется для оценки производительности модели.

Функции метрики аналогичны функциям потерь, за исключением того, что результаты оценки метрики не используются при обучении модели. Обратите внимание, что вы можете использовать любые функции потерь в качестве метрической функции.

def compute_accuracy(y_true, y_pred):
    '''Compute classification accuracy with a fixed threshold on distances.
    '''
    pred = y_pred.ravel() < 0.5
    return np.mean(pred == y_true)

def accuracy(y_true, y_pred):
    '''Compute classification accuracy with a fixed threshold on distances.
    '''
    return K.mean(K.equal(y_true, K.cast(y_pred < 0.5, y_true.dtype)))

Последний шаг, запускаем обучение сети :

rms = RMSprop()
model.compile(loss=contrastive_loss, optimizer=rms, metrics=[accuracy])
history = model.fit([tr_pairs[:, 0], tr_pairs[:, 1]], tr_y,
          batch_size=64,
          epochs = 1,
          validation_data=([te_pairs[:, 0], te_pairs[:, 1]], te_y),
          verbose = 2,
          workers = 4
          )
# Сохраняем веса сети в файл.
model.save_weights('my_checkpoint')
# Вычислите конечную точность на тренировочных и тестовых наборах
y_pred = model.predict([tr_pairs[:, 0], tr_pairs[:, 1]])
tr_acc = compute_accuracy(tr_y, y_pred)
y_pred = model.predict([test_pairs[:, 0], test_pairs[:, 1]])
te_acc = compute_accuracy(test_y, y_pred)
print('* Accuracy on training set: %0.2f%%' % (100 * tr_acc))
print('* Accuracy on test set: %0.2f%%' % (100 * te_acc))

Можно воспользоваться обратным вызовом для сохранения лучших результатов вестов, а не после тренировки.

from tensorflow.keras.callbacks import ModelCheckpoint

checkpoint = ModelCheckpoint(filepath='check_point', monitor='val_loss', verbose=0, save_best_only=True, mode='auto', period=1, save_weights_only=True)
history = model.fit([tr_pairs[:, 0], tr_pairs[:, 1]], tr_y,
          batch_size=128,
          epochs=epochs,
          validation_data=([te_pairs[:, 0], te_pairs[:, 1]], te_y),
          callbacks=[checkpoint]
          )

keras.callbacks.callbacks.ModelCheckpoint( filepath, monitor=’val_loss’, verbose=0, save_best_only=False, save_weights_only=False, mode=’auto’, period=1)

Сохраните модель после каждой эпохи. filepath может содержать именованные параметры форматирования, который будет заполнен значениями epochи 
ключи внутри logs(передано on_epoch_end). Если filepath есть
weights.{epoch:02d}-{val_loss:.2f}.hdf5, затем контрольные точки модели будут сохранены с номером эпохи и потеря проверки в имени файла.
  • save_best_only=True — самая последняя или самая лучшая модель согласно контролируемое количество не будет перезаписано.
  • save_weights_only=False — если True, то весами модели будут только сохранено ( model.save_weights(filepath)), иначе полная модель сохраняется ( model.save(filepath)).
  • mode=’auto’режим один из {auto, min, max}.

Проверка в боевых условиях.

Теперь давайте проверим нашу сеть на каком нибудь видео:


PATCH = '333.mp4'
font = cv2.FONT_HERSHEY_SIMPLEX
faceCascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
eyeCascade = cv2.CascadeClassifier('haarcascade_eye.xml')

def euclidean_distance(vects):
    x, y = vects
    sum_square = K.sum(K.square(x - y), axis=1, keepdims=True)
    return K.sqrt(K.maximum(sum_square, K.epsilon()))

def eucl_dist_output_shape(shapes):
    shape1, shape2 = shapes
    return (shape1[0], 1)

def create_base_network(input_shape):
    '''Создаем модель нейросети.
    '''
    models = Sequential()
    # Слой свертки
    models.add(Conv2D(48, kernel_size=11, activation='relu', input_shape=input_shape))
    # Слой свертки
    models.add(Conv2D(128, kernel_size=5, activation='relu'))
    # Слой пулинга
    models.add(MaxPooling2D(pool_size=3))
    # Слой нормализации
    models.add(Dropout(.25))
    # Слой свертки
    models.add(Conv2D(192, kernel_size=3, activation='relu'))
    models.add(MaxPooling2D(pool_size=3))
    models.add(Dropout(.25))
    models.add(Conv2D(192, kernel_size=3, activation='relu'))
    models.add(Conv2D(128, kernel_size=3, activation='relu'))
    models.add(Flatten())
    # Полносвязный слой
    models.add(Dense(2048, activation='relu'))
    models.add(Dense(2048, activation='relu'))
    models.add(Dense(500, activation='relu'))
    return models

def raspoznanie(cadr):
    rez = []
    # Создаем масив под пару фотографий
    CIA= np.zeros([1, 2, 100, 100])
    Enemy = np.zeros([1, 2,  100, 100])
    Ledy = np.zeros([1, 2, 100, 100])
    # Загружаем образцы лиц
    tmp1 = cv2.imread('detect/enemy.jpg' , cv2.IMREAD_UNCHANGED)
    tmp2 = cv2.imread('detect/cia.jpg' , cv2.IMREAD_UNCHANGED)
    tmp3 = cv2.imread('detect/ledy.jpg' , cv2.IMREAD_UNCHANGED)
    # Создаем пары из кадра и образцов
    Enemy[0, 0, :, :] = tmp1
    Enemy[0, 1, :, :] = cadr
    Enemy /= 255
    Enemy = Enemy.reshape(1, 2, 100, 100, 1)
    CIA[0, 0, :, :] = tmp2
    CIA[0, 1, :, :] = cadr
    CIA /= 255
    CIA = CIA.reshape(1, 2, 100, 100, 1)
    Lady[0, 0 , :, :] = tmp3
    Lady[0, 0 , :, :] = cadr
    Ledy /= 255
    Ledy = Ledy.reshape(1, 2, 100, 100, 1)
    # Прогоняем пары через сеть
    y_pred = model.predict([CIA[:, 0], CIA[:, 1]])
    rez.append(y_pred[0])
    y_pred = model.predict([Enemy[:, 0], Enemy[:, 1]])
    rez.append(y_pred[0])
    y_pred = model.predict([Ledy[:, 0], Ledy[:, 1]])
    rez.append(y_pred[0])
    data = ['CIA','Enemy','Lady']
    rez = np.array(rez)
    # Возвращаем метку распознанного лица или False
    if rez[np.argmin(rez)] > 0.5:
        return 'False', rez[np.argmin(rez)]
    return data[np.argmin(rez)], rez[np.argmin(rez)]
# Создаем сеть
input_shape = [100,100,1]
base_network = create_base_network(input_shape)
# Создаем два входа
input_a = Input(shape=input_shape)
input_b = Input(shape=input_shape)
# Получаем два вектора из сети
processed_a = base_network(input_a)
processed_b = base_network(input_b)
# Пишем функцию получения дистанции векторов (функция энергии).
distance = Lambda(euclidean_distance, output_shape=eucl_dist_output_shape)([processed_a, processed_b])
#Собираем модель сети, два входа, выход- дистанция
model = Model([input_a, input_b], distance)
print(model.summary())
# Оптимизатор будет RMSprop
rms = RMSprop()
# Собираем модель
model.compile(loss=contrastive_loss, optimizer=rms)
# Загружаем веса обученной сети
model.load_weights('my_checkpoint')
# Загружаем видео
cap = cv2.VideoCapture(PATCH)
while (cap.isOpened()):
    # Захват по кадрам
    ret, frame = cap.read()
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    faces = faceCascade.detectMultiScale(
        gray,
        scaleFactor=1.2,
        minNeighbors=5,
        minSize=(100, 100)
        )
        for (x, y, w, h) in faces:
            roi_gray = gray[y:y + h, x:x + w]  # Вырезаем область с лицами
            eyes = eyeCascade.detectMultiScale(
                roi_gray,  #
                scaleFactor=1.1,  # Ищем глаза в области с лицом
                minNeighbors=3,
                minSize=(10, 10),)
            if len(eyes) > 0:
                cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 2)
                size = (100, 100)
                outsev = cv2.resize(roi_gray, size, interpolation=cv2.INTER_AREA)
                # Отправляем найденное лицо на распознание
                Text,ping = raspoznanie(outsev)
                Text = Text + ' ' + str(ping)
                #Пишем метку и значение растения векторов
                cv2.putText(frame, Text, (x + 5, y - 5), font, 0.5, (255, 255, 255), 1)
    if ret == True:
        cv2.imshow('Frame', frame)
    else :
        break
#  Нажмите Q на клавиатуре, чтобы выйти
    if cv2.waitKey(25) & 0xFF == ord('q'):
        cap.release()
        # Закрывает все кадры
        cv2.destroyAllWindows()
        break
cap.release()
# Закрывает все кадры
cv2.destroyAllWindows()

Ну а далее будет небольшая нарезочка видео :

Выводы.

Лично мне не понравилось как распознает лица библиотека OpenCV, конечно можно попробовать другие классификаторы лиц. Или попробовать библиотеку dlib.

Теперь собственно о нашей нейронной сети. Если вы посмотрели видео то сеть справилась со своей задачью, но отчасти. Если косяки, порой даже очень забавные на взгляд человека. Мы можем уменьшить процент ошибок. Возможно вы заметили что спорные моменты были при дистанции больше 0,15. Можно сказать что до этого числа сеть уверенно распознавала человека. Так же у меня была маленькая база, всего лишь 618 пар, думаю это очень мало. Улучшив датасет можно добиться еще больших результатов.

Я точно такой же исследователь и новичок как и вы. Надеюсь эта статься вам пригодилась.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.