25. OpenCV шаг за шагом. Обработка изображения - свёртка

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.
23. Поиск объекта по цвету. Цветовое пространство HSV.
24. Работа с камерой через библиотеку videoInput.
25. Обработка изображения — свёртка

Свёртка (англ. convolution) — это операция, показывающая «схожесть» одной функции с отражённой и сдвинутой копией другой.

В случае работы с изображениями — свёртка — это операция вычисления нового значения заданного пикселя, при которой учитываются значения окружающих его соседних пикселей.
Главным элементом свёртки является т.н. ядро свёртки — это матрица (произвольного размера и отношения сторон; чаще всего используется квадратная матрица (по-умолчанию, размеры 3х3)).
[ ][ ][ ]
[ ][я][ ]
[ ][ ][ ] 

У ядра свёртки есть важный параметр — якорь — это элемент матрицы (чаще всего — центр), который прикладывается к заданному пикселю изображения.

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

Пример:
пусть у нас есть клеточное поле, которое соответствует пикселям исходного изображния:
[47][48][49][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[47][50][42][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[47][48][42][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[  ][  ][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[  ][  ][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ]

и есть ядро свёртки, представляющее собой матрицу 3х3 с якорем в центре и элементами:
[0][1][0]
[0][0][0]
[0][0][0] 

Результат операции наложения нашего ядра на пиксель со значением 50:
[  ][  ][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[  ][48][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[  ][  ][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[  ][  ][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[  ][  ][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ]

Всё очень просто:
результат = 47*0 + 48*1 + 49*0 + 47*0 + 50*0 + 42*0 + 47*0 + 48*0 + 42*0 = 48


С OpenCV операция свёртки реализуется функцией cvFilter2D()

CVAPI(void) cvFilter2D( const CvArr* src, CvArr* dst, const CvMat* kernel,
                        CvPoint anchor CV_DEFAULT(cvPoint(-1,-1)));
-свёртка изображения при помощи ядра kernel

src — исходное изображение
dst — изображение для сохранения результа
kernel — ядро свёртки (массив из одного канала из элементов типа float)
anchor — якорь ядра ( (-1,-1) говорит, что якорь в центре (для ядра нечётной размерности))

//
// применяем фильтр к изображению при помощи cvFilter2D()
//
// robocraft.ru
//

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

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

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 );

	// клонируем картинку 
	dst = cvCloneImage(image);

	// окно для отображения картинки
	cvNamedWindow("original",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("cvFilter2D",CV_WINDOW_AUTOSIZE);

	float kernel[9];
	kernel[0]=0;
	kernel[1]=0;
	kernel[2]=0;

	kernel[3]=0;
	kernel[4]=1;
	kernel[5]=0;

	kernel[6]=0;
	kernel[7]=0;
	kernel[8]=0;

	// матрица
	CvMat kernel_matrix=cvMat(3,3,CV_32FC1,kernel);

	// накладываем фильтр
	cvFilter2D(image, dst, &kernel_matrix, cvPoint(-1,-1));

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

	// ждём нажатия клавиши
	cvWaitKey(0);

	// освобождаем ресурсы
	cvReleaseImage(&image);
	cvReleaseImage(&dst);
	// удаляем окна
	cvDestroyAllWindows();
	return 0;
}

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

При данном ядре никаких изменений в картинке не наблюдается.
// никаких изменений
	kernel[0]=0;
	kernel[1]=0;
	kernel[2]=0;

	kernel[3]=0;
	kernel[4]=1;
	kernel[5]=0;

	kernel[6]=0;
	kernel[7]=0;
	kernel[8]=0;

Давайте попробуем поиграться с элементами ядра свёртки:

// сглаживание
	kernel[0]=0.1;
	kernel[1]=0.1;
	kernel[2]=0.1;

	kernel[3]=0.1;
	kernel[4]=0.1;
	kernel[5]=0.1;

	kernel[6]=0.1;
	kernel[7]=0.1;
	kernel[8]=0.1;



// увеличение чёткости
	kernel[0]=-0.1;
	kernel[1]=-0.1;
	kernel[2]=-0.1;

	kernel[3]=-0.1;
	kernel[4]=2;
	kernel[5]=-0.1;

	kernel[6]=-0.1;
	kernel[7]=-0.1;
	kernel[8]=-0.1;



// увеличение яркости
	kernel[0]=-0.1;
	kernel[1]=0.2;
	kernel[2]=-0.1;

	kernel[3]=0.2;
	kernel[4]=3;
	kernel[5]=0.2;

	kernel[6]=-0.1;
	kernel[7]=0.2;
	kernel[8]=-0.1;


// затемнение
	kernel[0]=-0.1;
	kernel[1]=0.1;
	kernel[2]=-0.1;

	kernel[3]=0.1;
	kernel[4]=0.5;
	kernel[5]=0.1;

	kernel[6]=-0.1;
	kernel[7]=0.1;
	kernel[8]=-0.1;



Т.о. накладывая ядро с разными коэффициентами можно получить различные эффекты.

Свёртка — очень полезная и распространённая операция, лежащая в основе различных фильтров (размытие, повышение резкости, нахождение краёв).

Но возникает вопрос, как должен вести себя алгоритм свёртки на краях изображения?
Если взять рассмотренный пример, то как нужно рассчитывать свёртку, если приложить якорь ядра к пикселю со значением 47?
Хорошим решением этой проблемы может быть создание изображение большего размера, чем исходное, у которого на краях будут заданное значение пикселей. Начинать обработку картинки нужно тогда не с 0-го, а с 1-го пикселя (и до n-1-го пискселя):
[0 ][0 ][0 ][0 ][0][0][0][0][0][0][0][0][0][0]
[0 ][47][48][49][ ][ ][ ][ ][ ][ ][ ][ ][ ][0]
[0 ][47][50][42][ ][ ][ ][ ][ ][ ][ ][ ][ ][0]
[0 ][47][48][42][ ][ ][ ][ ][ ][ ][ ][ ][ ][0]
[0 ][  ][  ][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ][0]
[0 ][  ][  ][  ][ ][ ][ ][ ][ ][ ][ ][ ][ ][0]
[0 ][0 ][0 ][0 ][0][0][0][0][0][0][0][0][0][0]


В OpenCV уже есть функция реализующая копирование изображения с последующим окружением границей с заданным значением — cvCopyMakeBorder()

CVAPI(void) cvCopyMakeBorder( const CvArr* src, CvArr* dst, CvPoint offset,
                              int bordertype, CvScalar value CV_DEFAULT(cvScalarAll(0)));
— копирует исходное изображение, окружая его границей

src — исходное изображение
dst — изображение для сохранения результа
offset — смещение (координаты левого верхнего угла целевого изображения)
bordertype — тип границы:
#define IPL_BORDER_CONSTANT   0 // все пиксели границы заливаются value
#define IPL_BORDER_REPLICATE  1 // крайние пиксели изображения используются для заливки
// другие типы границы пока не поддерживаются:
#define IPL_BORDER_REFLECT    2 // пока не поддерживается
#define IPL_BORDER_WRAP       3 // пока не поддерживается

value — цвет заливки границы

Пример использования:

//
// пример использования cvCopyMakeBorder()
//
// robocraft.ru
//

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

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

int main(int argc, char* argv[])
{
	// имя картинки задаётся первым параметром
	char* filename = argc == 2 ? argv[1] : "Image0.jpg";
	// получаем картинку
	image = cvLoadImage(filename,1);
	// создаём картинки
	dst = cvCreateImage( cvSize(image->width+20, image->height+20), image->depth, image->nChannels);
	dst2 = cvCreateImage( cvSize(image->width+20, image->height+20), image->depth, image->nChannels);

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

	// окно для отображения картинки
	cvNamedWindow("original",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("IPL_BORDER_CONSTANT",CV_WINDOW_AUTOSIZE);
	cvNamedWindow("IPL_BORDER_REPLICATE",CV_WINDOW_AUTOSIZE);

	// обрамляем границей
	cvCopyMakeBorder(image, dst, cvPoint(10,10), IPL_BORDER_CONSTANT, cvScalar(250));
	cvCopyMakeBorder(image, dst2, cvPoint(10,10), IPL_BORDER_REPLICATE, cvScalar(250));


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

	// ждём нажатия клавиши
	cvWaitKey(0);

	// освобождаем ресурсы
	cvReleaseImage(&image);
	cvReleaseImage(&dst);
	cvReleaseImage(&dst2);
	// удаляем окна
	cvDestroyAllWindows();
	return 0;
}

скачать иcходник (25-cvCopyMakeBorder.cpp)
результат работы:


Функция свёртки cvFilter2D() внутри себя уже вызывает функцию cvCopyMakeBorder() с параметром IPL_BORDER_REPLICATE.

Далее: 26. Обработка изображения — операторы Собеля и Лапласа

Ссылки:
http://en.wikipedia.org/wiki/Convolution
http://ru.wikipedia.org/wiki/Свёртка_(математический_анализ)
imageconvolution — функция свёртки в php
поиграться со свёрткой в браузере
  • +1
  • 23 января 2011, 14:38
  • noonv

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

RSS свернуть / развернуть
+
0
Здраствуйте, у меня вопрос. Как построить градиент изображения? А если конкретнее, то я понял что надо сделать 2 операции свертки, после которых у нас получится 2 матрицы. Каким образом из них получить необходимую матрицу?
avatar

Board

  • 17 февраля 2011, 23:19
+
0
Интересный эффект получил на OpenCVSharp, когда сначала вместо float-массивов использовал double-массивы :) Многие варианты давали почти полностью чёрное изображение, а массив для сглаживания дал эффект хорошего осветления :)
avatar

JohnJ

  • 7 августа 2015, 14:07

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