В этом сообщении в блоге я объясню, как построить алгоритм распознавания лиц с помощью компонентов машинного обучения в OpenCV. Мы будем использовать OpenCV для чтения изображения с камеры и обнаружения на нем лиц. Результат будет выглядеть следующим образом.

Вы можете найти весь код для этого поста в блоге на моем github.

Установка OpenCV

Мы будем использовать некоторые довольно новые части OpenCV и его модуль OpenCV_contrib. Самый удобный способ убедиться, что у вас есть доступ к этим модулям, — собрать OpenCV из исходного кода. Я использовал OpenCV версии 4.2.0 на Ubuntu 16.04. Для вашего удобства я включил скрипт bash, который позаботится об установке правильной версии OpenCV. Он также установит все необходимые зависимости. Скрипт лежит в сопутствующем репозитории GitHub.

Класс cv::dnn::Net, который мы будем использовать, был добавлен в OpenCV в версии 3.4.10, поэтому более ранние версии также могут работать. Но я не проверял это.

Настройка CMake

Мы создадим наш код с помощью CMake. Для этого мы создаем проект CMake с одним исполняемым файлом и устанавливаем стандарт C++ на 14.

cmake_minimum_required(VERSION 3.0) 
project(OpenCVFaceDetector LANGUAGES CXX)  
add_executable(${PROJECT_NAME} main.cpp) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_14) target_include_directories(${PROJECT_NAME} PRIVATE include)

Затем мы позаботимся о зависимости OpenCV. Мы находим пакет OpenCV и связываем с ним наш исполняемый файл.

# OpenCV setup 
find_package(OpenCV REQUIRED) 
target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS})

Весь файл CMakeLists.txt должен выглядеть так.

cmake_minimum_required(VERSION 3.0) 
project(OpenCVFaceDetector LANGUAGES CXX)  
add_executable(${PROJECT_NAME} main.cpp) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_14) target_include_directories(${PROJECT_NAME} PRIVATE include)  
# OpenCV setup find_package(OpenCV REQUIRED) target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS})

Получение изображения с камеры

Первое, что нам нужно сделать, это получить изображение с камеры для работы. К счастью, класс cv::videocapture упрощает эту задачу.

Мы включаем заголовок OpenCV, чтобы иметь доступ к функциям OpenCV. Далее мы создаем объект cv::videocapture и пытаемся открыть первую попавшуюся камеру.

#include <opencv4/opencv2/opencv.hpp>  
int main(int argc, char **argv) {
      cv::VideoCapture video_capture;
     if (!video_capture.open(0)) {
         return 0;
     }

После этого мы создаем cv::Mat для хранения кадра и отображения его в бесконечном цикле. Если пользователь нажимает «Esc», мы разрываем цикл, уничтожаем окно дисплея и освобождаем захват видео.

cv::Mat frame;
    while (true) {
        video_capture >> frame;

        imshow("Image", frame);
        const int esc_key = 27;
        if (cv::waitKey(10) == esc_key) { 
            break;
        }
    }

    cv::destroyAllWindows();
    video_capture.release();

    return 0;
}

Пока что файл main.cpp будет выглядеть следующим образом.

#include <opencv4/opencv2/opencv.hpp>  
int main(int argc, char **argv) {
      cv::VideoCapture video_capture;
     if (!video_capture.open(0)) {
         return 0;
     }
     cv::Mat frame;
     while (true) {
         video_capture >> frame;
         imshow("Image", frame);
         const int esc_key = 27;
         if (cv::waitKey(10) == esc_key) {
              break;
         }
     }
     cv::destroyAllWindows();
     video_capture.release();
     return 0;
 }

Теперь мы можем отображать изображения, снятые с камеры. 😀

Использование класса cv:dnn::Net для загрузки предварительно обученной сети обнаружения лиц SSD

Теперь приступим к сборке детектора лиц. Мы используем класс cv::dnn::Net и загружаем веса из предварительно обученной модели кафе.

Так как удобно иметь весь функционал в одном месте, мы создаем класс FaceDetector для модели. Итак, сначала мы создаем два новых файла src/FaceDetector.cpp и include/FaceDetector.h. Чтобы убедиться, что наш код все еще собирается, мы добавляем файл реализации в нашу цель CMake. То есть перейдите к своему CMakeLists.txt и измените строку, содержащую add_executable(...), чтобы она выглядела так

add_executable(${PROJECT_NAME} src/main.cpp src/FaceDetector.cpp)

В include/FaceDetector.h мы определяем этот класс. У модели есть конструктор, в который мы будем загружать веса модели. Кроме того, у него есть метод

std::vector<cv::Rect> detect_face_rectangles(const cv::Mat &frame)

который берет входное изображение и дает нам вектор обнаруженных лиц.

#ifndef VISUALS_FACEDETECTOR_H 
#define VISUALS_FACEDETECTOR_H 
#include <opencv4/opencv2/dnn.hpp>  
class FaceDetector { 
public:     
explicit FaceDetector();
/// Detect faces in an image frame 
/// \param frame Image to detect faces in 
/// \return Vector of detected faces     
std::vector<cv::Rect> detect_face_rectangles(const cv::Mat &frame);

Мы сохраняем фактическую сеть в частной переменной-члене. Помимо модели, мы также сохраним

  • input_image_width/height_ размеры входного изображения
  • scale_factor_ коэффициент масштабирования при преобразовании изображения в большой двоичный объект данных
  • mean_values_ средние значения для каждого канала, на котором обучалась сеть. Эти значения будут вычтены из изображения при преобразовании изображения в большой двоичный объект данных.
  • confidence_threshold_ порог достоверности для использования при обнаружении лиц. Модель предоставит значение достоверности для каждого обнаруженного лица. Лица со значением достоверности ›= confidence_threshold_ будут сохранены. Все остальные лица отбрасываются.
private:     
/// Face detection network     
cv::dnn::Net network_;     
/// Input image width     
const int input_image_width_;     
/// Input image height     
const int input_image_height_;     
/// Scale factor when creating image blob     
const double scale_factor_;     
/// Mean normalization values network was trained with     
const cv::Scalar mean_values_;     
/// Face detection confidence threshold     
const float confidence_threshold_;  
};  
 
#endif //VISUALS_FACEDETECTOR_H

Полный заголовочный файл находится здесь.

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

#include <sstream> 
#include <vector> 
#include <string> 
#include <FaceDetector.h> 
#include <opencv4/opencv2/opencv.hpp>  
FaceDetector::FaceDetector() :
     confidence_threshold_(0.5),
     input_image_height_(300),
     input_image_width_(300),
     scale_factor_(1.0), 
     mean_values_({104., 177.0, 123.0}) {

Внутри конструктора мы будем использовать cv::dnn::readNetFromCaffe для загрузки модели в нашу переменную network_. cv::dnn::readNetFromCaffe использует два файла для построения модели: первый (deploy.prototxt) — это конфигурация модели, которая описывает архитектуру модели. Второй (res10_300x300_ssd_iter_140000_fp16.caffemodel) — это двоичные данные для весов модели.

Мы могли бы переместить эти файлы в каталог, который содержит наш двоичный файл после сборки. Но это решение довольно хрупкое, потому что ломается при перемещении бинарника. Таким образом, мы передаем местоположение файла через CMake.

Быстрый переход к нашей конфигурации CMake

В этой записи StackOverflow я нашел хороший способ передать путь к файлу на C++. Они рекомендуют передавать путь как compile_definition к цели. Таким образом, CMake может определить правильный путь к файлу и передать его в переменную. Эта переменная будет использоваться в C++.

То есть добавляем в наш CMakeLists.txt следующие строки.

# Introduce preprocessor variables to keep paths of asset files set(FACE_DETECTION_CONFIGURATION
 "${PROJECT_SOURCE_DIR}/assets/deploy.prototxt")
set(FACE_DETECTION_WEIGHTS
 "${PROJECT_SOURCE_DIR}/assets/res10_300x300_ssd_iter_140000_fp16.caffemodel")  
target_compile_definitions(${PROJECT_NAME} PRIVATE  FACE_DETECTION_CONFIGURATION="${FACE_DETECTION_CONFIGURATION}")  
target_compile_definitions(${PROJECT_NAME} PRIVATE  FACE_DETECTION_WEIGHTS="${FACE_DETECTION_WEIGHTS}")

Завершение методов в FaceDetector.cpp

Теперь, когда мы нашли способ доступа к необходимым файлам, мы можем построить модель.

FaceDetector::FaceDetector() :
     confidence_threshold_(0.5),
      input_image_height_(300),
      input_image_width_(300),
     scale_factor_(1.0),
     mean_values_({104., 177.0, 123.0}) {
         // Note: The variables MODEL_CONFIGURATION_FILE
         // and MODEL_WEIGHTS_FILE are passed in via cmake
         network_ = cv::dnn::readNetFromCaffe(FACE_DETECTION_CONFIGURATION,
                 FACE_DETECTION_WEIGHTS);
      if (network_.empty()) {
         std::ostringstream ss;
         ss << "Failed to load network with the following settings:\n"
            << "Configuration: " + std::string(FACE_DETECTION_CONFIGURATION) + "\n"            
            << "Binary: " + std::string(FACE_DETECTION_WEIGHTS) + "\n";
         throw std::invalid_argument(ss.str());
     }

Следующим шагом является реализация detect_face_rectangles. Начнем с преобразования входного изображения в большой двоичный объект данных. Функция cv::dnn::blobFromImage заботится о масштабировании изображения до правильного входного размера для сети. Он также вычитает среднее значение в каждом цветовом канале.

std::vector<cv::Rect> FaceDetector::detect_face_rectangles(const cv::Mat &frame) {
     cv::Mat input_blob = cv::dnn::blobFromImage(frame,
             scale_factor_,
             cv::Size(input_image_width_, input_image_height_),
             mean_values_,
             false,
             false);

После этого мы можем пересылать наши данные по сети. Сохраняем результат в переменной detection_matrix.

     network_.setInput(input_blob, "data");
     cv::Mat detection = network_.forward("detection_out");
     cv::Mat detection_matrix(detection.size[2],
             detection.size[3],
             CV_32F,
             detection.ptr<float>());

Перебираем строки матрицы. Каждая строка содержит одно обнаружение. Во время итерации мы проверяем, превышает ли значение достоверности наш порог. Если это так, мы создаем cv::Rect и сохраняем его в результирующем векторе faces.

std::vector<cv::Rect> faces;

    for (int i = 0; i < detection_matrix.rows; i++) {
        float confidence = detection_matrix.at<float>(i, 2);

        if (confidence < confidence_threshold_) {
            continue;
        }
        int x_left_bottom = static_cast<int>(
                detection_matrix.at<float>(i, 3) * frame.cols);

        int y_left_bottom = static_cast<int>(
                detection_matrix.at<float>(i, 4) * frame.rows);

        int x_right_top = static_cast<int>(
                detection_matrix.at<float>(i, 5) * frame.cols);

        int y_right_top = static_cast<int>(
                detection_matrix.at<float>(i, 6) * frame.rows);

        faces.emplace_back(x_left_bottom,
                y_left_bottom,
                (x_right_top - x_left_bottom),
                (y_right_top - y_left_bottom));
    }

    return faces;
}

На этом мы завершаем реализацию FaceDetector. Щелкните ссылку эта для полного файла .cpp.

Визуализация обнаруженных лиц

Поскольку мы реализовали детектор лиц как класс, визуализировать прямоугольники несложно. Сначала подключите заголовочный файл FaceDetector.h. Затем мы создаем объект FaceDetector и вызываем метод detect_face_rectangles. Затем мы используем метод rectangle OpenCV, чтобы нарисовать прямоугольник поверх обнаруженных лиц.

#include <opencv4/opencv2/opencv.hpp> 
#include "FaceDetector.h"
int main(int argc, char **argv) {
      cv::VideoCapture video_capture;
     if (!video_capture.open(0)) {
         return 0;
     }
      FaceDetector face_detector;
      cv::Mat frame;
     while (true) {
         video_capture >> frame;
         auto rectangles = face_detector.detect_face_rectangles(frame);
         cv::Scalar color(0, 105, 205);
         int frame_thickness = 4;
         for(const auto & r : rectangles){
             cv::rectangle(frame, r, color, frame_thickness);
         }
         imshow("Image", frame);
         const int esc_key = 27;
         if (cv::waitKey(10) == esc_key) {
             break;
         }
     }
     cv::destroyAllWindows();
     video_capture.release();
     return 0;
 }

Если мы запустим это, мы увидим прямоугольник вокруг лица Бетховена!

Заворачивать

На этом мы завершаем наш пост об распознавании лиц в OpenCV. Мы увидели, как мы можем захватить изображение с камеры и найти в нем лица, используя предварительно обученную сеть SSD в OpenCV.

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

Подпишитесь на меня в Твиттере @bewagner_, чтобы узнать больше о программировании, машинном обучении и C++!

Первоначально опубликовано на https://bewagner.github.io.