В этом лабе мы реализуем трекинг объектов на видео с использованием OpenCV.
Вы должны пройти курс "Строительство Солнечной системы на C++" перед изучением этого проекта.
Что нужно изучить
Основы C++
Основы g++
Представление изображений
Применение OpenCV
Алгоритмы Meanshift и Camshift
Финальные результаты
В этом эксперименте будет реализована программа, которая может отслеживать планеты в Солнечной системе. (На следующем изображении мы выбрали Юпитер по желтой орбите, и вы можете увидеть, что отслеживаемый объект отмечен красной.eclipse):
Прежде чем писать этот проект, вы должны завершить наш курс "Строительство Солнечной системы на C++".
Создание видеофайла
В среде LabEx мы не поддерживаем камеру. Поэтому нам нужно создать видеофайл для нашего проекта.
После установки мы можем найти программу записи в меню приложений:
Затем вы можете запустить программу Солнечная система ./solarsystem и использовать RecordMyDesktop для записи экрана рабочего стола (10 - 30 секунд будет достаточно), и сохранить ее в ~/Code/camshift с именем video:
Когда вы хотите закончить запись, вы можете нажать кнопку Стоп в нижнем правом углу. Затем вы получите файл video.ogv:
Основы цифровых изображений
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 гистограмма чувствительна к изменениям освещения. Чтобы уменьшить влияние этого изменения на качество трекинга, гистограмма должна быть обратно проецирована. Этот процесс делится на три этапа:
Во - первых, мы преобразуем изображение из цветового пространства RGB в цветовое пространство HSV.
Затем мы вычисляем гистограмму канала H.
Значение каждого пикселя в изображении заменяется соответствующей вероятностью в таблице поиска вероятностей цветов, чтобы получить карту распределения вероятностей цветов.
Этот процесс называется обратной проекцией, а карта распределения вероятностей цветов - это черно - белое изображение.
Предполагается, что вы уже знаете базовый синтаксис 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
Примечание
Вы, возможно, увидите следующую ошибку:
libdc1394 error: Failed to initialize libdc1394
Это ошибка из OpenCV и она не влияет на нашу работу.
Если вы хотите устранить проблему, вы можете запустить следующий код перед запуском программы:
sudo ln /dev/null /dev/raw1394
Алгоритмы Meanshift и Camshift
Meanshift
Camshift
Установка события обратного вызова мыши для выбора отслеживаемого объекта
Чтение изображения из видеопотока
Реализация Camshift
Meanshift
Алгоритмы Meanshift и Camshift - это два классических алгоритма для отслеживания объектов. Camshift основан на Meanshift. Их математическое толкование сложно, но основная идея относительно простая. Поэтому мы пропустим эти математические факты и сначала представим алгоритм Meanshift.
Предположим, что на экране есть набор красных точек. Синий круг (окно) должен быть перемещен в ту область, где наиболее плотная (или где точек больше всего):
Как показано на изображении выше, отметим синий круг как 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:
Первый параметр, probImage, - это обратная проекция гистограммы цели. Второй параметр, window, - это поисковой окно алгоритма Camshift. Третий параметр, criteria, - это условие окончания (завершения) алгоритма.
Анализ
После того, как мы поняли основную идею алгоритма Camshift, мы можем проанализировать, что реализация этого кода в основном делится на несколько этапов:
Установка события обратного вызова мыши для выбора отслеживаемого объекта.
Чтение изображения из видеопотока.
Реализация процесса 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
Теперь мы можем выбрать объект в программе, и процесс отслеживания запущен:
На изображении выше мы выбрали Юпитера, и окно для отслеживания - это красная эллипс.