Привет всем. Я хотел бы рассказать о принципах, лежащих в основе распознавания объектов с использованием OpenCV. Благо какое-то время мне довелось поработать в лаборатории компьютерного зрения ВМК МГУ, и я немного вник в премудрости этой ветви computer science. Задача, которую я буду рассматривать здесь, предлагалась на Microsoft Computer Vision School Moscow 2011 на семинарах Виктора Ерухимова, одного из разработчиков программного комплекса OpenCV. Почти в таком же виде рассматриваемый код можно найти в демках OpenCV 2.4.
Формализация задачи
Поставим следующую задачу: на вход подается изображение сцены с обычной USB камеры и изображение целевого плоского объекта (например книга). Задача найти целевой объект на изображении сцены.
Начнем с камеры
Первое, подключим h файлы core.hpp и opencv.hpp, отвечающие за базовые классы opencv и features2d.hpp, — определяющий классы различных детекторов и дескрипторов (нас будет интересовать SURF).
#include <iostream>
#include "opencv2/opencv.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/nonfree/features2d.hpp"
#include <vector>
using namespace std;
using namespace cv;
void readme(string &message)
{
cout << message << endl;
}
Далее начинается тело main, будем считать, что к исполняемому файлу мы передаем 1 параметр, — путь к картинке образцу (плоскому объекту). Конструктор класса VideoCapture принимает на вход номер девайса (камеры), 0 — устройство по умолчанию (вероятно встроенная камера). Далее считывается целевая картинка в img_object.
int main( int argc, char** argv )
{
if(argc != 2)
{
string message = "Использование: ./cv_test <img_object>";
readme(message);
return -1;
}
VideoCapture cap(1); // Открыть камеру (устройство 1). Для открытия встроенной камеры вызывать 0 устройство.
if(!cap.isOpened()) // Проверка корректности отработки
{
string message = "Проверьте камеру или укажите другой номер устройства в коде";
readme(message);
return -1;
}
Mat img_object = imread( argv[1], CV_LOAD_IMAGE_GRAYSCALE );
В бесконечном цикле будем получать очередной кадр с устройства, это есть входное изображение сцены, в которой надо найти целевой объект.
for(;;)
{
Mat frame;
cap >> frame; // Получить очередной фрейм из камеры
Mat img_scene = frame;
if( !img_object.data || !img_scene.data ) // Проверка наличия информации в матрице изображения
{
string message = " Ошибка чтения ";
readme(message);
}
Нахождение ключевых точек
Теперь приступим к распознаванию объекта. Первое, что следует сделать, — это сдетектировать на изображении ключевые точки. Упрощенно можно считать, что это точки в местах резкого перепада градиента на изображении по x и по y (угловые точки). Принцип их определения основывается на использовании автокорреляционной матрицы и пирамиды изображений (для инвариантности к масштабу). Автокорреляционная матрица состоит из производных по x и y по изображению I.
смысл в том, что используемая метрика (лямбды — собственные числа, det — определитель, trace — след матрицы, альфа — константа)
позволяет идентифицировать угловые точки, т.к. в этих точках будут весомые перепады градиентов по x и y, и R будет в локальном максимуме. Задавая параметр minHessian мы определяем порог, по которому будет определяться ключевая ли данная точка или нет.
//-- Этап 1. Нахождение ключевых точек.
int minHessian = 400;
SurfFeatureDetector detector( minHessian );
std::vector<KeyPoint> keypoints_object, keypoints_scene;
detector.detect( img_object, keypoints_object );
detector.detect( img_scene, keypoints_scene );
Нахождение дескрипторов
Далее, необходимо вычислить дескриптор, — вектор кодирующий геометрию локальной окрестности вокруг точки. В основе этого, как правило, лежит SIFT (SURF это быстрый SIFT). Принцип тут следующий:
Патч вокруг данной точки разбивается на детерменированные блоки, в каждом блоке вычисляется доминирующее градиентное направление и магнитуда + осуществляется поворот в сторону доминирующего направления (инвариантность к повороту). Данный «градиентный рисунок» описывает локальный патч.
//-- Этап 2. Вычисление дескрипторов.
SurfDescriptorExtractor extractor;
Mat descriptors_object, descriptors_scene;
extractor.compute( img_object, keypoints_object, descriptors_object );
extractor.compute( img_scene, keypoints_scene, descriptors_scene );
Сравнение дескрипторов
На следующем этапе мы должны «сматчить» вектора дескрипторов, т.е. найти соответствующие точки на целевом объекте и в сцене. Для этой цели можно использовать FlannBasedMatcher (его следует использовать для больших наборов ключевых точек) или BruteForceMatcher (наоборот). Далее мы отбираем из всех сматченных точек только те, расстояние между дескрипторами которых не более 3 * min_dist, где min_dist — минимальное расстояние между дескрипторами.
//-- Этап 3: Необходимо сматчить вектора дескрипторов.
FlannBasedMatcher matcher;
vector< DMatch > matches;
matcher.match( descriptors_object, descriptors_scene, matches );
double max_dist = 0; double min_dist = 100;
//-- Вычисление максимального и минимального расстояния среди всех дескрипторов
// в пространстве признаков
for( int i = 0; i < descriptors_object.rows; i++ )
{
double dist = matches[i].distance;
if( dist < min_dist ) min_dist = dist;
if( dist > max_dist ) max_dist = dist;
}
printf("-- Max dist : %f n", max_dist );
printf("-- Min dist : %f n", min_dist );
//-- Отобрать только хорошие матчи, расстояние меньше чем 3 * min_dist
vector< DMatch > good_matches;
for( int i = 0; i < descriptors_object.rows; i++ )
{
if( matches[i].distance < 3 * min_dist )
{
good_matches.push_back( matches[i]);
}
}
Mat img_matches;
//-- Нарисовать хорошие матчи
drawMatches( img_object, keypoints_object, img_scene, keypoints_scene,
good_matches, img_matches, Scalar::all(-1), Scalar::all(-1),
vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS );
Использование гомографии
В компьютерного зрении любые два изображения одного и того же плоского объекта в пространстве связаны гомографией (если мы используем pin-hole модель камеры). Иными словами это преобразование плоскость — плоскость. Т.е. имея набор точек на целевом объекте и сопоставленный ему набор точек в сцене мы можем найти между ними соответствие в виде матрицы гомографии H (и наоборот соответственно). В основе нахождения этого преобразования лежит алгоритм RANSAC в основе которого лежит итеративная оценка гомографии для случайно выбранных точек (4 на изображении и 4 в сцене).
//-- Локализация объектов
vector<Point2f> obj;
vector<Point2f> scene;
for( int i = 0; i < good_matches.size(); i++ )
{
obj.push_back( keypoints_object[ good_matches[i].queryIdx ].pt );
scene.push_back( keypoints_scene[ good_matches[i].trainIdx ].pt );
}
Mat H = findHomography( obj, scene, CV_RANSAC );
Далее необходимо взять 4 точки по краям целевого объекта и отобразить их с помощью найденного преобразования на изображении сцены. Таким образом, мы найдем bounding box объекта в сцене. Заметьте, что при рисовании линий, к каждой точке мы прибавляем Point2f( img_object.cols, 0), т.к. изображение img_matches предполагает смежное размещение картинки целевого объекта (слева) и сцены (справа).
//-- Получить "углы" изображения с целевым объектом
std::vector<Point2f> obj_corners(4);
obj_corners[0] = cvPoint(0,0); obj_corners[1] = cvPoint( img_object.cols, 0 );
obj_corners[2] = cvPoint( img_object.cols, img_object.rows ); obj_corners[3] = cvPoint( 0, img_object.rows );
std::vector<Point2f> scene_corners(4);
//-- Отобразить углы целевого объекта, используя найденное преобразование, на сцену
perspectiveTransform( obj_corners, scene_corners, H);
//-- Соеденить отображенные углы
line( img_matches, scene_corners[0] + Point2f( img_object.cols, 0), scene_corners[1] + Point2f( img_object.cols, 0), Scalar(0, 255, 0), 4 );
line( img_matches, scene_corners[1] + Point2f( img_object.cols, 0), scene_corners[2] + Point2f( img_object.cols, 0), Scalar( 0, 255, 0), 4 );
line( img_matches, scene_corners[2] + Point2f( img_object.cols, 0), scene_corners[3] + Point2f( img_object.cols, 0), Scalar( 0, 255, 0), 4 );
line( img_matches, scene_corners[3] + Point2f( img_object.cols, 0), scene_corners[0] + Point2f( img_object.cols, 0), Scalar( 0, 255, 0), 4 );
//-- Show detected matches
imshow( "Good Matches & Object detection", img_matches );
if(waitKey(30) >= 0) break;
}
//-- Конец основного цикла обработки
Резюме
К сожалению, предвосхищения многих людей, несколько превосходят state-of-the-art в области computer vision. Данный код я использовал на примере распознавания шоколадки. Мне пришлось немного повертеть в руках шоколадку, перед тем, как я понял каковы границы положений стабильного ее распознавания. В виду вариативности возникающих ситуаций, стабильность распознавания является головной болью №1. Тем не менее этот пример является базовым и может быть модифицирован.
Литература
1. www.vision.ee.ethz.ch/~surf/eccv06.pdf
2. www.sci.utah.edu/~gerig/CS6640-F2010/tutorial2-homographies.pdf
3. engineering.purdue.edu/kak/courses-i-teach/ECE661.08/solution/hw4_s1.pdf
Автор: alexhoppus