В этой статье объясняется практическое использование OpenCV FaceRecognizer от opencv_contrib. Полная публикация в документации на официальной странице OpenCV.

Https://docs.opencv.org/2.4.9/modules/contrib/doc/facerec

"Введение"

OpenCV (Open Source Computer Vision) - популярная библиотека компьютерного зрения, созданная Intel в 1999 году. Кроссплатформенная библиотека фокусируется на обработке изображений в реальном времени и включает свободные от патентов реализации новейших алгоритмов компьютерного зрения. В 2008 году Willow Garage взял на себя поддержку, и OpenCV 2.3.1 теперь поставляется с программным интерфейсом для C, C ++, Python и Android. OpenCV выпущен под лицензией BSD, поэтому он используется как в академических проектах, так и в коммерческих продуктах.

OpenCV 2.4 теперь поставляется с совершенно новым классом FaceRecognizer для распознавания лиц, так что вы можете сразу же начать экспериментировать с распознаванием лиц. Этот документ - руководство, о котором я мечтал, когда работал над распознаванием лиц. Он показывает вам, как выполнять распознавание лиц с помощью FaceRecognizer в OpenCV (с полными списками исходного кода), и дает вам представление об используемых алгоритмах. Я также покажу, как создавать визуализации, которые можно найти во многих публикациях, потому что многие люди об этом просили.

В настоящее время доступны следующие алгоритмы:

Вам не нужно копировать и вставлять примеры исходного кода с этой страницы, потому что они доступны в папке src, поставляемой с этой документацией. Если вы создали OpenCV с включенными образцами, скорее всего, они уже скомпилированы! Хотя это может быть интересно для очень продвинутых пользователей, я решил опустить детали реализации, так как боюсь, что они запутают новых пользователей.

Весь код в этом документе выпущен под лицензией BSD, поэтому не стесняйтесь использовать его в своих проектах.

"Распознавание лица"

Распознавание лиц - простая задача для человека. Эксперименты в [Tu06] показали, что даже 1-3-дневные младенцы способны различать известные лица. Так насколько же сложно это может быть для компьютера? Оказывается, на сегодняшний день мы мало знаем о человеческом признании. Используются ли внутренние черты (глаза, нос, рот) или внешние (форма головы, линия роста волос) для успешного распознавания лиц? Как мы анализируем изображение и как мозг его кодирует? Дэвид Хьюбел и Торстен Визель показали, что в нашем мозгу есть специализированные нервные клетки, реагирующие на определенные локальные особенности сцены, такие как линии, края, углы или движение. Поскольку мы не видим мир как разрозненные части, наша зрительная кора должна каким-то образом объединять различные источники информации в полезные паттерны. Автоматическое распознавание лиц заключается в извлечении этих значимых функций из изображения, превращении их в полезное представление и выполнении над ними какой-то классификации.

Распознавание лиц, основанное на геометрических особенностях лица, вероятно, является наиболее интуитивно понятным подходом к распознаванию лиц. Одна из первых автоматизированных систем распознавания лиц была описана в [Kanade73]: маркерные точки (положение глаз, ушей, носа,…) использовались для построения вектора признаков (расстояние между точками, угол между ними,…). Распознавание производилось путем вычисления евклидова расстояния между векторами признаков зонда и эталонного изображения. Такой метод по своей природе устойчив к изменениям освещенности, но имеет огромный недостаток: точная регистрация точек маркера затруднена даже с использованием современных алгоритмов. Некоторые из последних работ по геометрическому распознаванию лиц были выполнены в [Bru92]. Использовался 22-мерный вектор признаков, и эксперименты с большими наборами данных показали, что одни только геометрические объекты не несут достаточно информации для распознавания лиц.

Метод Eigenfaces, описанный в [TP91], основывается на целостном подходе к распознаванию лиц: изображение лица - это точка из многомерного пространства изображений, и обнаруживается низкоразмерное представление, в котором упрощается классификация. Подпространство более низкой размерности находится с помощью анализа главных компонентов, который определяет оси с максимальной дисперсией. Хотя этот вид преобразования оптимален с точки зрения реконструкции, он не принимает во внимание какие-либо ярлыки классов. Представьте себе ситуацию, когда дисперсия генерируется из внешних источников, пусть она будет легкой. Оси с максимальной дисперсией не обязательно содержат какую-либо различительную информацию, поэтому классификация становится невозможной. Таким образом, для распознавания лиц в [BHK97] была применена классовая проекция с линейным дискриминантным анализом. Основная идея состоит в том, чтобы минимизировать дисперсию внутри класса, в то же время максимизируя дисперсию между классами.

В последнее время появились различные методы выделения локальных признаков. Чтобы избежать большой размерности входных данных, описываются только локальные области изображения, извлеченные функции (надеюсь) более устойчивы к частичному перекрытию, освещению и небольшому размеру выборки. Алгоритмы, используемые для выделения локальных признаков, - это вейвлеты Габора ([Wiskott97]), дискретное косинусное преобразование ([Messer06]) и локальные двоичные шаблоны ([AHP04]). Вопрос о том, как лучше всего сохранить пространственную информацию при извлечении локальных объектов, все еще остается открытым, поскольку пространственная информация является потенциально полезной информацией.

Собственные лица

Проблема с представлением изображения, которое нам дается, заключается в его высокой размерности. Двумерные [p x q] изображения в градациях серого охватывают [m = p q] -мерное векторное пространство, поэтому изображение с [100 x 100] пикселей уже находится в [10 000] -мерном пространстве изображения. Возникает вопрос: все ли измерения одинаково полезны для нас? Мы можем принять решение только в том случае, если есть какие-либо расхождения в данных, поэтому мы ищем компоненты, на которые приходится большая часть информации. Анализ главных компонентов (PCA) был независимо предложен Карлом Пирсоном (1901) и Гарольдом Хотеллингом (1933), чтобы превратить набор возможно коррелированных переменных в меньший набор некоррелированных переменных. Идея состоит в том, что многомерный набор данных часто описывается коррелированными переменными, и поэтому только несколько значимых измерений составляют большую часть информации. Метод PCA находит направления с наибольшим разбросом данных, называемые главными компонентами.

Собственные лица в OpenCV

Что касается первого примера исходного кода, я рассмотрю его вместе с вами. Сначала я даю вам полный список исходного кода, а после этого мы подробно рассмотрим наиболее важные строки. Обратите внимание: каждый листинг исходного кода подробно прокомментирован, поэтому у вас не должно возникнуть проблем с его использованием.

/*
 * Copyright (c) 2011. Philipp Wagner <bytefish[at]gmx[dot]de>.
 * Released to public domain under terms of the BSD Simplified license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the organization nor the names of its contributors
 *     may be used to endorse or promote products derived from this software
 *     without specific prior written permission.
 *
 *   See <http://www.opensource.org/licenses/bsd-license>
 */
#include "opencv2/core/core.hpp"
#include "opencv2/contrib/contrib.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <fstream>
#include <sstream>
using namespace cv;
using namespace std;
static Mat norm_0_255(InputArray _src) {
    Mat src = _src.getMat();
    // Create and return normalized image:
    Mat dst;
    switch(src.channels()) {
    case 1:
        cv::normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC1);
        break;
    case 3:
        cv::normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC3);
        break;
    default:
        src.copyTo(dst);
        break;
    }
    return dst;
}
static void read_csv(const string& filename, vector<Mat>& images, vector<int>& labels, char separator = ';') {
    std::ifstream file(filename.c_str(), ifstream::in);
    if (!file) {
        string error_message = "No valid input file was given, please check the given filename.";
        CV_Error(CV_StsBadArg, error_message);
    }
    string line, path, classlabel;
    while (getline(file, line)) {
        stringstream liness(line);
        getline(liness, path, separator);
        getline(liness, classlabel);
        if(!path.empty() && !classlabel.empty()) {
            images.push_back(imread(path, 0));
            labels.push_back(atoi(classlabel.c_str()));
        }
    }
}
int main(int argc, const char *argv[]) {
    // Check for valid command line arguments, print usage
    // if no arguments were given.
    if (argc < 2) {
        cout << "usage: " << argv[0] << " <csv.ext> <output_folder> " << endl;
        exit(1);
    }
    string output_folder = ".";
    if (argc == 3) {
        output_folder = string(argv[2]);
    }
    // Get the path to your CSV.
    string fn_csv = string(argv[1]);
    // These vectors hold the images and corresponding labels.
    vector<Mat> images;
    vector<int> labels;
    // Read in the data. This can fail if no valid
    // input filename is given.
    try {
        read_csv(fn_csv, images, labels);
    } catch (cv::Exception& e) {
        cerr << "Error opening file \"" << fn_csv << "\". Reason: " << e.msg << endl;
        // nothing more we can do
        exit(1);
    }
    // Quit if there are not enough images for this demo.
    if(images.size() <= 1) {
        string error_message = "This demo needs at least 2 images to work. Please add more images to your data set!";
        CV_Error(CV_StsError, error_message);
    }
    // Get the height from the first image. We'll need this
    // later in code to reshape the images to their original
    // size:
    int height = images[0].rows;
    // The following lines simply get the last images from
    // your dataset and remove it from the vector. This is
    // done, so that the training data (which we learn the
    // cv::FaceRecognizer on) and the test data we test
    // the model with, do not overlap.
    Mat testSample = images[images.size() - 1];
    int testLabel = labels[labels.size() - 1];
    images.pop_back();
    labels.pop_back();
    // The following lines create an Eigenfaces model for
    // face recognition and train it with the images and
    // labels read from the given CSV file.
    // This here is a full PCA, if you just want to keep
    // 10 principal components (read Eigenfaces), then call
    // the factory method like this:
    //
    //      cv::createEigenFaceRecognizer(10);
    //
    // If you want to create a FaceRecognizer with a
    // confidence threshold (e.g. 123.0), call it with:
    //
    //      cv::createEigenFaceRecognizer(10, 123.0);
    //
    // If you want to use _all_ Eigenfaces and have a threshold,
    // then call the method like this:
    //
    //      cv::createEigenFaceRecognizer(0, 123.0);
    //
    Ptr<FaceRecognizer> model = createEigenFaceRecognizer();
    model->train(images, labels);
    // The following line predicts the label of a given
    // test image:
    int predictedLabel = model->predict(testSample);
    //
    // To get the confidence of a prediction call the model with:
    //
    //      int predictedLabel = -1;
    //      double confidence = 0.0;
    //      model->predict(testSample, predictedLabel, confidence);
    //
    string result_message = format("Predicted class = %d / Actual class = %d.", predictedLabel, testLabel);
    cout << result_message << endl;
    // Here is how to get the eigenvalues of this Eigenfaces model:
    Mat eigenvalues = model->getMat("eigenvalues");
    // And we can do the same to display the Eigenvectors (read Eigenfaces):
    Mat W = model->getMat("eigenvectors");
    // Get the sample mean from the training data
    Mat mean = model->getMat("mean");
    // Display or save:
    if(argc == 2) {
        imshow("mean", norm_0_255(mean.reshape(1, images[0].rows)));
    } else {
        imwrite(format("%s/mean.png", output_folder.c_str()), norm_0_255(mean.reshape(1, images[0].rows)));
    }
    // Display or save the Eigenfaces:
    for (int i = 0; i < min(10, W.cols); i++) {
        string msg = format("Eigenvalue #%d = %.5f", i, eigenvalues.at<double>(i));
        cout << msg << endl;
        // get eigenvector #i
        Mat ev = W.col(i).clone();
        // Reshape to original size & normalize to [0...255] for imshow.
        Mat grayscale = norm_0_255(ev.reshape(1, height));
        // Show the image & apply a Jet colormap for better sensing.
        Mat cgrayscale;
        applyColorMap(grayscale, cgrayscale, COLORMAP_JET);
        // Display or save:
        if(argc == 2) {
            imshow(format("eigenface_%d", i), cgrayscale);
        } else {
            imwrite(format("%s/eigenface_%d.png", output_folder.c_str(), i), norm_0_255(cgrayscale));
        }
    }
    // Display or save the image reconstruction at some predefined steps:
    for(int num_components = min(W.cols, 10); num_components < min(W.cols, 300); num_components+=15) {
        // slice the eigenvectors from the model
        Mat evs = Mat(W, Range::all(), Range(0, num_components));
        Mat projection = subspaceProject(evs, mean, images[0].reshape(1,1));
        Mat reconstruction = subspaceReconstruct(evs, mean, projection);
        // Normalize the result:
        reconstruction = norm_0_255(reconstruction.reshape(1, images[0].rows));
        // Display or save:
        if(argc == 2) {
            imshow(format("eigenface_reconstruction_%d", num_components), reconstruction);
        } else {
            imwrite(format("%s/eigenface_reconstruction_%d.png", output_folder.c_str(), num_components), reconstruction);
        }
    }
    // Display if we are not writing to an output folder:
    if(argc == 2) {
        waitKey(0);
    }
    return 0;
}

Я использовал цветовую карту Jet, чтобы вы могли видеть, как значения оттенков серого распределяются в пределах конкретных Eigenfaces. Вы можете видеть, что Eigenfaces кодируют не только черты лица, но и освещение на изображениях (см. Левый свет в Eigenface # 4, правый свет в Eigenfaces # 5):

Мы уже видели, что можем реконструировать лицо на основе его приближения меньшей размерности. Итак, давайте посмотрим, сколько Eigenfaces нужно для хорошей реконструкции. Я сделаю подзаговор с [10, 30,…, 310]

Собственные лица:

// Display or save the image reconstruction at some predefined steps:
for(int num_components = 10; num_components < 300; num_components+=15) {
    // slice the eigenvectors from the model
    Mat evs = Mat(W, Range::all(), Range(0, num_components));
    Mat projection = subspaceProject(evs, mean, images[0].reshape(1,1));
    Mat reconstruction = subspaceReconstruct(evs, mean, projection);
    // Normalize the result:
    reconstruction = norm_0_255(reconstruction.reshape(1, images[0].rows));
    // Display or save:
    if(argc == 2) {
        imshow(format("eigenface_reconstruction_%d", num_components), reconstruction);
    } else {
        imwrite(format("%s/eigenface_reconstruction_%d.png", output_folder.c_str(), num_components), reconstruction);
    }
}

10 собственных векторов явно недостаточно для хорошей реконструкции изображения, 50 собственных векторов уже может быть достаточно для кодирования важных черт лица. Вы получите хорошую реконструкцию с примерно 300 собственными векторами для AT&T Facedatabase. Существует правило большого пальца, сколько Eigenfaces вы должны выбрать для успешного распознавания лиц, но оно сильно зависит от входных данных. [Zhao03] - идеальная отправная точка для начала исследования:

Рыболовы

Анализ главных компонентов (PCA), который является ядром метода Eigenfaces, находит линейную комбинацию функций, которая максимизирует общую дисперсию данных. Хотя это явно мощный способ представления данных, он не учитывает какие-либо классы, поэтому при выбрасывании компонентов может быть потеряно много отличительной информации. Представьте себе ситуацию, когда расхождения в ваших данных генерируются внешним источником, пусть это будет свет. Компоненты, идентифицированные с помощью PCA, не обязательно содержат какую-либо различительную информацию, поэтому проецируемые выборки размываются вместе, и классификация становится невозможной (см. Пример http://www.bytefish.de/wiki/pca_lda_with_gnu_octave).

Линейный дискриминантный анализ выполняет уменьшение размерности для конкретных классов и был изобретен великим статистиком сэром Р. А. Фишером. Он успешно использовал его для классификации цветов в своей статье 1936 года Использование множественных измерений в таксономических задачах [Fisher36]. Чтобы найти комбинацию характеристик, которая лучше всего разделяет классы, линейный дискриминантный анализ максимизирует соотношение между классами и разбросом внутри классов вместо максимизации общего разброса. Идея проста: одни и те же классы должны плотно сгруппироваться вместе, в то время как разные классы должны находиться как можно дальше друг от друга в представлении более низкой размерности. Это также признали Belhumeur, Hespanha и Kriegman, и поэтому они применили дискриминантный анализ для распознавания лиц в [BHK97].

Рыболовы в OpenCV

/*
 * Copyright (c) 2011. Philipp Wagner <bytefish[at]gmx[dot]de>.
 * Released to public domain under terms of the BSD Simplified license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the organization nor the names of its contributors
 *     may be used to endorse or promote products derived from this software
 *     without specific prior written permission.
 *
 *   See <http://www.opensource.org/licenses/bsd-license>
 */
#include "opencv2/core/core.hpp"
#include "opencv2/contrib/contrib.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <fstream>
#include <sstream>
using namespace cv;
using namespace std;
static Mat norm_0_255(InputArray _src) {
    Mat src = _src.getMat();
    // Create and return normalized image:
    Mat dst;
    switch(src.channels()) {
    case 1:
        cv::normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC1);
        break;
    case 3:
        cv::normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC3);
        break;
    default:
        src.copyTo(dst);
        break;
    }
    return dst;
}
static void read_csv(const string& filename, vector<Mat>& images, vector<int>& labels, char separator = ';') {
    std::ifstream file(filename.c_str(), ifstream::in);
    if (!file) {
        string error_message = "No valid input file was given, please check the given filename.";
        CV_Error(CV_StsBadArg, error_message);
    }
    string line, path, classlabel;
    while (getline(file, line)) {
        stringstream liness(line);
        getline(liness, path, separator);
        getline(liness, classlabel);
        if(!path.empty() && !classlabel.empty()) {
            images.push_back(imread(path, 0));
            labels.push_back(atoi(classlabel.c_str()));
        }
    }
}
int main(int argc, const char *argv[]) {
    // Check for valid command line arguments, print usage
    // if no arguments were given.
    if (argc < 2) {
        cout << "usage: " << argv[0] << " <csv.ext> <output_folder> " << endl;
        exit(1);
    }
    string output_folder = ".";
    if (argc == 3) {
        output_folder = string(argv[2]);
    }
    // Get the path to your CSV.
    string fn_csv = string(argv[1]);
    // These vectors hold the images and corresponding labels.
    vector<Mat> images;
    vector<int> labels;
    // Read in the data. This can fail if no valid
    // input filename is given.
    try {
        read_csv(fn_csv, images, labels);
    } catch (cv::Exception& e) {
        cerr << "Error opening file \"" << fn_csv << "\". Reason: " << e.msg << endl;
        // nothing more we can do
        exit(1);
    }
    // Quit if there are not enough images for this demo.
    if(images.size() <= 1) {
        string error_message = "This demo needs at least 2 images to work. Please add more images to your data set!";
        CV_Error(CV_StsError, error_message);
    }
    // Get the height from the first image. We'll need this
    // later in code to reshape the images to their original
    // size:
    int height = images[0].rows;
    // The following lines simply get the last images from
    // your dataset and remove it from the vector. This is
    // done, so that the training data (which we learn the
    // cv::FaceRecognizer on) and the test data we test
    // the model with, do not overlap.
    Mat testSample = images[images.size() - 1];
    int testLabel = labels[labels.size() - 1];
    images.pop_back();
    labels.pop_back();
    // The following lines create an Fisherfaces model for
    // face recognition and train it with the images and
    // labels read from the given CSV file.
    // If you just want to keep 10 Fisherfaces, then call
    // the factory method like this:
    //
    //      cv::createFisherFaceRecognizer(10);
    //
    // However it is not useful to discard Fisherfaces! Please
    // always try to use _all_ available Fisherfaces for
    // classification.
    //
    // If you want to create a FaceRecognizer with a
    // confidence threshold (e.g. 123.0) and use _all_
    // Fisherfaces, then call it with:
    //
    //      cv::createFisherFaceRecognizer(0, 123.0);
    //
    Ptr<FaceRecognizer> model = createFisherFaceRecognizer();
    model->train(images, labels);
    // The following line predicts the label of a given
    // test image:
    int predictedLabel = model->predict(testSample);
    //
    // To get the confidence of a prediction call the model with:
    //
    //      int predictedLabel = -1;
    //      double confidence = 0.0;
    //      model->predict(testSample, predictedLabel, confidence);
    //
    string result_message = format("Predicted class = %d / Actual class = %d.", predictedLabel, testLabel);
    cout << result_message << endl;
    // Here is how to get the eigenvalues of this Eigenfaces model:
    Mat eigenvalues = model->getMat("eigenvalues");
    // And we can do the same to display the Eigenvectors (read Eigenfaces):
    Mat W = model->getMat("eigenvectors");
    // Get the sample mean from the training data
    Mat mean = model->getMat("mean");
    // Display or save:
    if(argc == 2) {
        imshow("mean", norm_0_255(mean.reshape(1, images[0].rows)));
    } else {
        imwrite(format("%s/mean.png", output_folder.c_str()), norm_0_255(mean.reshape(1, images[0].rows)));
    }
    // Display or save the first, at most 16 Fisherfaces:
    for (int i = 0; i < min(16, W.cols); i++) {
        string msg = format("Eigenvalue #%d = %.5f", i, eigenvalues.at<double>(i));
        cout << msg << endl;
        // get eigenvector #i
        Mat ev = W.col(i).clone();
        // Reshape to original size & normalize to [0...255] for imshow.
        Mat grayscale = norm_0_255(ev.reshape(1, height));
        // Show the image & apply a Bone colormap for better sensing.
        Mat cgrayscale;
        applyColorMap(grayscale, cgrayscale, COLORMAP_BONE);
        // Display or save:
        if(argc == 2) {
            imshow(format("fisherface_%d", i), cgrayscale);
        } else {
            imwrite(format("%s/fisherface_%d.png", output_folder.c_str(), i), norm_0_255(cgrayscale));
        }
    }
    // Display or save the image reconstruction at some predefined steps:
    for(int num_component = 0; num_component < min(16, W.cols); num_component++) {
        // Slice the Fisherface from the model:
        Mat ev = W.col(num_component);
        Mat projection = subspaceProject(ev, mean, images[0].reshape(1,1));
        Mat reconstruction = subspaceReconstruct(ev, mean, projection);
        // Normalize the result:
        reconstruction = norm_0_255(reconstruction.reshape(1, images[0].rows));
        // Display or save:
        if(argc == 2) {
            imshow(format("fisherface_reconstruction_%d", num_component), reconstruction);
        } else {
            imwrite(format("%s/fisherface_reconstruction_%d.png", output_folder.c_str(), num_component), reconstruction);
        }
    }
    // Display if we are not writing to an output folder:
    if(argc == 2) {
        waitKey(0);
    }
    return 0;
}

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

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

Fisherfaces позволяет реконструировать проецируемое изображение, как и Eigenfaces. Но поскольку мы только определили особенности, чтобы различать объекты, вы не можете ожидать хорошей реконструкции исходного изображения. Для метода Fisherfaces вместо этого мы спроецируем образец изображения на каждый из Fisherfaces. Итак, у вас будет хорошая визуализация, в которой каждый из Fisherfaces описывает:

// Display or save the image reconstruction at some predefined steps:
for(int num_component = 0; num_component < min(16, W.cols); num_component++) {
    // Slice the Fisherface from the model:
    Mat ev = W.col(num_component);
    Mat projection = subspaceProject(ev, mean, images[0].reshape(1,1));
    Mat reconstruction = subspaceReconstruct(ev, mean, projection);
    // Normalize the result:
    reconstruction = norm_0_255(reconstruction.reshape(1, images[0].rows));
    // Display or save:
    if(argc == 2) {
        imshow(format("fisherface_reconstruction_%d", num_component), reconstruction);
    } else {
        imwrite(format("%s/fisherface_reconstruction_%d.png", output_folder.c_str(), num_component), reconstruction);
    }
}

Различия могут быть незаметными для человеческого глаза, но вы сможете увидеть некоторые различия:

Гистограммы локальных двоичных паттернов

Eigenfaces и Fisherfaces используют несколько целостный подход к распознаванию лиц. Вы обрабатываете свои данные как вектор где-то в многомерном пространстве изображений. Мы все знаем, что высокая размерность - это плохо, поэтому определяется подпространство меньшей размерности, где (вероятно) сохраняется полезная информация. Подход Eigenfaces максимизирует общий разброс, что может привести к проблемам, если дисперсия генерируется внешним источником, потому что компоненты с максимальной дисперсией по всем классам не обязательно полезны для классификации (см. Http: //www.bytefish. de / wiki / pca_lda_with_gnu_octave ). Итак, чтобы сохранить некоторую различительную информацию, мы применили линейный дискриминантный анализ и оптимизировали, как описано в методе Fisherfaces. Метод Fisherfaces отлично сработал… по крайней мере, для сценария с ограничениями, который мы использовали в нашей модели.

Настоящая жизнь не идеальна. Вы просто не можете гарантировать идеальные настройки освещения на своих изображениях или 10 разных изображениях человека. А что, если у каждого человека есть только одно изображение? Наши оценки ковариации для подпространства могут быть ужасно неверными, как и распознавание. Помните, у метода Eigenfaces уровень распознавания в базе данных Facedatabase AT&T составлял 96%? Сколько изображений нам действительно нужно, чтобы получить такие полезные оценки? Вот коэффициенты распознавания Rank-1 метода Eigenfaces и Fisherfaces в базе данных Facedatabase AT&T, которая представляет собой довольно простую базу данных изображений:

Таким образом, чтобы получить хорошие показатели распознавания, вам понадобится как минимум 8 (+ - 1) изображений для каждого человека, и метод Fisherfaces здесь не очень помогает. Вышеупомянутый эксперимент представляет собой результат 10-кратной перекрестной проверки, проведенный с помощью фреймворка facerec по адресу: https://github.com/bytefish/facerec. Это не публикация, поэтому я не буду подкреплять эти цифры глубоким математическим анализом. Пожалуйста, загляните в [KM01] для подробного анализа обоих методов, когда дело касается небольших обучающих наборов данных.

Поэтому некоторые исследования были сосредоточены на извлечении локальных особенностей из изображений. Идея состоит в том, чтобы не рассматривать все изображение как многомерный вектор, а описывать только локальные особенности объекта. Объекты, которые вы извлекаете таким образом, будут неявно иметь низкую размерность. Замечательная идея! Но вскоре вы заметите, что представление изображения, которое мы получаем, страдает не только от вариаций освещения. Подумайте о таких вещах, как масштаб, перевод или поворот изображений - ваше локальное описание должно хотя бы немного соответствовать этим вещам. Как и SIFT, методология Local Binary Patterns берет свое начало в анализе 2D текстур. Основная идея локальных двоичных паттернов состоит в том, чтобы обобщить локальную структуру изображения путем сравнения каждого пикселя с его окрестностями. Возьмите пиксель в качестве центра и ограничьте его соседями. Если интенсивность центрального пикселя больше, чем у его соседа, то обозначьте его 1 и 0, если нет. У вас будет двоичное число для каждого пикселя, например 11001111. Таким образом, с 8 окружающими пикселями вы получите 2⁸ возможных комбинаций, называемых локальными двоичными шаблонами или иногда называемыми Коды LBP. Первый оператор LBP, описанный в литературе, фактически использовал фиксированную окрестность 3 x 3 примерно так:

Гистограммы локальных двоичных паттернов в OpenC V

/*
 * Copyright (c) 2011. Philipp Wagner <bytefish[at]gmx[dot]de>.
 * Released to public domain under terms of the BSD Simplified license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the organization nor the names of its contributors
 *     may be used to endorse or promote products derived from this software
 *     without specific prior written permission.
 *
 *   See <http://www.opensource.org/licenses/bsd-license>
 */
#include "opencv2/core/core.hpp"
#include "opencv2/contrib/contrib.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <fstream>
#include <sstream>
using namespace cv;
using namespace std;
static void read_csv(const string& filename, vector<Mat>& images, vector<int>& labels, char separator = ';') {
    std::ifstream file(filename.c_str(), ifstream::in);
    if (!file) {
        string error_message = "No valid input file was given, please check the given filename.";
        CV_Error(CV_StsBadArg, error_message);
    }
    string line, path, classlabel;
    while (getline(file, line)) {
        stringstream liness(line);
        getline(liness, path, separator);
        getline(liness, classlabel);
        if(!path.empty() && !classlabel.empty()) {
            images.push_back(imread(path, 0));
            labels.push_back(atoi(classlabel.c_str()));
        }
    }
}
int main(int argc, const char *argv[]) {
    // Check for valid command line arguments, print usage
    // if no arguments were given.
    if (argc != 2) {
        cout << "usage: " << argv[0] << " <csv.ext>" << endl;
        exit(1);
    }
    // Get the path to your CSV.
    string fn_csv = string(argv[1]);
    // These vectors hold the images and corresponding labels.
    vector<Mat> images;
    vector<int> labels;
    // Read in the data. This can fail if no valid
    // input filename is given.
    try {
        read_csv(fn_csv, images, labels);
    } catch (cv::Exception& e) {
        cerr << "Error opening file \"" << fn_csv << "\". Reason: " << e.msg << endl;
        // nothing more we can do
        exit(1);
    }
    // Quit if there are not enough images for this demo.
    if(images.size() <= 1) {
        string error_message = "This demo needs at least 2 images to work. Please add more images to your data set!";
        CV_Error(CV_StsError, error_message);
    }
    // Get the height from the first image. We'll need this
    // later in code to reshape the images to their original
    // size:
    int height = images[0].rows;
    // The following lines simply get the last images from
    // your dataset and remove it from the vector. This is
    // done, so that the training data (which we learn the
    // cv::FaceRecognizer on) and the test data we test
    // the model with, do not overlap.
    Mat testSample = images[images.size() - 1];
    int testLabel = labels[labels.size() - 1];
    images.pop_back();
    labels.pop_back();
    // The following lines create an LBPH model for
    // face recognition and train it with the images and
    // labels read from the given CSV file.
    //
    // The LBPHFaceRecognizer uses Extended Local Binary Patterns
    // (it's probably configurable with other operators at a later
    // point), and has the following default values
    //
    //      radius = 1
    //      neighbors = 8
    //      grid_x = 8
    //      grid_y = 8
    //
    // So if you want a LBPH FaceRecognizer using a radius of
    // 2 and 16 neighbors, call the factory method with:
    //
    //      cv::createLBPHFaceRecognizer(2, 16);
    //
    // And if you want a threshold (e.g. 123.0) call it with its default values:
    //
    //      cv::createLBPHFaceRecognizer(1,8,8,8,123.0)
    //
    Ptr<FaceRecognizer> model = createLBPHFaceRecognizer();
    model->train(images, labels);
    // The following line predicts the label of a given
    // test image:
    int predictedLabel = model->predict(testSample);
    //
    // To get the confidence of a prediction call the model with:
    //
    //      int predictedLabel = -1;
    //      double confidence = 0.0;
    //      model->predict(testSample, predictedLabel, confidence);
    //
    string result_message = format("Predicted class = %d / Actual class = %d.", predictedLabel, testLabel);
    cout << result_message << endl;
    // Sometimes you'll need to get/set internal model data,
    // which isn't exposed by the public cv::FaceRecognizer.
    // Since each cv::FaceRecognizer is derived from a
    // cv::Algorithm, you can query the data.
    //
    // First we'll use it to set the threshold of the FaceRecognizer
    // to 0.0 without retraining the model. This can be useful if
    // you are evaluating the model:
    //
    model->set("threshold", 0.0);
    // Now the threshold of this model is set to 0.0. A prediction
    // now returns -1, as it's impossible to have a distance below
    // it
    predictedLabel = model->predict(testSample);
    cout << "Predicted class = " << predictedLabel << endl;
    // Show some informations about the model, as there's no cool
    // Model data to display as in Eigenfaces/Fisherfaces.
    // Due to efficiency reasons the LBP images are not stored
    // within the model:
    cout << "Model Information:" << endl;
    string model_info = format("\tLBPH(radius=%i, neighbors=%i, grid_x=%i, grid_y=%i, threshold=%.2f)",
            model->getInt("radius"),
            model->getInt("neighbors"),
            model->getInt("grid_x"),
            model->getInt("grid_y"),
            model->getDouble("threshold"));
    cout << model_info << endl;
    // We could get the histograms for example:
    vector<Mat> histograms = model->getMatVector("histograms");
    // But should I really visualize it? Probably the length is interesting:
    cout << "Size of the histograms: " << histograms[0].total() << endl;
    return 0;
}

"Вывод"

Вы узнали, как использовать новый FaceRecognizer в реальных приложениях. Прочитав документ, вы также узнаете, как работают алгоритмы, так что теперь пришло время поэкспериментировать с доступными алгоритмами. Используйте их, улучшайте и позвольте сообществу OpenCV участвовать!

Выравнивание изображений лиц

Точное выравнивание данных вашего изображения особенно важно в таких задачах, как обнаружение эмоций, когда вам нужно как можно больше деталей. Поверьте ... Вы же не хотите делать это вручную. Итак, я приготовил вам крошечный скрипт Python. Код действительно прост в использовании. Чтобы масштабировать, повернуть и обрезать изображение лица, вам просто нужно вызвать CropFace (image, eye_left, eye_right, offset_pct, dest_sz), где:

  • eye_left: положение левого глаза
  • eye_right: положение правого глаза
  • offset_pct: процент изображения, который вы хотите оставить рядом с глазами (горизонтальное, вертикальное направление).
  • dest_sz - размер выходного изображения.

Если вы используете одинаковые offset_pct и dest_sz для своих изображений, все они будут выровнены по глазам.

#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2012, Philipp Wagner
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following
#    disclaimer in the documentation and/or other materials provided
#    with the distribution.
#  * Neither the name of the author nor the names of its
#    contributors may be used to endorse or promote products derived
#    from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import sys, math, Image
def Distance(p1,p2):
  dx = p2[0] - p1[0]
  dy = p2[1] - p1[1]
  return math.sqrt(dx*dx+dy*dy)
def ScaleRotateTranslate(image, angle, center = None, new_center = None, scale = None, resample=Image.BICUBIC):
  if (scale is None) and (center is None):
    return image.rotate(angle=angle, resample=resample)
  nx,ny = x,y = center
  sx=sy=1.0
  if new_center:
    (nx,ny) = new_center
  if scale:
    (sx,sy) = (scale, scale)
  cosine = math.cos(angle)
  sine = math.sin(angle)
  a = cosine/sx
  b = sine/sx
  c = x-nx*a-ny*b
  d = -sine/sy
  e = cosine/sy
  f = y-nx*d-ny*e
  return image.transform(image.size, Image.AFFINE, (a,b,c,d,e,f), resample=resample)
def CropFace(image, eye_left=(0,0), eye_right=(0,0), offset_pct=(0.2,0.2), dest_sz = (70,70)):
  # calculate offsets in original image
  offset_h = math.floor(float(offset_pct[0])*dest_sz[0])
  offset_v = math.floor(float(offset_pct[1])*dest_sz[1])
  # get the direction
  eye_direction = (eye_right[0] - eye_left[0], eye_right[1] - eye_left[1])
  # calc rotation angle in radians
  rotation = -math.atan2(float(eye_direction[1]),float(eye_direction[0]))
  # distance between them
  dist = Distance(eye_left, eye_right)
  # calculate the reference eye-width
  reference = dest_sz[0] - 2.0*offset_h
  # scale factor
  scale = float(dist)/float(reference)
  # rotate original around the left eye
  image = ScaleRotateTranslate(image, center=eye_left, angle=rotation)
  # crop the rotated image
  crop_xy = (eye_left[0] - scale*offset_h, eye_left[1] - scale*offset_v)
  crop_size = (dest_sz[0]*scale, dest_sz[1]*scale)
  image = image.crop((int(crop_xy[0]), int(crop_xy[1]), int(crop_xy[0]+crop_size[0]), int(crop_xy[1]+crop_size[1])))
  # resize it
  image = image.resize(dest_sz, Image.ANTIALIAS)
  return image
if __name__ == "__main__":
  image =  Image.open("arnie.jpg")
  CropFace(image, eye_left=(252,364), eye_right=(420,366), offset_pct=(0.1,0.1), dest_sz=(200,200)).save("arnie_10_10_200_200.jpg")
  CropFace(image, eye_left=(252,364), eye_right=(420,366), offset_pct=(0.2,0.2), dest_sz=(200,200)).save("arnie_20_20_200_200.jpg")
  CropFace(image, eye_left=(252,364), eye_right=(420,366), offset_pct=(0.3,0.3), dest_sz=(200,200)).save("arnie_30_30_200_200.jpg")
  CropFace(image, eye_left=(252,364), eye_right=(420,366), offset_pct=(0.2,0.2)).save("arnie_20_20_70_70.jpg")

Представьте, что нам дали эту фотографию Арнольда Шварценеггера, которая находится под лицензией Public Domain. Положение (x, y) глаз составляет примерно (252,364) для левого глаза и (420,366) для правого глаза. Теперь вам нужно только определить смещение по горизонтали, вертикали и размер вашего масштабированного, повернутого и обрезанного лица.

Вот некоторые примеры:

Конфигурация Обрезанная, масштабированная, повернутая грань 0,1 (10%), 0,1 (10%), (200 200)

0.2 (20%), 0.2 (20%), (200,200)

0.3 (30%), 0.3 (30%), (200,200)

0.2 (20%), 0.2 (20%), (70,70)