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 градусов).
Затем идет проверка того, достигает ли величина градиента локального максимума в соответствующем направлении.

Например, для сетки 3×3:
* если угол направления градиента равен нулю, точка будет считаться границей, если её интенсивность больше чем у точки выше и ниже рассматриваемой точки,
* если угол направления градиента равен 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).

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

примеры его работы можно посмотреть здесь:
image2comics-banner
например, вот пример работы Генератора:

Далее: 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


28 комментариев на «“27. OpenCV шаг за шагом. Обработка изображения — детектор границ Кенни (Canny)”»

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

    • ок, надо попробовать в реальном времени, наверно прикольно получится

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

    • ошибка заключается в исключении, которое происходит непонятно почему?

  2. поставил 2.2, теперь даже не компилится, cvCanny ваще нет, Canny принимает не IplImage а Mat, как перейти на 2.2, теперь вместо IplImage Mat писать чтоли???

    • у меня таких проблем не возникало 🙂

  3. применил вместо cvPyrMeanShiftFiltering cvPyrSegmentation, результат неплохой, примерно кадр в секунду
    только всёравно линии цветные а не чёрные как у вас, может вы ещё чего добавили?

    • у меня нормально получилось как в статье. Правда я на 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();
                          }
                      }
                  }
  4. Автор noonv, добрый день. Я только знакомлюсь с OpenCV и у меня стоит задание распознать сколько на рисунке кругов и сколько прямоугольников. Хочу написать эту программу в VisualStudio2017 с использованием библиотеки OpenCv 2014 или 2015. Буду очень благодарна, если Вы подскажите, какие настройки нужно сделать, чтобы все Ваши программы на стирание контуров и т. д. заработали у меня в Вижуал.

  5. Спасибо за ссылку. Но мне нужно посчитать сколько на рисунке четырехугольников и сколько кругов. https://docs.google.com/document/d/1K1iiHKvLjuecAPoV9d0wrVAdIjfk6tNTG8Y7bktIgqQ/edit Может, у Вас получится что-то подобрать к этому заданию? Буду очень очень благодарна!)))

  6. почему вот это не работет:

    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», а по старому либо ваще не работает либо чёрный экран

  7. У меня переделанный пример с 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 без проблем. Как сделать сделать правильно манипуляции с захваченным видео на лету?

  8. Спасибо за статью. Если определять контуры по исходному изображению, то видно много шумовых контуров. Сначала наеобходимо убрать шум из мелких деталей, те немного размыть изображение и определить контуры, потом еще немного размыть. Предлагаю готовый код. Что вам мешает найти контуры на изображениии комиксов?

    // 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;
    }
    • Еще лучше размывать изображение и увеличивать контрастность в несколько итераций.

  9. Лучший результат позволяет получить следующий код:

    //Нижегородский государственный университет им. Н.И. Лобачевского
    //Факультет вычислительной математики и кибернетики
    //Учебный курс «Разработка мультимедийных приложений
    //с использованием библиотек 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;
    }

Добавить комментарий

Arduino

Что такое Arduino?
Зачем мне Arduino?
Начало работы с Arduino
Для начинающих ардуинщиков
Радиодетали (точка входа для начинающих ардуинщиков)
Первые шаги с Arduino

Разделы

  1. Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях…

  2. Добрый день! Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны! Хотел узнать зачем использовать переменную типа…

3D-печать AI Android Arduino Bluetooth CraftDuino DIY IDE iRobot Kinect LEGO OpenCV Open Source Python Raspberry Pi RoboCraft ROS swarm ИК автоматизация андроид балансировать бионика версия видео военный датчик дрон интерфейс камера кибервесна манипулятор машинное обучение наше нейронная сеть подводный пылесос работа распознавание робот робототехника светодиод сервомашинка собака управление ходить шаг за шагом шаговый двигатель шилд юмор

OpenCV
Робототехника
Будущее за бионическими роботами?
Нейронная сеть - введение