Цветовая сегментация для чайников

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

Как-то встала передо мной следующая задача. У меня было много фотографий болгарских перцев и необходимо было отделить растение от фона. На примере этой задачи я покажу один из самых примитивных способов как это можно сделать при помощи openCV 2.4.

Суть задачи: закрасить белым все что не является растением.


Исходная фотография (слева) и то что должно получиться (справа).

Для начала загрузим изображение:

Mat src = imread("1.jpg"); //Исходное изображение

По умолчанию в opencv цветное изображение хранится палитре BGR. Определять цвет в BGR не очень удобно, поэтому для начала переведем изображение в формат HSV.
HSV (или HSB) означает Hue, Saturation, Value (Brightness), где:
Hue — цветовой тон, т.е. оттенок цвета.

Saturation — насыщенность. Чем выше этот параметр, тем «чище» будет цвет, а чем ниже, тем ближе он будет к серому.

Value (Brightness) — значение (яркость) цвета. Чем выше значение, тем ярче будет цвет (но не белее). А чем ниже, тем темнее (0% — черный)

image

Так как искать растение мы будем именно по цвету, то больше всего нас интересует именно тон.

Преобразуем изображение в палитру HSV и разобьем на три составляющие Hue Saturation Value соответственно.

//Переводим в формат HSV
Mat hsv = Mat(src.cols, src.rows, 8, 3); //
vector<Mat> splitedHsv = vector<Mat>();
cvtColor(src, hsv, CV_BGR2HSV);
split(hsv, splitedHsv);

Зададим диапазон значений тона. В OpenCV зеленый находится в диапазоне от 34 до 72. Перец на фотографиях не полностью зеленый. Поэтому опытным путем был подобран диапазон от 21 до 110.

const int GREEN_MIN = 21;
const int GREEN_MAX = 110;

Далее пробежимся по нашему изображению. Для каждого пикселя получим все три компоненты. Интенсивность мы использовать не будем, но, чтобы было понятнее и не перескакивать индексы я ее оставлю. Если тон не укладывается в заданный диапазон или яркость слишком низкая – значит это фон, поэтому закрашиваем все белым цветом.

for (int y = 0; y < hsv.cols; y++) {
	for (int x = 0; x < hsv.rows; x++) {
		// получаем HSV-компоненты пикселя
		int H = static_cast<int>(splitedHsv[0].at<uchar>(x, y));        // Тон
		int S = static_cast<int>(splitedHsv[1].at<uchar>(x, y));        // Интенсивность
		int V = static_cast<int>(splitedHsv[2].at<uchar>(x, y));        // Яркость
		
		//Если яркость слишком низкая либо Тон не попадает у заданный диапазон, то закрашиваем белым
		if ((V < 20) || (H < GREEN_MIN) || (H > GREEN_MAX)) {
			src.at<Vec3b>(x, y)[0] = 255;
			src.at<Vec3b>(x, y)[1] = 255;
			src.at<Vec3b>(x, y)[2] = 255;
		}
	}
}

В результате у нас получится такое изображение:



В целом фон удалился, но остались непонятные шумы в левом углу.

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

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

Эрозия (морфологическое сужение) – обратная операция. Действие эрозии подобно дилатации, разница лишь в том, что используется оператор поиска локального минимума серым цветом залиты пиксели, которые станут черными в результате эрозии.

Подробнее про это дело можно почитать тут. Применим морфологические операции к нашим картинкам. В качестве структурного элемента возьмем эллипс.

int an = 5;
//Морфологическое замыкание для удаления остаточных шумов.
Mat element = getStructuringElement(MORPH_ELLIPSE, Size(an * 2 + 1, an * 2 + 1), Point(an, an));
dilate(src, tmp, element);
erode(tmp, tmp, element);


Результат морфологической обработки.

Большинство шумов убрались, но и само изображение размылось, а это не совсем то что мы хотели. Поэтому мы будем использовать преобразованное изображение как маску, чтобы удалить ненужный шум.

Mat grayscaleMat;
cvtColor(tmp, grayscaleMat, CV_BGR2GRAY);

//Делаем бинарную маску
Mat mask(grayscaleMat.size(), grayscaleMat.type());
Mat out(src.size(), src.type());
threshold(grayscaleMat, mask, 200, 255, THRESH_BINARY_INV);

//Финальное изображение предварительно красим в белый цвет
out = Scalar::all(255);
//Копируем зашумленное изображение через маску
src.copyTo(out, mask);


Слева маска, справа результат применения маски.

Вот таким способом можно примитивно выделить объект из фона.

Полный код примера

UPD: исправлена битая ссылка.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 19

  • UFO just landed and posted this here
      0
      Закажите непосредственно у Adobe.
        0
        Экшн всего из двух операций: color range + refine edge.
          0
          Поделюсь своим методом обтравки. В своё время сильно помогало, когда клепали виньетки для школ и фотографии на документы. Снял видео. На видео грубый вариант обтравки первого попавшегося изображения, но в целом можно понять последовательность действий. Если приложить ещё немного фантазии и усилий, то метод вполне годится для массовой обтравки.
          Видео
          Видео почему-то иногда залипает (видимо хостинг у них слабоват) — просто отматывайте назад и оно подгружается.
          В целом последовательность действий чем-то перекликается с методикой, описанной в статье.
            0
            В фотошопе выделение по цвету давно есть (Меню Select->Color Range). Статья то не про то как сделать в фотошопе, а про то как можно сделать такой инструмент, если делаешь «свой фотошоп».
            +1

            У вас справа ус отклеился после дилатации.


            Видимо ваши перцы были не критичны к такому качеству, мне приходится быть довольно точным, автоматизировать не удаётся. Мне часто приходится травить изображения фидерных устройств, но к сожалению они сложны даже для ручной обработки, часто приходится "выдумывать" где протравить, так как фон почти не отличается от предмета. Вот примерно как и у вас.

              0
              В ссылке на морфологию вы забыли двоеточие после http.
                +1
                Спасибо, исправила.
                0
                В первом цикле, где ищется цвет, слишком много строк кода, есть же inRange
                Надо отметить что очень статья очень банально рассказывает о довольно сложной теме.
                Гораздо интереснее рассказать о GrabCut и Watershed алгоритмах, а так же о их возможных вариациях.
                  0
                  Когда я начинала вникать в тему, мне как раз не хватало банальных примеров.
                  Возможно позже я напишу про другие алгоритмы.
                    0
                    не знаю в каком году вы начинали вникать, но по ссылке что я указывал, весь материал из статьи есть. И датировано это 3 годами ранее.
                  0
                  отличный пример. и предлагаю тему для следующей вашей статьи.
                  уверен, что у многих периодически возникает задача нормализации и очистки от мусора скана документа. ну, допустим, надо сравнивать два скана: один эталонный, и другой уже после подписания печатной копии, после повторного сканирования. понятно, что разные сканеры, разный немного размер изображений, какие-то нарушения геометрии и т.п. задача — найти отличия второго скана от эталона.
                  как вам тема? напишете? ;)
                    0
                    Юлия, какая примерно длительность обработки одного изображения?
                    0

                    В последнее время также увлекся обработкой изображений, но с целью экономии времени пишу код на Python. А так как opencv для python очень тесно связан с numpy, то и всяческие операции с изображением (которое по сути является матрицей numpy) много удобнее производить именно в матричном виде. В вашем случае я бы


                    1. использовал threshold из opencv (пример могу привести позже, когда не с телефона сидеть буду) для отделения пикселей по значению нужного канала h/s/v и тут же для закрашивания их в какой-то цвет (я бы предпочел черный; почему — см. п.2)


                    2. не красил изображение, а сперва сформировал бы dilated/"улучшенную" маску, а потом плпросту наложил бы ее на изображение с помощью бинарных операторов opencv (and в данном случае)

                    Сейчас забавы ради пишу скрипт, склеивающий стек изображений с разной плоскостью фото (для (супер-)макро нужно), там использую это подход с большим успехом =)

                      0

                      Мне больше HSL система координат нравится. Интуитивно понятнее.

                        0
                        Во первых это не сегментация. Во вторых OpenCV очень так себе. Когда я работал в R&D по темам распознавания образов и стеганографии, все лучше (по качеству и скорости) было писать самому.
                        Именно из алгоритмов сегментации мне больше всего понравился этот http://cs.brown.edu/~pff/segment/
                          0
                          а чем не понравился OpenCV? Понятно что не все есть там, но базовые примитивы оттуда удобно использовать.
                            0
                            Тут есть целых три проблемы.
                            Первая. Используя OpenCV разработчик большую часть воспринимает как набор BlackBox-ов.
                            Вторая. Адаптация большинства сторонних алгоритмов к использованию с OpenCV неоправданно высока. Как пример http://home.cse.ust.hk/~cstws/research/TensorVoting3D/ (собственно там TV3D).
                            И третья. Реализация слишком многих вещей в OpenCV вызывает недоумение по поводу их реализации. Не говоря уже об оптимизации их для реальных проектов. Например на ARM-ах без использования NEON-оа.

                            Фактически это приводит к тому что, начав проект с использования OpenCV вы ограничиваете себя неким набором посредственно имплементированных алгоритмов.

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