Отслеживание объектов в видео с использованием OpenCV

C++C++Beginner
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом лабе мы реализуем трекинг объектов на видео с использованием OpenCV.
Вы должны пройти курс "Строительство Солнечной системы на C++" перед изучением этого проекта.

Что нужно изучить

  • Основы C++
  • Основы g++
  • Представление изображений
  • Применение OpenCV
  • Алгоритмы Meanshift и Camshift

Финальные результаты

В этом эксперименте будет реализована программа, которая может отслеживать планеты в Солнечной системе. (На следующем изображении мы выбрали Юпитер по желтой орбите, и вы можете увидеть, что отслеживаемый объект отмечен красной.eclipse):

image desc

Прежде чем писать этот проект, вы должны завершить наш курс "Строительство Солнечной системы на C++".

Создание видеофайла

В среде LabEx мы не поддерживаем камеру. Поэтому нам нужно создать видеофайл для нашего проекта.

Установим инструмент записи видео:

sudo apt-get update && sudo apt-get install gtk-recordmydesktop

После установки мы можем найти программу записи в меню приложений:

image desc

Затем вы можете запустить программу Солнечная система ./solarsystem и использовать RecordMyDesktop для записи экрана рабочего стола (10 - 30 секунд будет достаточно), и сохранить ее в ~/Code/camshift с именем video:

image desc

Когда вы хотите закончить запись, вы можете нажать кнопку Стоп в нижнем правом углу. Затем вы получите файл video.ogv:

image desc

Основы цифровых изображений

OpenCV - это библиотека компьютерного зрения, которая является открытым и кроссплатформенной. В отличие от рендеринга изображений в OpenGL, OpenCV реализует многие общие алгоритмы для обработки изображений и компьютерного зрения. Перед изучением OpenCV нам нужно понять некоторые базовые концепции изображений и видео в компьютере.

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

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

Более распространенным является пиксельная карта. Например, размер изображения часто составляет 1024 * 768. Это означает, что изображение имеет 1024 пикселей по горизонтали и 768 пикселей по вертикали.

Пиксель - это базовая единица пиксельной карты. Обычно пиксель является смесью трех основных цветов (красный, зеленый и синий). Поскольку природа компьютера - это распознавание чисел, обычно мы представляем основной цвет по яркости от 0 до 255. Другими словами, для основного красного цвета 0 означает самый темный, то есть черный, а 255 означает самый яркий, то есть чистый красный.

Таким образом, пиксель может быть представлен в виде тройки (R,G,B), так что белый может быть представлен как (255,255,255), а черный - как (0,0,0). Затем мы называем это изображение изображением в цветном пространстве RGB. R, G и B становятся тремя каналами изображения; и существует много других цветовых пространств, помимо цветового пространства RGB, таких как HSV, YCrCb и т.д.

Как пиксель относится к пиксельной карте, так изображение является базовой единицей видео. Видео состоит из серии изображений, в которых каждое изображение называется кадром. А то, что мы обычно называем частотой кадров видео, означает, что это видео содержит такое количество кадровых изображений в секунду. Например, если частота кадров равна 25, то это видео будет воспроизводить 25 кадров в секунду.

Если в 1 секунде 1000 миллисекунд, и пусть частота кадров равна rate, то интервал времени между кадровыми изображениями равен 1000/rate.

Цветовая гистограмма изображения

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

Если изображение находится в цветном пространстве RGB, то мы можем подсчитать количество вхождений каждого цвета в канале R. Таким образом, мы можем получить массив длиной 256 (таблицу поиска вероятностей цветов). Разделить все значения одновременно на общее количество пикселей (ширина умноженная на высоту) в изображении и преобразовать полученную последовательность в гистограмму. Результат - это цветовая гистограмма канала R. По аналогии можно получить гистограммы в канале G и канале B.

Обратная проекция гистограммы

Было доказано, что в цветовом пространстве RGB гистограмма чувствительна к изменениям освещения. Чтобы уменьшить влияние этого изменения на качество трекинга, гистограмма должна быть обратно проецирована. Этот процесс делится на три этапа:

  1. Во - первых, мы преобразуем изображение из цветового пространства RGB в цветовое пространство HSV.
  2. Затем мы вычисляем гистограмму канала H.
  3. Значение каждого пикселя в изображении заменяется соответствующей вероятностью в таблице поиска вероятностей цветов, чтобы получить карту распределения вероятностей цветов.

Этот процесс называется обратной проекцией, а карта распределения вероятностей цветов - это черно - белое изображение.

Основы OpenCV

Сначала нам нужно установить OpenCV:

sudo apt update
sudo apt-get install libopencv-dev

Предполагается, что вы уже знаете базовый синтаксис C++. Вы знаете, что практически каждый программа будет использовать заголовочный файл #include <iostream> и using namespace std; или std::cout. OpenCV также имеет свою собственную пространство имен.

Для использования OpenCV нам нужно только включить следующий заголовочный файл:

#include <opencv2/opencv.hpp>

Затем:

using namespace cv;

чтобы включить пространство имен OpenCV (или напрямую использовать префикс cv:: для всех API).

Это ваш первый опыт использования OpenCV, и вы, возможно, не знакомы с интерфейсами OpenCV, поэтому мы рекомендуем использовать префикс cv:: для изучения API OpenCV.

Напишем нашу первую программу для чтения нашего записанного видео:

//
// main.cpp
//
#include <opencv2/opencv.hpp> // OpenCV head file

int main() {

    // create a video capsure object
    // OpenCV offers VideoCapture object and
    // treat reading video from file as same as reading from camera.
    // when input parameter is a file path, it will read a video file;
    // if it is a identifier number of camera (usually it is 0),
    // it will read the camera
    cv::VideoCapture video("video.ogv"); // reading from file
    // cv::VideoCapture video(0);        // reading from camera

    // container for the reading image frame, Mat object in OpenCV
    // The key class in OpenCV is Mat, which means Matrix
    // OpenCV use matrix to describe images
    cv::Mat frame;
    while(true) {

        // write video data to frame, >> is overwrited by OpenCV
        video >> frame;

        // when there is no frame, break the loop
        if(frame.empty()) break;

        // visualize current frame
        cv::imshow("test", frame);

        // video frame rate is 15, so we need wait 1000/15 for playing smoothly
        // waitKey(int delay) is a waiting function in OpenCV
        // at this point, the program will wait `delay` milsec for keyboard input
        int key = cv::waitKey(1000/15);

        // break the loop when click ECS button on keyboard
        if (key == 27) break;
    }
    // release memory
    cv::destroyAllWindows();
    video.release();
    return 0;

}

Разместите файл main.cpp в той же папке, что и video.ogv в ~/Code/camshift, и скомпилируйте программу:

g++ main.cpp `pkg-config opencv --libs --cflags opencv` -o  main

Когда мы запускаем программу, мы можем увидеть, что видео воспроизводится:

./main
image desc

Примечание

Вы, возможно, увидите следующую ошибку:

libdc1394 error: Failed to initialize libdc1394

Это ошибка из OpenCV и она не влияет на нашу работу.

Если вы хотите устранить проблему, вы можете запустить следующий код перед запуском программы:

sudo ln /dev/null /dev/raw1394

Алгоритмы Meanshift и Camshift

  • Meanshift
  • Camshift
  • Установка события обратного вызова мыши для выбора отслеживаемого объекта
  • Чтение изображения из видеопотока
  • Реализация Camshift

Meanshift

Алгоритмы Meanshift и Camshift - это два классических алгоритма для отслеживания объектов. Camshift основан на Meanshift. Их математическое толкование сложно, но основная идея относительно простая. Поэтому мы пропустим эти математические факты и сначала представим алгоритм Meanshift.

Предположим, что на экране есть набор красных точек. Синий круг (окно) должен быть перемещен в ту область, где наиболее плотная (или где точек больше всего):

image desc

Как показано на изображении выше, отметим синий круг как C1, а центр круга - как C1_o. Однако центроид этого круга - это C1_r, отмеченный как синий实心ный круг.

Когда C1_o и C1_r не совпадают, круг C1 перемещается к центру круга C1_r повторно. В конечном итоге он остановится на круге с самой высокой плотностью C2.

Для обработки изображений мы обычно используем обратно проецированную гистограмму изображения. Когда отслеживаемый объект движется, очевидно, что этот процесс движения может быть отражён в обратно проецированной гистограмме. Таким образом, алгоритм Meanshift в конечном итоге перемещает наше выбранное окно в позицию движущегося объекта. (Алгоритм доказал сходимость в конце.)

Camshift

После предыдущего описания мы увидели, что алгоритм Meanshift всегда отслеживает фиксированный размер окна, что не соответствует нашим потребностям, потому что в видео объект - цель может быть не большой.

Поэтому был создан Camshift для решения этой проблемы. Это также можно увидеть из названия Continuously Adaptive Meanshift для Camshift.

Его основная идея заключается в том, что сначала применяется алгоритм Meanshift. Как только результаты Meanshift сходятся, Camshift обновляет размер окна, вычисляет направленную эллипс для соответствия окну и затем применяет эллипс в качестве нового окна для применения алгоритма Meanshift.

OpenCV предоставляет общий интерфейс к алгоритму Camshift:

RotatedRect CamShift(InputArray probImage, Rect& window, TermCriteria criteria)

Первый параметр, probImage, - это обратная проекция гистограммы цели. Второй параметр, window, - это поисковой окно алгоритма Camshift. Третий параметр, criteria, - это условие окончания (завершения) алгоритма.

Анализ

После того, как мы поняли основную идею алгоритма Camshift, мы можем проанализировать, что реализация этого кода в основном делится на несколько этапов:

  1. Установка события обратного вызова мыши для выбора отслеживаемого объекта.
  2. Чтение изображения из видеопотока.
  3. Реализация процесса Camshift.

Ниже мы продолжаем модифицировать код в main.cpp.

Выбор отслеживаемого объекта с помощью обратного вызова функции мыши

OpenCV отличается от OpenGL. Для обратного вызова функции мыши указаны пять параметров. Три первых параметра - это те, которые нам нужны: с помощью значения event мы можем получить событие нажатия левой кнопки мыши (CV_EVENT_LBUTTONDOWN), событие отпускания левой кнопки мыши (CV_EVENT_LBUTTONUP) и т.д.:

bool selectObject = false; // используется для определения, был ли выбран объект или нет
int trackObject = 0;       // 1 означает, что есть объект для отслеживания, 0 означает, что объекта нет, а -1 означает, что свойства Camshift еще не вычислены
cv::Rect selection;        // сохраняет выбранную область мышью
cv::Mat image;             // кэш изображения из видео

// Обратный вызов функции мыши от OpenCV:
// void onMouse(int event, int x, int y, int flag, void *param)
// четвертый параметр `flag` представляет дополнительное состояние,
// param означает пользовательский параметр, нам они не нужны, поэтому не именованы.
void onMouse( int event, int x, int y, int, void* ) {
    static cv::Point origin;
    if(selectObject) {
        // определение выбранной высоты, ширины и позиции верхнего левого угла
        selection.x = MIN(x, origin.x);
        selection.y = MIN(y, origin.y);
        selection.width = std::abs(x - origin.x);
        selection.height = std::abs(y - origin.y);

        // & перезаписывается cv::Rect
        // это означает пересечение двух областей,
        // главная цель здесь - обработать область за пределами выбранной области
        selection &= cv::Rect(0, 0, image.cols, image.rows);
    }

    switch(event) {
            // обработка нажатия левой кнопки
        case CV_EVENT_LBUTTONDOWN:
            origin = cv::Point(x, y);
            selection = cv::Rect(x, y, 0, 0);
            selectObject = true;
            break;
            // обработка отпускания левой кнопки
        case CV_EVENT_LBUTTONUP:
            selectObject = false;
            if( selection.width > 0 && selection.height > 0 )
                trackObject = -1; // для объекта для отслеживания еще не вычислены свойства Camshift
            break;
    }
}

Чтение изображений из видеопотока

Мы уже реализовали структуру чтения видеопотока. Теперь допишем более подробно:

int main() {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");

    // 1. регистрируем обратный вызов события мыши
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame;

    // 2. читаем изображение из видео
    while(true) {
        video >> frame;
        if(frame.empty()) break;

        // записываем изображение из кадра в глобальную переменную image для кэширования
        frame.copyTo(image);

        // рисуем прямоугольник, если выбираем объект
        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    // освобождаем выделенную память
    cv::destroyAllWindows();
    video.release();
    return 0;
}

Примечание:

ROI (Region of Interest): В обработке изображений любая область, которую нужно обработать, может быть областью интереса, то есть ROI.

Реализация Camshift с использованием OpenCV

Для вычисления обратной проекции гистограммы для отслеживаемого объекта необходимо использовать функцию cvtColor, которая может преобразовать исходное изображение из цветового пространства RGB в цветовое пространство HSV. Вычисление гистограммы должно быть после выбора начальной цели, поэтому:

int main() {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame;
    cv::Mat hsv, hue, mask, hist, backproj;
    cv::Rect trackWindow;             // окно для отслеживания
    int hsize = 16;                   // для гистограммы
    float hranges[] = {0,180};        // для гистограммы
    const float* phranges = hranges;  // для гистограммы

    while(true) {
        video >> frame;
        if(frame.empty()) break;
        frame.copyTo(image);

        // переводим в пространство HSV
        cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
        // обрабатываем, когда есть объект
        if(trackObject) {

            // обрабатываем только H: 0~180, S: 30~256, V: 10~256, фильтруем остальное и копируем оставшуюся часть в маску
            cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 10), mask);
            // отделяем канал h из hsv
            int ch[] = {0, 0};
            hue.create(hsv.size(), hsv.depth());
            cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);

            // извлекаем свойства, если для объекта для отслеживания еще не были вычислены
            if( trackObject < 0 ) {

                // настраиваем канал h и ROI маски
                cv::Mat roi(hue, selection), maskroi(mask, selection);
                // вычисляем гистограмму ROI
                calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
                // нормализация гистограммы
                normalize(hist, hist, 0, 255, CV_MINMAX);

                // настраиваем окно для отслеживания объекта
                trackWindow = selection;

                // отмечаем, что для объекта для отслеживания были вычислены свойства
                trackObject = 1;
            }
            // обратная проекция гистограммы
            calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
            // получаем общую область
            backproj &= mask;
            // вызываем алгоритм Camshift
            cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
            // обрабатываем ситуацию, когда область для рисования слишком мала
            if( trackWindow.area() <= 1 ) {
                int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
                trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
                                       trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
            }
            // рисуем область для отслеживания
            ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );

        }


        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    cv::destroyAllWindows();
    video.release();
    return 0;
}

Обзор

Ниже представлено все, что мы написали в этом проекте:

#include <opencv2/opencv.hpp>

bool selectObject = false; // используется для определения, был ли выбран объект или нет
int trackObject = 0;       // 1 означает, что есть объект для отслеживания, 0 означает, что объекта нет, а -1 означает, что свойства Camshift еще не вычислены
cv::Rect selection;        // сохраняет выбранную область мышью
cv::Mat image;             // кэш изображения из видео

// Обратный вызов функции мыши от OpenCV:
// void onMouse(int event, int x, int y, int flag, void *param)
// четвертый параметр `flag` представляет дополнительное состояние,
// param означает пользовательский параметр, нам они не нужны, поэтому не именованы.
void onMouse( int event, int x, int y, int, void* ) {
    static cv::Point origin;
    if(selectObject) {
        // определение выбранной высоты, ширины и позиции верхнего левого угла
        selection.x = MIN(x, origin.x);
        selection.y = MIN(y, origin.y);
        selection.width = std::abs(x - origin.x);
        selection.height = std::abs(y - origin.y);

        // & перезаписывается cv::Rect
        // это означает пересечение двух областей,
        // главная цель здесь - обработать область за пределами выбранной области
        selection &= cv::Rect(0, 0, image.cols, image.rows);
    }

    switch(event) {
            // обработка нажатия левой кнопки
        case CV_EVENT_LBUTTONDOWN:
            origin = cv::Point(x, y);
            selection = cv::Rect(x, y, 0, 0);
            selectObject = true;
            break;
            // обработка отпускания левой кнопки
        case CV_EVENT_LBUTTONUP:
            selectObject = false;
            if( selection.width > 0 && selection.height > 0 )
                trackObject = -1; // для объекта для отслеживания еще не вычислены свойства Camshift
            break;
    }
}

int main( int argc, const char** argv ) {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame, hsv, hue, mask, hist, backproj;
    cv::Rect trackWindow;             // окно для отслеживания
    int hsize = 16;                   // для гистограммы
    float hranges[] = {0,180};        // для гистограммы
    const float* phranges = hranges;  // для гистограммы

    while(true) {
        video >> frame;
        if(frame.empty()) break;
        frame.copyTo(image);

        // переводим в пространство HSV
        cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
        // обрабатываем, когда есть объект
        if(trackObject) {

            // обрабатываем только H: 0~180, S: 30~256, V: 10~256, фильтруем остальное и копируем оставшуюся часть в маску
            cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 256), mask);
            // отделяем канал h из hsv
            int ch[] = {0, 0};
            hue.create(hsv.size(), hsv.depth());
            cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);

            // извлекаем свойства, если для объекта для отслеживания еще не были вычислены
            if( trackObject < 0 ) {

                // настраиваем канал h и ROI маски
                cv::Mat roi(hue, selection), maskroi(mask, selection);
                // вычисляем гистограмму ROI
                calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
                // нормализация гистограммы
                normalize(hist, hist, 0, 255, CV_MINMAX);

                // настраиваем окно для отслеживания объекта
                trackWindow = selection;

                // отмечаем, что для объекта для отслеживания были вычислены свойства
                trackObject = 1;
            }
            // обратная проекция гистограммы
            calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
            // получаем общую область
            backproj &= mask;
            // вызываем алгоритм Camshift
            cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
            // обрабатываем ситуацию, когда область для рисования слишком мала
            if( trackWindow.area() <= 1 ) {
                int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
                trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
                                       trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
            }
            // рисуем область для отслеживания
            ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );

        }


        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    cv::destroyAllWindows();
    video.release();
    return 0;
}

Пересобираем main.cpp:

g++ main.cpp $(pkg-config opencv --libs --cflags opencv) -o main

и запускаем его:

./main

Теперь мы можем выбрать объект в программе, и процесс отслеживания запущен:

image desc

На изображении выше мы выбрали Юпитера, и окно для отслеживания - это красная эллипс.