В этой статье я хотел бы остановиться на алгоритмах распознавания лиц, а заодно познакомить вас с очень интересной и полезной библиотекой OpenCV. Уверен, что этот материал окажется полезным для новичков в этой области.

Что нам понадобится:
• Установленный Python 2.7 с библиотеками NumPy и PIL
• OpenCV 2-й версии

Здесь ссылка на материал по установке всех необходимых компонентов. Установка всего необходимого не составит труда.

Для начала давайте разберемся, как распознать лицо на фотографии. Во-первых, нужно найти, где на фото расположено лицо человека и не спутать его с часами на стене и кактусом на подоконнике. Казалось бы, простая задача для человека, оказывается не такой простой для компьютера. Для того, чтобы найти лицо мы должны выделить его основные компоненты, такие как нос, лоб, глаза, губы и т.д. Для этого будем использовать шаблоны (они же примитивы Хаара) на подобие таких:



Если шаблоны соответствуют конкретным областям на изображении, будем считать, что на изображении есть человеческое лицо. На самом деле подобных шаблонов гораздо больше. Для каждого из них считается разность между яркостью белой и черной областей. Это значение сравнивается с эталоном и принимается решение о том, есть ли здесь часть человеческого лица или нет.
Этот метод называется методом Виолы-Джонса (так же известен как каскады Хаара). Давайте представим, что у нас на фотографии не одно большое лицо, а много мелких. Если применить шаблоны ко всей картинке мы не найдем там лиц, т.к. они будут меньше шаблонов. Для того чтобы искать на всем фото лица разных размеров используется метод скользящего окна. Именно внутри этого окна и высчитываются примитивы. Окно как бы скользит по всему изображению. После каждого прохождения изображения окно увеличивается, чтобы найти лица большего масштаба.

Наглядно демонстрацию алгоритма можно посмотреть на этом видео:


И так мы нашли лицо на фотографии, но как определить, что это лицо именно того кого мы ищем? Для решения этой задачи будем использовать алгоритм Local Binary Patterns. Суть его заключается в том, что мы разбиваем изображение на части и в каждой такой части каждый пиксель сравнивается с соседними 8 пикселями. Если значение центрального пикселя больше соседнего, то пишем 0, в противном случае 1. И так для каждого пикселя у нас получается некоторое число. Далее на основе этих чисел для всех частей, на которые мы разбивали фотографию, считается гистограмма. Все гистограммы со всех частей объединяются в один вектор характеризующий изображение в целом. Если мы хотим узнать насколько похожи два лица, нам придется вычислить для каждого из них такой вектор и сравнить их. Рисунки ниже помогут лучше понять суть алгоритма:




Ну хорошо, давайте, наконец напишем немного кода. За основу я взял код из этой статьи.

# Импортируем необходимые модули
import cv2, os
import numpy as np
from PIL import Image

# Для детектирования лиц используем каскады Хаара
cascadePath = "haarcascade_frontalface_default.xml"
faceCascade = cv2.CascadeClassifier(cascadePath)

# Для распознавания используем локальные бинарные шаблоны
recognizer = cv2.createLBPHFaceRecognizer(1,8,8,8,123)

Параметр cascadePath содержит имя файла с уже готовыми значениями для распознавания лиц. Этот файл можно взять из директории с OpenCV (opencv\build\etc\haarcascades\).
Далее создаем объект CascadeClassifier и объект распознавания лиц LBPHFaceRecognizer. На последнем остановимся поподробнее, точнее, на его параметрах. Первые два значения 1 и 8 характеризуют окрестности пикселя. Наглядно, что это такое можно продемонстрировать этой картинкой:


То есть первое число это радиус в котором мы выбираем пиксели, а второй число этих пикселей. Чем больше пикселей в окрестности точки мы возьмем, тем точнее будет наше распознавание.
Следующие параметры (8,8) характеризуют размеры областей на которые мы разбиваем исходное изображение с лицом. Чем оно меньше, тем больше будет таких областей и тем качественнее распознавание.
И наконец, последнее значение это параметр confidence threshold, определяющий пороговое значение для распознавания лица. Чем меньше confidence тем больше алгоритм уверен в том, что на фотографии изображено известное ему лицо. Порог означает, что когда уверенности мало алгоритм просто считает это лицо незнакомым. В данном случае порог равен 123.

Идем дальше. Напишем функцию, которая находит по определенному пути во всех фотографиях лица людей и сохраняет их.


def get_images(path):
    # Ищем все фотографии и записываем их в image_paths
    image_paths = [os.path.join(path, f) for f in os.listdir(path) if not f.endswith('.happy')]
    
    images = []
    labels = []

    for image_path in image_paths:
        # Переводим изображение в черно-белый формат и приводим его к формату массива
        gray = Image.open(image_path).convert('L')
        image = np.array(gray, 'uint8')
        # Из каждого имени файла извлекаем номер человека, изображенного на фото
        subject_number = int(os.path.split(image_path)[1].split(".")[0].replace("subject", ""))
        
        # Определяем области где есть лица
        faces = faceCascade.detectMultiScale(image, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
        # Если лицо нашлось добавляем его в список images, а соответствующий ему номер в список labels
        for (x, y, w, h) in faces:
            images.append(image[y: y + h, x: x + w])
            labels.append(subject_number)
            # В окне показываем изображение
            cv2.imshow("", image[y: y + h, x: x + w])
            cv2.waitKey(50)
    return images, labels

Для примера я использовал БД лиц под названием Yale Faces. В ней есть 15 человек с разными выражениями лиц на каждой фотографии.


Имя каждого файла в этой БД выглядит следующим образом: subject01.sad. Сначала идет слово subject, далее порядковый номер человека, а после характеристика фото. Например, характеристика sad означает грустное лицо, happy веселое и т.п.
Функция get_images считывает каждую фотографию, кроме тех, что с окончанием .happy и выделяет ту область, где находится лицо. Фотографии с веселым выражением лица будем использовать на следующем шаге для распознавания, это будет контрольная выборка, т.е. те фото на которых мы будем проверять качество распознавания.
Так же из каждого названия файла извлекается номер человека на фотографии и сохраняется список labels. Каждой фотографии в итоге будет сопоставлен этот номер.
Функция faceCascade.detectMultiScale() определяет области на фотографии, где есть человеческие
лица. Она возвращает список с параметрами [x,y,w,h] для каждого найденного лица. Эти
параметры описывают прямоугольную область в том месте, где нашлось лицо.

Теперь давайте разберемся с параметрами функции:
image – исходное изображение
scaleFactor – определяет то, на сколько будет увеличиваться скользящее окно поиска на каждой итерации. 1.1 означает на 10%, 1.05 на 5% и т.д. Чем больше это значение, тем быстрее работает алгоритм.
minNeighbors — Чем больше это значение, тем более параноидальным будет поиск и тем чаще он будет пропускать реальные лица, считая, что это ложное срабатывание. Оптимальное значение 3-6.
minSize – минимальный размер лица на фото. 30 на 30 обычно вполне достаточно.

Ну что же, теперь мы можем создать набор лиц и соответствующих им меток. Давайте научим программу распознавать эти лица.

# Путь к фотографиям
path = './yalefaces'
# Получаем лица и соответствующие им номера
images, labels = get_images(path)
cv2.destroyAllWindows()

# Обучаем программу распознавать лица
recognizer.train(images, np.array(labels))


Указываем путь к нашим фото, получаем список с фотографиями и метками. А дальше запускаем нашу функцию тренировки с помощью алгоритма LBP. Ничего сверхъестественного в ней нет, просто передаем ей значения, полученные после запуска функции get_images(). Все остальное программа сделает сама.
И так у нас есть обученный «распознаватель» и есть набор счастливых лиц. Теперь нам необходимо попросить алгоритм распознать эти счастливые лица.

# Создаем список фотографий для распознавания
image_paths = [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.happy')]

for image_path in image_paths:
    # Ищем лица на фотографиях
    gray = Image.open(image_path).convert('L')
    image = np.array(gray, 'uint8')
    faces = faceCascade.detectMultiScale(image, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))

    
    for (x, y, w, h) in faces:
        # Если лица найдены, пытаемся распознать их
        # Функция  recognizer.predict в случае успешного распознавания возвращает номер и параметр confidence,
        # этот параметр указывает на уверенность алгоритма, что это именно тот человек, чем он меньше, тем больше уверенность
        number_predicted, conf = recognizer.predict(image[y: y + h, x: x + w])

        # Извлекаем настоящий номер человека на фото и сравниваем с тем, что выдал алгоритм
        number_actual = int(os.path.split(image_path)[1].split(".")[0].replace("subject", ""))
        
        if number_actual == number_predicted:
            print "{} is Correctly Recognized with confidence {}".format(number_actual, conf)
        else:
            print "{} is Incorrect Recognized as {}".format(number_actual, number_predicted)
        cv2.imshow("Recognizing Face", image[y: y + h, x: x + w])
        cv2.waitKey(1000)


В цикле опять определяем расположение лица на каждом фото с окончанием .happy. Все параметры и процедуры такие же, как и на предыдущем этапе.
Для каждого найденного лица запускаем функцию recognizer.predict(), возвращающую номер-идентификатор субъекта, который предположительно находится на фото, а так же параметр confidence. Далее сравниваем значение, которое нам вернула функция с реальным номером субъекта, если они равны, распознавание прошло успешно.
Ну, вот и все, дальше в консоль выводятся результаты распознавания для каждой фотографии из контрольной выборки.


Исходный код программы можно найти здесь.
Поделиться с друзьями
-->

Комментарии (9)


  1. myxo
    18.05.2016 12:07
    +1

    Стоит заметить, что на практике ручным выделение фичей никто не занимается, эта задача отдается каким-то статистическим методам или нейронкам.


  1. darkAlert
    18.05.2016 12:13
    +3

    Я бы посоветовал выравнивать лицо после его детекции. Это также можно сделать в opencv штатными средствами — алгоритмом Виолы-Джонса обученным на детекцию глаз/рта/носа (xml идут в комплекте с opencv). Сделать можно так:
    1) Определяем глаза и рот (можно нос) на ранее детектированном лице.
    2) Ищем среднюю точку m_eyes между двух глаз.
    3) Ищем среднюю точку m_face между m_eyes и ртом.
    4) Определяем по двум глазам угол поворота лица и вращаем лицо вокруг точки m_face с этим углом.
    5) Теперь мы можем вырезать лицо по заданным параметрами: точку m_eyes принимаем за центр вырезки, а расстояние L=dist(m_face,m_eyes) за единицу масштаба.


  1. siemdi
    18.05.2016 13:59

    Очень интересное описание алгоритмов. Просто и понятно.
    Спасибо)


  1. mikkab
    18.05.2016 18:11

    Все Ok, но зачем PIL если есть cv2.imread()? Стоит отметить что Local Binary Patterns будет работать как минимум с данными того же ракурса, если немного повернули объект, то скорее всего уже ничего не получится.


    1. AlexeyAB
      24.05.2016 17:41
      +1

      Да, сырой LBP будет работать только без сильного изменения освещения и при том же повороте лица (влево-вправо / вверх-вниз). Единственное, как писали выше, легко исправить наклоны головы вбок (или камеры).
      Т.е. сырой LBP подойдет только для фронтальных изображений:
      image:

      В реальных проектах LBP может использоваться только со сложными примочками high-dim LBP, HPEN + HD-LBP + JB или MRF-MLBP — и уже сможет распознавать практически любые изображения лиц:
      image

      Но лучшими остаются алгоритмы Spartans с гибкими 3D моделям, TSML with feature fusion, MDML-DCPs или конечно же ColorReco — глубокая сверточная нейронная сеть.
      http://vis-www.cs.umass.edu/lfw/results.html


  1. ciiccii
    18.05.2016 18:34
    +1

    Это перевод или плагиат? http://hanzratech.in/2015/02/03/face-recognition-using-opencv.html


    1. galvanom
      18.05.2016 20:08

      Ни то ни другое. За основу был взят код из этой статьи. Упоминание о ней я добавил в публикацию. Текст полностью мой.


  1. alexhouse
    25.05.2016 12:57

    OpanCV мощный пакет. Может вы знаете, как в нем делать подсчет объектов при пересечении линии. Пример кода с подсчетом автомобилей нужен.


    1. galvanom
      25.05.2016 15:11

      Вот вам ссылка на описание методов подсчета автомобилей на видео.