CraftDuino v2.0
  • - это CraftDuino - наш вариант полностью Arduino-совместимой платы.
  • CraftDuino - настоящий конструктор, для очень быстрого прототипирования и реализации идей.
  • Любая возможность автоматизировать что-то с лёгкостью реализуется с CraftDuino!
Просто добавьте CraftDuino!

27. OpenCV шаг за шагом. Обработка изображения - детектор границ Кенни (Canny)


Оглавление
1. OpenCV шаг за шагом. Введение.
2. Установка.
3. Hello World.
4. Загрузка картинки.
...
25. Обработка изображения — свёртка
26. Обработка изображения — операторы Собеля и Лапласа
27. Обработка изображения — детектор границ Кенни (Canny)

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

Проще говоря, край — это резкий переход/изменение яркости.
Причины возникновения краёв:
* изменение освещенности
* изменение цвета
* изменение глубины сцены (ориентации поверхности)

Получается, что края отражают важные особенности изображения и поэтому, целями преобразования изображения в набор кривых являются:
* выделение существенных характеристик изображения
* сокращение объема информации для последующего анализа

Самым популярным методом выделения границ является детектор границ Кенни.

Хотя работа Кенни была проведена на заре компьютерного зрения (1986), детектор границ Кенни до сих пор является одним из лучших детекторов.

Шаги детектора:
— Убрать шум и лишние детали из изображения
— Рассчитать градиент изображения
— Сделать края тонкими (edge thinning)
— Связать края в контура (edge linking)

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

Границы на изображении могут находиться в различных направлениях, поэтому алгоритм Кенни использует четыре фильтра для выявления горизонтальных, вертикальных и диагональных границ. Воспользовавшись оператором обнаружения границ (например, оператором Собеля) получается значение для первой производной в горизонтальном направлении (Gу) и вертикальном направлении (Gx).
Из этого градиента можно получить угол направления границы:
Q=arctan(Gx/Gy)

Угол направления границы округляется до одной из четырех углов, представляющих вертикаль, горизонталь и две диагонали (например, 0, 45, 90 и 135 градусов).
Затем идет проверка того, достигает ли величина градиента локального максимума в соответствующем направлении.

Например, для сетки 3x3:
* если угол направления градиента равен нулю, точка будет считаться границей, если её интенсивность больше чем у точки выше и ниже рассматриваемой точки,
* если угол направления градиента равен 90 градусам, точка будет считаться границей, если её интенсивность больше чем у точки слева и справа рассматриваемой точки,
* если угол направления градиента равен 135 градусам, точка будет считаться границей, если её интенсивность больше чем у точек находящихся в верхнем левом и нижнем правом углу от рассматриваемой точки
* если угол направления градиента равен 45 градусам, точка будет считаться границей, если её интенсивность больше чем у точек находящихся в верхнем правом и нижнем левом углу от рассматриваемой точки.

Таким образом, получается двоичное изображение, содержащее границы (т.н. «тонкие края»).

В OpenCV, детектор границ Кенни реализуется функцией cvCanny(), которая обрабатывает только одноканальные изображения.

CVAPI(void)  cvCanny( const CvArr* image, CvArr* edges, double threshold1,
                      double threshold2, int  aperture_size CV_DEFAULT(3) );
— выполнение алгоритма Canny для поиска границ

image — одноканальное изображение для обработки (градации серого)
edges — одноканальное изображение для хранения границ, найденных функцией
threshold1 — порог минимума
threshold2 — порог максимума
aperture_size — размер для оператора Собеля

//
// пример работы детектора границ Кенни - cvCanny()
//
// robocraft.ru
//

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

IplImage* image = 0;
IplImage* gray = 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 );

	// создаём одноканальные картинки
	gray = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
	dst = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );

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

	// преобразуем в градации серого
	cvCvtColor(image, gray, CV_RGB2GRAY);

	// получаем границы
	cvCanny(gray, dst, 10, 100, 3);

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

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

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

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



Обратите внимание, как меняется картина, если увеличить размер оператора Собеля:
вот результат работы функции
cvCanny(gray, dst, 10, 100, 5);



И в качестве бонуса :)
Вот какой прикольный эффект можно получить, если найденные контуры вычесть из изображения.

Выглядит, как комикс :)

Для вычитания используем функцию cvSub():

cvSub — поэлементрная разница между двумя массивами
cvSubS — разница между элементами массива и скаляром
cvSubRS — разница между скаляром и элементами массива

CVAPI(void)  cvSub( const CvArr* src1, const CvArr* src2, CvArr* dst,
                    const CvArr* mask CV_DEFAULT(NULL));
— поэлементрная разница между двумя массивами:
dst(mask) = src1(mask) — src2(mask)
src1 — первый исходный массив
src2 — второй исходный массив
dst — целевой массив
mask — маска (8-битный однаканальный массив, указывающий какие элементы целефого массива могут быть изменены)

функция вычитает один массив из другого по формуле:
dst(I)=src1(I)-src2(I) if mask(I)!=0

массивы должны быть одного типа (кроме маски) и одинакового размера (или ROI).

//
// Прикольный эффект с использованием детектора Кенни:
// находятся контуры и вычитаются из изображения
//
// robocraft.ru
//

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

int main(int argc, char* argv[])
{
	IplImage *src=0, *dst=0, *dst2=0;

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

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

	// покажем изображение
	cvNamedWindow( "original", 1 );
	cvShowImage( "original", src );

	// получим бинарное изображение
	dst2 = cvCreateImage( cvSize(src->width, src->height), IPL_DEPTH_8U, 1);
	cvCanny(src, dst2, 50, 200);

	cvNamedWindow( "bin", 1 );
	cvShowImage( "bin", dst2);

	//cvScale(src, dst);
	cvSub(src, dst2, dst2);
	cvNamedWindow( "sub", 1 );
	cvShowImage( "sub", dst2);


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

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


Ещё вариации функции вычитания cvSubS и cvSubRS:

/* dst(mask) = src(mask) - value = src(mask) + (-value) */
CV_INLINE  void  cvSubS( const CvArr* src, CvScalar value, CvArr* dst,
                         const CvArr* mask CV_DEFAULT(NULL))
{
    cvAddS( src, cvScalar( -value.val[0], -value.val[1], -value.val[2], -value.val[3]),
            dst, mask );
}
— разница между элементами массива и скаляром

/* dst(mask) = value - src(mask) */
CVAPI(void)  cvSubRS( const CvArr* src, CvScalar value, CvArr* dst,
                      const CvArr* mask CV_DEFAULT(NULL));
— разница между скаляром и элементами массива
src — первый исходный массив
value — скаляр из которого производится вычитание
dst — целевой массив
mask — маска (8-битный однаканальный массив, указывающий какие элементы целефого массива могут быть изменены)

функция вычитает каждый элемент массива из скаляра:
dst(I)=value-src(I) if mask(I)!=0

массивы должны быть одного типа (кроме маски) и одинакового размера (или ROI).

Получающиеся картинки мне очень понравились и я на скорую руку набросал сервис Генератора комиксов :)

примеры его работы можно посмотреть здесь:

например, вот пример работы Генератора:


Далее: 28. Преобразование Хафа

Ссылки:
http://en.wikipedia.org/wiki/Canny_edge_detector
http://ru.wikipedia.org/wiki/Выделение_границ
Оригинальная статья: JOHN CANNY, A Computational Approach to Edge Detection
И.М.Журавель «Краткий курс теории обработки изображений»: Границы изображений: Края и их обнаружение

Дополнительно:
Deriche edge detector
  • 0
  • 1 апреля 2011, 10:35
  • noonv

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

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

Romiks

  • 1 апреля 2011, 19:26
+
0
точно так :)
avatar

noonv

  • 1 апреля 2011, 19:27
+
0
ок, надо попробовать в реальном времени, наверно прикольно получится
avatar

Romiks

  • 1 апреля 2011, 19:29
+
0
сделал, но получилось не совсем так, какие значения для cvCanny использовали???
и у меня контуры цветные а не чёрные получились
avatar

Romiks

  • 2 апреля 2011, 15:24
+
+1
экспериментируйте :) но если вы хотите повторить Генератор комиксов, то там ещё применена сегментация ;)
avatar

noonv

  • 2 апреля 2011, 16:29
+
0
ок, ща ползунки прикручу
avatar

Romiks

  • 2 апреля 2011, 16:29
+
0
9 глава, Mean-shift segmentation, да???
avatar

Romiks

  • 2 апреля 2011, 16:34
+
+1
угу — cvPyrMeanShiftFiltering(), но обратите внимание, что в версии 2.1 в реализации этой функции есть ошибка. Впрочем, у меня она нормально работала в Release-версиях программы.
Можете погуглить и найти, как пофиксить эту ошибку, а затем пересобрать библиотеку, или же использовать версию 2.2, где эта ошибка исправлена.
avatar

noonv

  • 2 апреля 2011, 16:47
+
0
ошибка заключается в исключении, которое происходит непонятно почему?
avatar

Romiks

  • 2 апреля 2011, 17:01
+
0
поставил 2.2, теперь даже не компилится, cvCanny ваще нет, Canny принимает не IplImage а Mat, как перейти на 2.2, теперь вместо IplImage Mat писать чтоли???
avatar

Romiks

  • 2 апреля 2011, 18:15
+
0
у меня таких проблем не возникало :)
avatar

noonv

  • 2 апреля 2011, 18:17
+
0
нашёл! она в legacy лежит, только теперь камера на ноуте не работает, а на компе всё норм вроде
avatar

Romiks

  • 2 апреля 2011, 18:29
+
0
почему вот это не работет:
VideoCapture cap("0");

if(!cap.isOpened()) {
	cout << "Can't create camera capture, check your camera!";
	_getch();
	return 1;
}

namedWindow("Original video");

while(1)
{
	Mat img;
	cap >> img;

	imshow("Original video", img);

	if(waitKey(33) >= 0)
		break;
}

сразу же вылетает «Access violation», а по старому либо ваще не работает либо чёрный экран
avatar

Romiks

  • 2 апреля 2011, 18:50
+
0
всё, работает! только ужасно медленно, примерно 1 кадр в 5-10 секунд, нельзя ли как-нить ускорить сегментацию?
avatar

Romiks

  • 3 апреля 2011, 09:09
+
0
если сможете это сделать — расскажите :))
avatar

noonv

  • 3 апреля 2011, 10:01
+
0
??
avatar

Romiks

  • 3 апреля 2011, 12:15
+
0
применил вместо cvPyrMeanShiftFiltering cvPyrSegmentation, результат неплохой, примерно кадр в секунду
только всёравно линии цветные а не чёрные как у вас, может вы ещё чего добавили?
avatar

Romiks

  • 3 апреля 2011, 10:37
+
0
И меня интересуют линии эти черные) хоть ты тресни, но разноцветными получаются. Колдунство какое-то)
avatar

DikiiSlon

  • 25 сентября 2014, 22:20
+
0
у меня нормально получилось как в статье. Правда я на c# пишу. Предоставляю код, может он подскажет вам как изменить ваш алгоритм:

            var filename = "cat1.jpg";
            using (var image = Cv.LoadImage(filename)) {
                CvWindow w = null;
                CvWindow gray = null;
                CvWindow canny = null;
                CvWindow comix = null;
                IplImage grayImg = null;
                IplImage cannyImg = null;
                IplImage comixImg = null;
                var imgColors = new List<IplImage>();
                try {
                    w = new CvWindow("границы Кенни - Оригинал");
                    gray = new CvWindow("границы Кенни - Серость");
                    canny = new CvWindow("границы Кенни - Кенни");
                    comix = new CvWindow("границы Кенни - границы на оригинале");
                    grayImg = new IplImage(image.Size, BitDepth.U8, 1);
                    cannyImg = new IplImage(image.Size, BitDepth.U8, 1);
                    comixImg = new IplImage(image.Size, BitDepth.U8, 3);

                    image.CvtColor(grayImg, ColorConversion.RgbToGray);
                    grayImg.Canny(cannyImg, 10, 100, ApertureSize.Size3);

                    for (var i = 0; i < 3; i++) {
                        imgColors.Add(new IplImage(image.Size, BitDepth.U8, 1));
                    }
                    image.Split(imgColors[0], imgColors[1], imgColors[2], null);
                    for (var i = 0; i < 3; i++) {
                        imgColors[i].Sub(cannyImg, imgColors[i]);
                    }
                    comixImg.Merge(imgColors[0], imgColors[1], imgColors[2], null);

                    w.ShowImage(image);
                    gray.ShowImage(grayImg);
                    canny.ShowImage(cannyImg);
                    comix.ShowImage(comixImg);

                    Cv.WaitKey(0);
                } finally {
                    w?.Dispose();
                    gray?.Dispose();
                    canny?.Dispose();
                    comix?.Dispose();
                    grayImg?.Dispose();
                    cannyImg?.Dispose();
                    comixImg?.Dispose();
                    foreach (var img in imgColors) {
                        img?.Dispose();
                    }
                }
            }
avatar

JohnJ

  • 9 августа 2015, 08:13
+
0
У меня переделанный пример с cvQueryFrame выдает ошибку:

frame1 = cvQueryFrame( capture );
frame = cvCreateImage( cvSize(frame1->width, frame1->height), IPL_DEPTH_8U, 1);
cvCvtColor(frame1, frame, CV_RGB2GRAY);
cvCanny(frame, frame2, 10, 100, 3);
cvShowImage("capture", frame2);


Необработанное исключение в «0x766eb727» в «cv1.exe»: Исключение Microsoft C++: cv::Exception по адресу 0x0015e71c…

А с cvLoadImage без проблем. Как сделать сделать правильно манипуляции с захваченным видео на лету?
avatar

r2d2

  • 12 августа 2016, 08:26
+
0
Спасибо за статью. Если определять контуры по исходному изображению, то видно много шумовых контуров. Сначала наеобходимо убрать шум из мелких деталей, те немного размыть изображение и определить контуры, потом еще немного размыть. Предлагаю готовый код. Что вам мешает найти контуры на изображениии комиксов?
// contours.cpp: определяет точку входа для консольного приложения.
//

#include "stdafx.h"


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

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

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

        // создаём одноканальные картинки
        gray = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
		smgray2 = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
		smgray3 = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
        dst1 = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
		dst2 = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );
		dst3 = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 1 );

        // окно для отображения картинки
        cvNamedWindow("original",CV_WINDOW_AUTOSIZE);
        cvNamedWindow("gray",CV_WINDOW_AUTOSIZE);
		cvNamedWindow("smoothgray1",CV_WINDOW_AUTOSIZE);
		cvNamedWindow("smoothgray2",CV_WINDOW_AUTOSIZE);
        cvNamedWindow("cvCanny1",CV_WINDOW_AUTOSIZE);
		cvNamedWindow("cvCanny2",CV_WINDOW_AUTOSIZE);
		cvNamedWindow("cvCanny3",CV_WINDOW_AUTOSIZE);


        // преобразуем в градации серого
        cvCvtColor(image, gray, CV_RGB2GRAY);
		// получаем границы
		cvCanny(gray, dst1, 10, 100, 3);

		// размываем изображение
		cvSmooth(gray, smgray2, CV_GAUSSIAN, 5, 5);
		// получаем границы
		cvCanny(smgray2, dst2, 10, 100, 3);

		// еще раз размываем изображение
		cvSmooth(smgray2, smgray3, CV_GAUSSIAN, 3, 3);
		// получаем границы
        cvCanny(smgray3, dst3, 10, 100, 3);

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

		cvShowImage("smoothgray2",smgray2);
		cvShowImage("cvCanny2", dst2 );

		cvShowImage("smoothgray3",smgray3);
        cvShowImage("cvCanny3", dst3 );

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

        // освобождаем ресурсы
        cvReleaseImage(&image);
        cvReleaseImage(&gray);
		cvReleaseImage(&smgray3);
		cvReleaseImage(&smgray2);
        cvReleaseImage(&dst1);
		cvReleaseImage(&dst2);
		cvReleaseImage(&dst3);

        // удаляем окна
        cvDestroyAllWindows();
        return 0;
}
avatar

tester

  • 4 ноября 2016, 07:06
+
0
Еще лучше размывать изображение и увеличивать контрастность в несколько итераций.
avatar

tester

  • 4 ноября 2016, 07:48
+
0
Лучший результат позволяет получить следующий код:

//Нижегородский государственный университет им. Н.И. Лобачевского
//Факультет вычислительной математики и кибернетики
//Учебный курс «Разработка мультимедийных приложений
//с использованием библиотек OpenCV и IPP», Lec06_OpenCV_text.pdf

#include "stdafx.h"

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

using namespace cv;

int main(int argc, char** argv)
{
Mat img, gray, edges; // Объявление матриц

char* filename = argc == 2 ? argv[1] : "lena.jpg";
// имя картинки задаётся первым параметром

//img = imread(filename, 1); // не работает 
img = cvLoadImage(filename,1);// Читаем изображение
// получаем картинку

imshow("original", img); 
// Отрисовываем изображение

cvtColor(img, gray, CV_RGB2GRAY);
// Конвертируем в монохромный формат

GaussianBlur(gray, gray, Size(7, 7), 1.5);
// Устраняем размытие

imshow("gray", gray); 
// Отрисовываем изображение

Canny(gray, edges, 0, 50);
// Запускаем детектор ребер

imshow("edges", edges);
// Отрисовываем изображение

waitKey();
//Ожидаем нажатия клавиши
return 0;
}
avatar

tester

  • 6 ноября 2016, 04:35

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