Распознавание плоских объектов OpenCV 2.4

Привет всем. Я хотел бы рассказать о принципах, лежащих в основе распознавания объектов с использованием 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.
image
смысл в том, что используемая метрика (лямбды — собственные числа, det — определитель, trace — след матрицы, альфа — константа)
image
позволяет идентифицировать угловые точки, т.к. в этих точках будут весомые перепады градиентов по 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). Принцип тут следующий:
image

Патч вокруг данной точки разбивается на детерменированные блоки, в каждом блоке вычисляется доминирующее градиентное направление и магнитуда + осуществляется поворот в сторону доминирующего направления (инвариантность к повороту). Данный «градиентный рисунок» описывает локальный патч.

//-- Этап 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
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 17

    0
    Вы не пробовали автомобильный номер локализовать методом SURF?
    Чисто теоретически это возможно?
    Просто стандартный метод поиска светлых прямоугольников хорошо работает, но в случае помех или больших углов дает некорректные результаты
      +1
      Пробовал, это одно из заданий на курсах по компьютерному зрению в МГУ. Да это и теоретически и практически возможно. Причем и на ночной базе тоже работало, правда, естественно, с меньшей точностью. Вы можете использовать surf на размеченных картинках и обучить модель svm, которая в дальнейшем будет относить сдетектированные точки к номеру или фону.
        0
        Но там ведь будет такая проблема — собственно какие ключевые точки он будет брать
        Номера то разные.
        Или все-таки обучение эту проблему решает?
          +1
          По ключевым точкам можно находить на изображении автомобильные номера. А способов распознать сам номер уже много. Можно при помощи того же SVM классификатора.
            0
            Это я знаю=) Я имел ввиду, что на разных номерах будут разные ключевые точки))
              +1
              image как видно, в номере всегда присутствуют рамки, флаг и надпись RUS. Поэтому вполне можно использовать например такой шаблон в качестве эталона для сопоставления ОТ image. Только в качестве детектора лучше использовать какой-нибудь детектор углов типа детектора Харриса а не SIFT/SURF. И тут еще надо учитывать, что номер имеет маленький размер на изображении, а значит будет мало особых точек для сопоставления и вполне возможен вариант, когда данных не хватит для построения искомой гомографии.
                +1
                BelBES,Так делать не надо, нужна как можно более вариативная выборка. (все возможные цифры и символы которые могут встретиться). Если вы обучаете SVM по крайней мере по сифтам с точек.
                  +1
                  Имеется ввиду поиск непосредственно номера на фотографии, и в этом есть смысл. OCR написать уже потом можно.
                    0
                    Насколько я знаю для того, чтобы детектировать номер при помощи SVM классификатора, необходимо получить на сцене какие-то области-кандидаты похожие на изображение знака, а потом уже для каждой такой области уточнять модель при помощи классификатора. Когда мы нашли особые точки на изображении сцены(пуская тем-же SIFT'ом), как среди всех точек выделить те, которые могут соответствовать номерам?
                      0
                      Можно каждую точку классифицировать как точку фона или номера, предварительно обучившись на размеченной базе, а потом выделить подходящий кластер точек как область в которой есть номер. Далее уже распознавать.
                        0
                        Вот сейчас просто применением SURF-детектора и SIFT-дескрипторов получил такие картинки:


                        Как видно в качестве паттерна использовал первую попавшую под руки картинку с номером. При этом номер худо-бедно локализуется. Если подумать еще немного и применить знания о предметной области то получится лучше и точность можно повысить до приемлемой…
                        А какой характеристический вектор особой точки используется для подобного обучения классификатора? Дескриптор в чистом виде и используется идея о характерных для номеров перепадах градиента? Вы не могли-бы показать какой-нибудь пример локализации номера таким способом?
                          0
                          Знакомые картинки: )Я не знаю, что такое характеристический вектор особой точки. Я это же задание делал два года назад, описанным мною выше способом. Я могу что-то путать, но вроде я такой применял метод. Кстати, код у меня до сих пор сохранился, но рыться в нем сейчас нет никакого желания.
                            0
                            Вы обучаете классфикатор просто подсовывая ему дескрипторы особых точек? Если да, то какой процент ложных срабатываний у обученного классификатора? думается мне, что большой…
                            p.s. картинки нашлись в ходе 5-минутного гугления, вроде это лабораторка в каком-то из ВУЗ'ов)
                              0
                              Я бы методом окон искал… А не просто фичи со всей картинки брал. И как минимум перед тем как искать фичи стоит сделать блур по гаусу, что бы шум не так сильно влиял.
                                0
                                Можно взять фичи со всей картинки, сматчить на них, получить примерное расположение номеров, а потом матчить по маске только в эти регионы. Так вероятно получится быстрее чем скользящим окном по всей картинке бегать.
                                0
                                Картинки — материал задания на курсах по компьютерному зрению в МГУ (детекция распознавание номеров). Возможно большой, это не предметный спор, надо на конкретной выборке смотреть.
                    +1
                    Ключевые точки будут разные, но если у Вас в обучающей выборке будет достаточное количество номеров, то общие паттерны будут выявляться, все равно на номере ограниченный набор символов, это раз. Во — вторых там явные градиентные перепады (контраст белое на черном) концентрированные. Найти номер будет несложно. После того как Вы нашли номер, надо будет уже распознавать его как сказал Fesor.

          Only users with full accounts can post comments. Log in, please.