Как-то встала передо мной следующая задача. У меня было много фотографий болгарских перцев и необходимо было отделить растение от фона. На примере этой задачи я покажу один из самых примитивных способов как это можно сделать при помощи openCV 2.4.
Суть задачи: закрасить белым все что не является растением.
Исходная фотография (слева) и то что должно получиться (справа).
Для начала загрузим изображение:
Mat src = imread("1.jpg"); //Исходное изображение
По умолчанию в opencv цветное изображение хранится палитре BGR. Определять цвет в BGR не очень удобно, поэтому для начала переведем изображение в формат HSV.
HSV (или HSB) означает Hue, Saturation, Value (Brightness), где:
Hue — цветовой тон, т.е. оттенок цвета.
Saturation — насыщенность. Чем выше этот параметр, тем «чище» будет цвет, а чем ниже, тем ближе он будет к серому.
Value (Brightness) — значение (яркость) цвета. Чем выше значение, тем ярче будет цвет (но не белее). А чем ниже, тем темнее (0% — черный)
Так как искать растение мы будем именно по цвету, то больше всего нас интересует именно тон.
Преобразуем изображение в палитру 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: исправлена битая ссылка.
Комментарии (19)
QDeathNick
05.07.2017 12:49+1У вас справа ус отклеился после дилатации.
Видимо ваши перцы были не критичны к такому качеству, мне приходится быть довольно точным, автоматизировать не удаётся. Мне часто приходится травить изображения фидерных устройств, но к сожалению они сложны даже для ручной обработки, часто приходится "выдумывать" где протравить, так как фон почти не отличается от предмета. Вот примерно как и у вас.
ser-mk
05.07.2017 15:16В первом цикле, где ищется цвет, слишком много строк кода, есть же inRange
Надо отметить что очень статья очень банально рассказывает о довольно сложной теме.
Гораздо интереснее рассказать о GrabCut и Watershed алгоритмах, а так же о их возможных вариациях.greenroach
05.07.2017 15:49Когда я начинала вникать в тему, мне как раз не хватало банальных примеров.
Возможно позже я напишу про другие алгоритмы.ser-mk
05.07.2017 17:46не знаю в каком году вы начинали вникать, но по ссылке что я указывал, весь материал из статьи есть. И датировано это 3 годами ранее.
kostus1974
05.07.2017 16:46отличный пример. и предлагаю тему для следующей вашей статьи.
уверен, что у многих периодически возникает задача нормализации и очистки от мусора скана документа. ну, допустим, надо сравнивать два скана: один эталонный, и другой уже после подписания печатной копии, после повторного сканирования. понятно, что разные сканеры, разный немного размер изображений, какие-то нарушения геометрии и т.п. задача — найти отличия второго скана от эталона.
как вам тема? напишете? ;)
shybovycha
06.07.2017 02:25В последнее время также увлекся обработкой изображений, но с целью экономии времени пишу код на Python. А так как opencv для python очень тесно связан с numpy, то и всяческие операции с изображением (которое по сути является матрицей numpy) много удобнее производить именно в матричном виде. В вашем случае я бы
использовал threshold из opencv (пример могу привести позже, когда не с телефона сидеть буду) для отделения пикселей по значению нужного канала h/s/v и тут же для закрашивания их в какой-то цвет (я бы предпочел черный; почему — см. п.2)
- не красил изображение, а сперва сформировал бы dilated/"улучшенную" маску, а потом плпросту наложил бы ее на изображение с помощью бинарных операторов opencv (and в данном случае)
Сейчас забавы ради пишу скрипт, склеивающий стек изображений с разной плоскостью фото (для (супер-)макро нужно), там использую это подход с большим успехом =)
MShevchenko
06.07.2017 13:36Во первых это не сегментация. Во вторых OpenCV очень так себе. Когда я работал в R&D по темам распознавания образов и стеганографии, все лучше (по качеству и скорости) было писать самому.
Именно из алгоритмов сегментации мне больше всего понравился этот http://cs.brown.edu/~pff/segment/ser-mk
07.07.2017 00:01а чем не понравился OpenCV? Понятно что не все есть там, но базовые примитивы оттуда удобно использовать.
MShevchenko
07.07.2017 00:58Тут есть целых три проблемы.
Первая. Используя OpenCV разработчик большую часть воспринимает как набор BlackBox-ов.
Вторая. Адаптация большинства сторонних алгоритмов к использованию с OpenCV неоправданно высока. Как пример http://home.cse.ust.hk/~cstws/research/TensorVoting3D/ (собственно там TV3D).
И третья. Реализация слишком многих вещей в OpenCV вызывает недоумение по поводу их реализации. Не говоря уже об оптимизации их для реальных проектов. Например на ARM-ах без использования NEON-оа.
Фактически это приводит к тому что, начав проект с использования OpenCV вы ограничиваете себя неким набором посредственно имплементированных алгоритмов.
Dmitry_5
Кто-нибудь может оформить это как action фотошопа? Было бы удобно 'обтравливать' изображения
alexpolevoy
Закажите непосредственно у Adobe.
shumski
Экшн всего из двух операций: color range + refine edge.
ICELedyanoj
Поделюсь своим методом обтравки. В своё время сильно помогало, когда клепали виньетки для школ и фотографии на документы. Снял видео. На видео грубый вариант обтравки первого попавшегося изображения, но в целом можно понять последовательность действий. Если приложить ещё немного фантазии и усилий, то метод вполне годится для массовой обтравки.
Видео
Видео почему-то иногда залипает (видимо хостинг у них слабоват) — просто отматывайте назад и оно подгружается.
В целом последовательность действий чем-то перекликается с методикой, описанной в статье.
Vadimner
В фотошопе выделение по цвету давно есть (Меню Select->Color Range). Статья то не про то как сделать в фотошопе, а про то как можно сделать такой инструмент, если делаешь «свой фотошоп».