В этом сообщении в блоге я объясню, как построить алгоритм распознавания лиц с помощью компонентов машинного обучения в 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.