В этом лабе мы реализуем трекинг объектов на видео с использованием 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
Теперь мы можем выбрать объект в программе, и процесс отслеживания запущен:
На изображении выше мы выбрали Юпитера, и окно для отслеживания - это красная эллипс.
We use cookies for a number of reasons, such as keeping the website reliable and secure, to improve your experience on our website and to see how you interact with it. By accepting, you agree to our use of such cookies. Privacy Policy