Страницы

Поиск по вопросам

четверг, 2 января 2020 г.

Как снизить потребление ресурсов в процессе детекции лица?

#cpp #opencv #opencv_faq


С помощью класса CascadeClassifier, а в частности его метода detectMultiScale() осуществляется
поиск лица в кадре. В целом всё устраивает, кроме повышенного потребления вычислительных
ресурсов при в общем-то стандартной кадровой частоте в 30 кадров/сек.

Можно было бы попробовать применить cuda::CascadeClassifier, но останавливает соответствующее
требование к железу, плюс особое внимание к сборке как OpenCV, так и самого проекта
в виду необходимости использования дополнительных библиотек.

Метод detectMultiScale() принимает на вход параметры, которыми можно снизить нагрузку.
Например, значения minSize и maxSize ограничивают допустимый размер лица в пикселях.
Регулировка этих параметров избавляет алгоритм от необходимости учитывать все возможные
вариации размеров, определяемых с шагом scaleFactor. minNeighbors в свою очередь позволяет
избавиться от т.н. false positive срабатываний алгоритма, которые чаще всего указывают
на область, не являющуюся лицом. Однако всё это во многом сводится на нет, если требуется
ловить лицо как вблизи камеры, так и на некотором удалении от оной. То есть в широком
диапазоне размеров.

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

// Объект _classifier типа CascadeClassifier
// уже загружен соответствующим xml-файлом классификации.

// _scale_factor, _min_neighbors, _min_size, _max_size
// также инициализируются заранее в качестве атрибутов класса Detector.

bool Detector::run(const cv::Mat &src_mat, std::vector &rects) {
    if(src_mat.empty() || _classifier.empty())
        return false;

    cv::Mat gry_mat;
    switch(src_mat.channels()) {
       case 1: gry_mat = src_mat.clone(); break;
       case 3: cv::cvtColor(src_mat, gry_mat, cv::COLOR_BGR2GRAY); break;
       default: return false;
    }

    int pyr_cnt = 0;
    while(gry_mat.cols > 512 || gry_mat.rows > 384) {
        cv::pyrDown(gry_mat, gry_mat); ++pyr_cnt;
    }

    _classifier.detectMultiScale(gry_mat, rects
        , _scale_factor, _min_neighbors
        , cv::CASCADE_FIND_BIGGEST_OBJECT
        , _min_size, _max_size);

    if(rects.empty()) return true;

    if(pyr_cnt > 0) {
        for(int i = 0, n = rects.size(); i < n; ++i) {
            cv::Rect &rc = rects[i];

            int cnt = pyr_cnt;
            while(cnt--) {
                rc.x *= 2; rc.width  *= 2;
                rc.y *= 2; rc.height *= 2;
            }
        }
    }

    return true;
}


Строка while(gry_mat.cols > 512 || gry_mat.rows > 384) {...} определяет, сколько
раз нужно уменьшать кадр в два раза. В общем-то, это от "балды" и подставить можно
любые значения, нежели чем 512х384, например, в зависимости от предполагаемой удалённости
объекта от видеокамеры.

Функция pyrDown(), уменьшающая кадр ровно в два раза, использована по причине того,
что отработает быстрее, нежели чем просто задействовать обычно используемую в подобных
случаях cv::resize().

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

Каким ещё способом можно попытаться решить проблему?
    


Ответы

Ответ 1



Можно использовать оптический поток. Итеративный алгоритм Лукаса-Канаде работает быстро, а с учётом того, что для задачи трекинга лица не требуется большое количество точек интереса, то скорость обработки каждого кадра будет очень высокой. Действительно, если лицо обнаружено классификатором каскада, то почему бы далее не использовать возможности трекинга с тем, чтобы отследить местоположение объекта интереса на последующих кадрах. Таким образом, всего лишь совместив детекцию и трекинг, получим значительный прирост в производительности. Важно учесть один момент: детектор всегда выдаёт прямоугольную область, включающую лицо, но никогда обрамляющую её. Это означает, что прежде чем начать искать точки интереса на области лица, эту самую область необходимо уменьшить с тем, чтобы на фон, который безусловно будет виден в оригинальном прямоугольнике, не попало ни одной точки интереса. Для устранения препоны можно использовать разные методы, а можно просто уменьшить область интереса, сообразуясь с пропорциями ширины и высоты лица. Собственно, для трекинга будет вполне достаточно использовать небольшого размера прямоугольник, полностью размещающийся внутри области лица: cv::Rect Tracker::getFaceSubRoi(const cv::Rect &face_roi) { cv::Rect sub_roi; sub_roi.x = face_roi.x + face_roi.width * 0.3; sub_roi.y = face_roi.y + face_roi.height * 0.05; sub_roi.width = face_roi.width - face_roi.width * 0.6; sub_roi.height = face_roi.height - face_roi.height * 0.8; return sub_roi; } В данном примере коэффициенты подобраны опытным путём для выделения области лба. Можно выбрать свои. Главное, повторюсь, это отсутствие видимости фона за лицом. Далее необходимо организовать поиск точек интереса на лице, только что обнаруженном детектором, с учётом уменьшенной области интереса: // Исходный кадр в оттенках серого. cv::Mat gry_mat = ... // Создаём матрицу маски размером с исходный кадр // и закрашиваем её в уменьшенной области интереса белым цветом. cv::Mat msk_mat = cv::Mat::zeros(gry_mat.size(), CV_8U); cv::rectangle(msk_mat, sub_roi, cv::Scalar(255), -1); // Максимально допустимое кол-во точек интереса. // Сотни штук обычно за глаза. const int max_num_pnts = 100; // Минимально допустимое кол-во точек интереса. const int min_num_pnts = 10; // Находим точки на кадре в области, ограниченной маской. // Прочие параметры устанавливаем по желанию и ситуации. std::vector prv_pnts; cv::goodFeaturesToTrack(gry_mat, prv_pnts, max_num_pnts, 0.001, 5.0, msk_mat, 3); cv::cornerSubPix(gry_mat, prv_pnts, cv::Size(10,10), cv::Size(-1,-1) , cv::TermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,50,0.0001)); // Бывают случаи, когда количество обнаруженных точек меньше допустимого. // Тогда со следующего кадра можно опять начать с детекции. if((int)prv_pnts.size() < min_num_pnts) { /* Ругнуться и выйти */ } // Остаётся построить пирамиду изображений. std::vector prv_pyr; cv::buildOpticalFlowPyramid(gry_mat, prv_pyr, cv::Size(21,21), 3, true , cv::BORDER_REFLECT_101, cv::BORDER_CONSTANT, true); Теперь имеется всё необходимое, чтобы на следующем кадре производить уже не детекцию, а трекинг. while(true) { // Читаем кадр в оттенках серого. cv::Mat gry_mat = ... // Строим пирамиду изображений для следующего кадра. std::vector nxt_pyr; cv::buildOpticalFlowPyramid(gry_mat, nxt_pyr, cv::Size(21,21), 3, true , cv::BORDER_REFLECT_101, cv::BORDER_CONSTANT, true); // Вычисляем оптический поток, используя "prv_pyr" и "prv_pnts", // полученные ранее. std::vector statuses; std::vector errors; std::vector nxt_pnts; cv::calcOpticalFlowPyrLK(prv_pyr, nxt_pyr, prv_pnts, nxt_pnts , statuses, errors, cv::Size(21,21), 3 , cv::TermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,50,0.0001) , cv::OPTFLOW_LK_GET_MIN_EIGENVALS, 0.0005); // Векторы для хранения смещения каждой из точек интереса по осям. std::vector hrz_offset, vrt_offset; const int n = nxt_pnts.size(); hrz_offset.resize(n); vrt_offset.resize(n); int k = 0; for(int i = 0; i < n; ++i) { if(!statuses.at(i)) continue; const cv::Point2f &prv_pnt = prv_pnts.at(i); const cv::Point2f &nxt_pnt = nxt_pnts.at(i); // Если точки вышли за границы области интереса, // то не используем их. if(nxt_pnt.x < sub_roi.x || nxt_pnt.x > sub_roi.br().x) continue; if(nxt_pnt.y < sub_roi.y || nxt_pnt.y > sub_roi.br().y) continue; hrz_offset[k] = nxt_pnt.x-prv_pnt.x; vrt_offset[k] = nxt_pnt.y-prv_pnt.y; nxt_pnts[k++] = nxt_pnt; } nxt_pnts.resize(k); hrz_offset.resize(k); vrt_offset.resize(k); std::swap(prv_pnts, nxt_pnts); std::swap(prv_pyr, nxt_pyr); if((int)prv_pnts.size() < min_num_pnts) { /* Активировать детектор или выполнить поиск точек заново */ continue; } const double &avg_hrz_offset = calculateAverageOffset(hrz_offset); const double &avg_vrt_offset = calculateAverageOffset(vrt_offset); } Особенность вышеприведённого цикла for(int i = 0; i < n; ++i) {...} в том, что он отсеивает те точки интереса, которые в силу различных условий перестали быть актуальными. Например по той причине, что алгоритм Лукаса-Канаде для каких-либо из них не смог провести соответствия между предыдущим и следующим кадром. Постепенно количество ранее обнаруженных точек будет уменьшаться и когда достигнет минимального значения min_num_pnts потребуется снова активировать детектор или выполнить поиск точек интереса заново. Под конец цикла while(true) {...} вызывается функция calculateAverageOffset(), которая может просто вычислять среднее значение вектора, поданного ей на вход, либо, например, по пику в распределении Гаусса брать из вектора определённое значение. Рассмотренный код функционирует на порядок быстрее, нежели если производить детекцию лица на каждом кадре. Однако у него имеются и отрицательные стороны. Голова - это трёхмерный объект, а значит сильные повороты и наклоны могут приводить к не вполне ожидаемым результатам. Также необходимо помнить о том, что оптический поток плохо работает на смещениях, скорость которых слишком высока по отношению к скорости видеозахвата. Иными словами, если объект будет резко мотать головой при стандартных 30 кадрах/сек, то трекинг собъётся и результат окажется неприемлемым. Напоследок хочется добавить, что подход вовсе не ограничен применением одного только Лукаса-Канаде. Он в той же мере может быть использован, например, с алгоритмом Фарнебэка, реализация которого в OpenCV также имеется. Или, например, можно использовать трекинг по корреляции фазы, вопреки её традиционному использованию в основном в задачах стабилизации видео.

Комментариев нет:

Отправить комментарий