22. OpenCV шаг за шагом. Поиск объекта по цвету - RGB

1. OpenCV шаг за шагом. Введение.
2. Установка.
3. Hello World.
4. Загрузка картинки.
5. Вывод видео
6. Ползунок
7. Захват видео с камеры
8. Запись видео
9. События от мышки
10. Обработка изображения — сглаживание
11. Обработка изображения — изменение размеров
12. ROI — интересующая область изображения
13. Типы данных OpenCV
14. Матрица
15. Сохранение данных в XML
16. Генерация случайных чисел
17. Обработка изображения — морфологические преобразования
18. Обработка изображения — морфологические преобразования 2
19. Обработка изображения — заливка части изображения
20. Обработка изображения — альфа-смешивание
21. Обработка изображения — пороговое преобразование
22. Поиск объекта по цвету — RGB.

Самый распространённый способ выделить объект — это цвет.

Цвет — это свойство тел отражать или испускать видимое излучение определенного спектрального состава и интенсивности.


Везде и всюду нас окружают цветовые индикаторы.
Светофоры, белые и жёлтые линии дорожной разметки, корпоративные цвета продуктов, дорожные указатели и различные индикаторы.
Например, для слабовидящих людей приклеивают жёлтые круги на дверях магазинов (жёлтый – последний цвет, которые они видят);
чтобы не перепутать, например, стеклянную витрину со стеклянной дверью.
Так же появляются жёлтые полосы на ступеньках пешеходных переходов. Покрываются жёлтой краской бордюры. Этот цвет один из самых ярких и людям с плохим зрением становится легче ориентироваться в городе.


Одной из важных проблем поиска по цвету — это влияние множества факторов.
Например, освещённость. Нельзя так же забывать, что видимый цвет — это результат взаимодействия спектра излучаемого света и поверхности. Т.е. если белый лист освещать светом красной лампочки, то и лист будет казаться красным.

Трихроматическая теория (сетчатка глаза имеет 3 вида рецепторов света, ответственных за цветное зрение) полагает, что достаточно всего трёх чисел, чтобы описать цвет (красный, синий, зелёный).

Т.е. используя три значения R, G, B

Цветовые пространства бывают линейные и нелинейные.
К линейным относится RGB

Нелинейные: HSV, LAB
Удобство HSV состоит в том, что координаты выбраны с учетом человеческого восприятия:
Hue (Тон)
Saturation(Насыщенность),
Value (Intensity) (Интенсивность)


Рассмотрим практическую задачу системы машинного зрения.

сама задача прозвучала на робофоруме
и упрощенно её можно сформулировать так:
Над колесом игорной рулетки установлена камера.
Требуется программа распознавания номера, в который попал шарик.

Исходные данные:
* шарик лежит во внешнем углу ячейки.
* номера на всех колесах располагаются одинаково.
* цвет номеров строго чередуется (красное-чёрное)
* номер 0 (Zero) зелёного цвета.

Один из возможных алгоритмов решения задачи — это найти зелёный сектор Zero и отсчитать от него сектор в котором детектируется белый шарик.
Т.о., в первом приближении, задача состоит в нахождении цветных объектов на картинке.

Я порылся в интернете и нашёл вот такую фотографию колеса игорной рулетки:


Попробуем с ней поработать.

Напишем программу, которая считывает картинку (в качестве первого параметра), разбивает её на слои (cvSplit()), над каждым из которых можно проделать пороговое преобразование (cvInRangeS()), причём значения интервалов удобным образом изменяются с помощью ползунков.

Результирующее значение выводится в окошке «rgb and» и представляет собой логическое И — cvAnd() между получившимися пороговыми картинками.
//
// позволяет подобрать параметры
// Rmin, Rmax, Gmin, Gmax, Bmin, Bmax
// для выделения нужного объекта по цвету
//
// robocraft.ru
//

#include <cv.h>
#include <highgui.h>
#include <stdlib.h>
#include <stdio.h>

IplImage* image = 0;
IplImage* dst = 0;

// для хранения каналов RGB
IplImage* rgb = 0;
IplImage* r_plane = 0;
IplImage* g_plane = 0;
IplImage* b_plane = 0;
// для хранения каналов RGB после преобразования
IplImage* r_range = 0;
IplImage* g_range = 0;
IplImage* b_range = 0;
// для хранения суммарной картинки
IplImage* rgb_and = 0;

int Rmin = 0;
int Rmax = 256;

int Gmin = 0;
int Gmax = 256;

int Bmin = 0;
int Bmax = 256;

int RGBmax = 256;

//
// функции-обработчики ползунка
//
void myTrackbarRmin(int pos) {
	Rmin = pos;
	cvInRangeS(r_plane, cvScalar(Rmin), cvScalar(Rmax), r_range);
}

void myTrackbarRmax(int pos) {
	Rmax = pos;
	cvInRangeS(r_plane, cvScalar(Rmin), cvScalar(Rmax), r_range);
}

void myTrackbarGmin(int pos) {
	Gmin = pos;
	cvInRangeS(g_plane, cvScalar(Gmin), cvScalar(Gmax), g_range);
}

void myTrackbarGmax(int pos) {
	Gmax = pos;
	cvInRangeS(g_plane, cvScalar(Gmin), cvScalar(Gmax), g_range);
}

void myTrackbarBmin(int pos) {
	Bmin = pos;
	cvInRangeS(b_plane, cvScalar(Bmin), cvScalar(Bmax), b_range);
}

void myTrackbarBmax(int pos) {
	Bmax = pos;
	cvInRangeS(b_plane, cvScalar(Bmin), cvScalar(Bmax), b_range);
}

int main(int argc, char* argv[])
{
	// имя картинки задаётся первым параметром
	char* filename = argc == 2 ? argv[1] : "Image0.jpg";
	// получаем картинку
	image = cvLoadImage(filename,1);

	printf("[i] image: %s\n", filename);
	assert( image != 0 );

	// создаём картинки
	rgb = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 3 );
	r_plane = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
	g_plane = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
	b_plane = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
	r_range = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
	g_range = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
	b_range = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
	rgb_and = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
	//  копируем
	cvCopyImage(image, rgb);
	// разбиваем на отельные каналы
	cvSplit( rgb, b_plane, g_plane, r_plane, 0 );

	//
	// определяем минимальное и максимальное значение
	// у каналов HSV
	double framemin=0;
	double framemax=0;

	cvMinMaxLoc(r_plane, &framemin, &framemax);
	printf("[R] %f x %f\n", framemin, framemax );
	Rmin = framemin;
	Rmax = framemax;
	cvMinMaxLoc(g_plane, &framemin, &framemax);
	printf("[G] %f x %f\n", framemin, framemax );
	Gmin = framemin;
	Gmax = framemax;
	cvMinMaxLoc(b_plane, &framemin, &framemax);
	printf("[B] %f x %f\n", framemin, framemax );
	Bmin = framemin;
	Bmax = framemax;

	// окна для отображения картинки
	cvNamedWindow("original",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("R",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("G",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("B",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("R range",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("G range",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("B range",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("rgb and",CV_WINDOW_AUTOSIZE);

	cvCreateTrackbar("Rmin", "R range", &Rmin, RGBmax, myTrackbarRmin);
	cvCreateTrackbar("Rmax", "R range", &Rmax, RGBmax, myTrackbarRmax);
	cvCreateTrackbar("Gmin", "G range", &Gmin, RGBmax, myTrackbarGmin);
	cvCreateTrackbar("Gmax", "G range", &Gmax, RGBmax, myTrackbarGmax);
	cvCreateTrackbar("Bmin", "B range", &Gmin, RGBmax, myTrackbarBmin);
	cvCreateTrackbar("Bmax", "B range", &Gmax, RGBmax, myTrackbarBmax);

	//
	// разместим окна по рабочему столу
	//
	if(image->width <1920/4 && image->height<1080/2){
		cvMoveWindow("original", 0, 0);
		cvMoveWindow("R", image->width+10, 0);
		cvMoveWindow("G", (image->width+10)*2, 0);
		cvMoveWindow("B", (image->width+10)*3, 0);
		cvMoveWindow("rgb and", 0, image->height+30);
		cvMoveWindow("R range", image->width+10, image->height+30);
		cvMoveWindow("G range", (image->width+10)*2, image->height+30);
		cvMoveWindow("B range", (image->width+10)*3, image->height+30);
	}

	while(true){

		// показываем картинку
		cvShowImage("original",image);

		// показываем слои
		cvShowImage( "R", r_plane );
		cvShowImage( "G", g_plane );
		cvShowImage( "B", b_plane );

		// показываем результат порогового преобразования
		cvShowImage( "R range", r_range );
		cvShowImage( "G range", g_range );
		cvShowImage( "B range", b_range );

		// складываем 
		cvAnd(r_range, g_range, rgb_and);
		cvAnd(rgb_and, b_range, rgb_and);

		// показываем результат
		cvShowImage( "rgb and", rgb_and );

		char c = cvWaitKey(33);
		if (c == 27) { // если нажата ESC - выходим
			break;
		}
	}
	printf("\n[i] Results:\n" );
	printf("[i][R] %d : %d\n", Rmin, Rmax );
	printf("[i][G] %d : %d\n", Gmin, Gmax );
	printf("[i][B] %d : %d\n", Bmin, Bmax );

	// освобождаем ресурсы
	cvReleaseImage(& image);
	cvReleaseImage(&rgb);
	cvReleaseImage(&r_plane);
	cvReleaseImage(&g_plane);
	cvReleaseImage(&b_plane);
	cvReleaseImage(&r_range);
	cvReleaseImage(&g_range);
	cvReleaseImage(&b_range);
	cvReleaseImage(&rgb_and);
	// удаляем окна
	cvDestroyAllWindows();
	return 0;
}

скачать иcходник (22-rgb.cpp)



Вот какие значения удалось подобрать мне:
R [0, 61)
G [135, 255)
B [47, 255)

Как видите сектор Zero очень даже хорошо выделяется.

Должен отметить важный момент — хотя работа со статической картинкой, фактически, ничем не отличается от работы с кадром видеопотока, но, на самом деле, работа с видеоданными требует отдельного тестирования; т.к. видеокартинка динамична, имеет различные шумы и т.д.

Простым вариантом может быть использование видеороликов, снятых камерой, которая будет использоваться в дальнейшем.
В данном случае — для тестирования можно просто сохранить видеролик с youtub-а :)

Итак, сектор Zero выделяется хорошо, но попытка выделить по цвету шарик не даст ожидаемого результата. Разумеется, можно добиться, чтобы шарик был виден, но так же будут видны белые числа номеров, а ещё блики (на оси рулетки и металлической ленте).

Как и предложили на форуме, эти ложные срабатывания можно устранить, если работать только с заданной частью изображния, где их нет.
Т.е. сначала мы ждём, когда в заданном секторе будет задетектировано Zero, а далее мы отсчитываем сектора рулетки, до тех пор, пока не будет задетектирован шарик.

Использованы функции:
#define cvCvtPixToPlane cvSplit
CVAPI(void)  cvSplit( const CvArr* src, CvArr* dst0, CvArr* dst1,
                      CvArr* dst2, CvArr* dst3 );
— разделяет многоканальный массив на одноканальные массивы или выделяет отдельные цветовые плоскости
src — исходный массив(изображение)
dst0..dst3 — массивы(изображения) для сохранения результатов

обратная операция выполняется функцией:
#define cvCvtPlaneToPix cvMerge
CVAPI(void)  cvMerge( const CvArr* src0, const CvArr* src1,
                      const CvArr* src2, const CvArr* src3,
                      CvArr* dst );
— объединяет несколько одноканальных массивов в один многоканальный массив
src0..src2 — исходные массивы(изображения)
dst — массив(изображение) для сохранения результатов

CVAPI(void) cvAnd( const CvArr* src1, const CvArr* src2,
                  CvArr* dst, const CvArr* mask CV_DEFAULT(NULL));
-выполняет по-элементую операцию конъюнкции (логического И (AND)) над элементами двух массивов.
src1 — первый исходный массив(изображение) (если NULL, то считается, что массив состоит из 1)
src2 — второй исходный массив(изображение)
dst — массив(изображение) для сохранения результата
mask — маска (8-битный 1-канальный массив)

результат операции описывается формулой:
dst(I)=src1(I)&src2(I) if mask(I)!=0

CVAPI(void)  cvMinMaxLoc( const CvArr* arr, double* min_val, double* max_val,
                          CvPoint* min_loc CV_DEFAULT(NULL),
                          CvPoint* max_loc CV_DEFAULT(NULL),
                          const CvArr* mask CV_DEFAULT(NULL) );
— определяет минимальное и максимальное значения массива, а так же их местоположение
arr — массив(изображение) для поиска (одноканальный или многоканальный с установленным COI)
min_val — указатель на переменную для сохранения минимального значения
max_val — указатель на переменную для сохранения максимального значения
min_loc — указатель на точку местоположения минимума
max_loc — указатель на точку местоположения максимума
mask — маска для выбора подмассива

CVAPI(void) cvMoveWindow( const char* name, int x, int y );
— перемещает окно
name — название окна
x,y — новые координаты верхнего левого угла окна

заодно укажем на функцию изменения размера окна:

CVAPI(void) cvResizeWindow( const char* name, int width, int height );
— изменяет размер окна
name — название окна
width, height — новые ширина и высота окна

Ссылки:
ru.wikipedia.org/wiki/Цвет
ru.wikipedia.org/wiki/RGB
ru.wikipedia.org/wiki/HSV_(цветовая_модель)
ru.wikipedia.org/wiki/LAB

Читать далее: 23. Поиск объекта по цвету. Цветовое пространство HSV.
  • +1
  • 30 ноября 2010, 10:37
  • noonv

Комментарии (3)

RSS свернуть / развернуть
+
0
Интересно, что у меня получились совершенно другие настройки. Я пишу на c#, используя openCVSharp. Но не думаю, что из-за этого могла появиться такая разница :)
avatar

JohnJ

  • 6 августа 2015, 15:30
+
0
Не знаю как вставить скриншот, но у меня вышло:
К: 42, 151
З: 125, 244
С: 0, 58

А с указанными в статье данными вообще получается ерунда: выделяется коричневая область круга, что внутри циферблата.
avatar

JohnJ

  • 6 августа 2015, 15:32

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.