- PVSM.RU - https://www.pvsm.ru -
Есть битовая матрица, содержащая изображение круга, квадрата или треугольника (фигуры закрашены). Изображение может быть немного искажено или содержать помехи. Задача – написать алгоритм, который по матрице выяснит, какая фигура нарисована на изображении.

Эта простая с первого взгляда задача встретилась мне на вступительном экзамене в DMLabs. На первом занятии мы обсудили решение, а преподаватель (Александр Шлемов; он также руководил дальнейшей реализацией) показал, почему для решения лучше использовать машинное обучение.
В процессе дискуссии мы обнаружили, что наши решения делятся на два этапа: фильтрацию помех и вычисление какой-то метрики, по которой будет проходить классификация. Тут возникает проблема нахождения границ: необходимо знать, какие значения метрики могут получаться для каждой из фигур. Можно проложить эти границы вручную “на глазок”, но лучше поручить это дело математически обоснованному алгоритму. Таким образом мы подходим к использованию методов машинного обучения (Machine Learning).
Таким образом эта учебная задачка стала для меня введением в Machine Learning, и я хотел бы поделиться с вами этим опытом.
Можно придумать несколько метрик, у которых будут разные «плохие случаи» — когда на картинке нарисована одна фигура, а метрика указывает на другую или дает значение, близкое к граничному. Тогда при возникновении такого случая у одной остальные её поправят.
Проблему с границами мы решим с помощью дерева решений (Decision tree), а точнее, с помощью её разновидности: CART (подробнее можно прочитать тут) — она проста и лучше интерпретируется в терминах границ. Это дерево в каждом узле делит данные на подгруппы так, чтобы в результате максимально уменьшить неоднородность в подгруппах (критерий Гини), и повторяет это рекурсивно для каждой подгруппы.
Суть нашего метода: мы создаем много случайных изображений кругов, треугольников и четырехугольников. После этого мы пропускаем каждое через набор фильтров, и вычисляем метрики. На основе этой статистики мы сможем провести границы, что делает дерево решений.
После этого, получив неизвестное изображение, пропустив его через фильтры и вычислив метрики, с помощью этого дерева мы сможем сказать, к какому классу оно относится.
Генератор должен не только создавать картинки, а также уметь накладывать на них помехи. При генерации мы должны отбрасывать фигуры с сторонами меньше некой величины (например, 15 пикселей) и с тупыми углами (больше 150 градусов) — даже человеку сложно их классифицировать.
Скрипт заботливо предоставил Виктор Евстратов.
bitbucket.org/rampeer/image-classifier/src/HEAD/picture_generator.py [1]
В процессе дискуссии с коллегами мы нашли хорошие идеи для фильтров. Первая — что мы можем определить помехи как пиксели черного цвета, окруженные пикселями противоположного цвета. Эта идея легла в основу медианного фильтра. Вторая — что мы можем определить искомый контур как большое скопление черных пикселей. Это — идея бикомпонентного фильтра.
Медианный фильтр: для каждого черного пикселя мы узнаем количество его «черных» соседей в окрестности с радиусом R. Если это число меньше некого T, то мы отбрасываем этот пиксель, то есть закрашиваем в белый цвет (в «оригинальном» медианном фильтре мы отбрасываем пиксель если у него меньше половины черных соседей, а у нас — порог T, так что на самом деле это квантильный фильтр, но сути это не меняет)

Я написал медианный фильтр честно и «в лоб». При таком написании он будет работать за O(WHR2), где W и H – размеры картинки. Преподаватель сообщил, что это можно сделать быстрее, использовав преобразование Фурье. И вправду, медианный фильтр можно выразить через свертку. А свертку можно вычислить быстро с помощью быстрого преобразования Фурье. Таким образом, у нас получается, что вычислить матрицу с количеством соседей можно как
result = FFT-1 (FFT(data) FFT(window)).
Это работает за O(WHlog(W+H)), не зависит от размеров окна, и в нашем случае работает немного быстрее наивной реализации. Из-за цикличности свертки возникает артефакт на границах: при обработки крайних левых пикселей: их соседями будут считаться крайние правые. Это можно победить, добавив рамку из чистых пикселей по краям изображения. А можно с этим и не бороться, что я и сделал — всё равно этот эффект незначителен и не наносит ущерба работоспособности.
bitbucket.org/rampeer/image-classifier/src/HEAD/filter_median.py [2]
Я обнаружил у этого фильтра нехорошее свойство: он “скругляет” острые углы треугольников. Из-за этого приходится держать радиус окна R довольно маленьким, и проходится по изображению фильтром только несколько (N) раз. Хотя поначалу казалось, что можно применять медианный фильтр до тех пор, пока он удаляет какие-то пиксели, но на практике этот способ не оправдал себя.
Бикомпонентный фильтр: берем произвольный черный пиксель, назначаем ему какое-то число Q. Затем назначаем это же число «черным» соседям этого пикселя в окрестности с радиусом R. Затем повторим этот процесс для соседей соседей. Будем это повторять до тех пор, пока имеются соседи (это напоминает действие инструмента «Заливка» в Paint, а красим в цвет Q). Затем увеличим число Q на единичку, выберем очередной пиксель без назначенного ему числа, и повторим процесс.
После выполнения этого алгоритма у нас получится набор несоприкасающихся островов. Мы можем с высокой достоверностью сказать, что самый большой остров – это и есть фигура, а остальные – помехи.

<filter_bicomp.py>
В отличие от предыдущего, этот фильтр не портит изображение. Он может нанести ущерб только если фигуру пересечет линия из помех шириной больше T, что маловероятно.
Я обнаружил у медианного фильтра ещё одно свойство, в этот раз — положительное. Он разбивает пространство, заполненное помехами, на “островки”. Значит, применив потом бикомпонентный фильтр, мы получим контур с прилепившимися помехами. После обработки бикомпонентным стоит ещё раз пройтись медианным, чтобы убрать оставшиеся неровности. Затем надо построить вокруг оставшихся точек выпуклый контур, заполнить его и вычислять метрики уже для него.
bitbucket.org/rampeer/image-classifier/src/HEAD/process.py [3]
Работа фильтров:
| Исходное изображение | Медианный фильтр | Медианный, бикомпонентный | Медианный, бикомпонентный, медианный, заливка |
![]() |
![]() |
![]() |
![]() |
Параметры фильтров подбираются исходя из размеров изображений в выборке и их зашумленности. Для хитрого четырехугольника, изображенного выше:
median.filter(img, 2, 6, 1)
bicomp.filter(img, 2)
median.filter(img, 2, 5, 2)
В нашей выборке изображения будут менее зашумлены, и настройки будут более щадящие.
В ходе всё той же дискуссии с коллегами мы придумали разные интересные метрики.
Подсчет углов. Самое первое, что приходит, наверное, каждому в голову после прочтения задачи. Но из-за помех могут появиться дополнительные углы, близкие к 0 градусам. Я безуспешно пытался бороться с этим с помощью установления порогов или путем «склейки» почти коллинеарных векторов. Такие методы тяжело настроить, а так как при фильтрации фигурка сглаживается, они всё равно могут давать некорректный результат. Лучше просуммировать квадраты синусов углов, а когда углы больше некого порога T– округлять квадрат вверх до единички. Это дает достаточно хороший результат: острые углы прибавляют к счетчику единичку, а углы, близки к 0, почти ничего не прибавляют. Кстати, мне показалось забавным, что в таком случае у треугольника количество углов может варьироваться от 2,5 до 4.

bitbucket.org/rampeer/image-classifier/src/HEAD/feature_edges.py [4]
Зеркалирование. Её смысл – посчитать, насколько фигура будет совпадать сама с собой при отражении по горизонтали/вертикали/вдоль обоих осей. То есть, эта метрика — своеобразная мера симметрии, совпадение изображения с перевернутым. Круг как ни крути – будет всегда полностью совпадать сам с собой. Также, я интуитивно предположил, что квадрат будет больше совпадать сам с собой больше, чем треугольник.

bitbucket.org/rampeer/image-classifier/src/HEAD/feature_mirror.py [5]
Отношение площади к квадрату периметра. Периметр необходимо возводить в квадрат, чтобы метрика не имела размерности и не зависела от размера изображения. Периметр и площадь будем считать от выпуклого контура. Круг имеет набольшее значение метрики среди фигур: s/p^2 =(πr^2)/(4π^2 r^2 )=1/4π. Для равностороннего треугольника (у него это соотношение самое большое среди треугольников): s/p^2 =(4√3 a^2)/(3*9a^2 )=(4√3)/27. Для квадрата: s/p^2 =(a^2)/(16*a^2 )=1/16.

bitbucket.org/rampeer/image-classifier/src/HEAD/feature_perimeter.py [6]
Метрики на описывающем прямоугольнике. Предыдущие метрики не очень хорошо разделяли четырех- и треугольник, и я решил придумать новую метрику. Построим ограничивающий прямоугольник вокруг фигуры. Для каждой стороны прямоугольника найдем первое (“минимальное”) и последнее (“максимальное”) пересечение с фигурой. У нас получится “восьмиугольник”, для которого можно вычислить разные метрики.

Например, отношение площади фигуры к площади описывающего квадрата (sbound).
bitbucket.org/rampeer/image-classifier/src/HEAD/feature_sbound.py [7]
Также многообещающей метрикой может быть отношение площади фигуры к площади этого восьмиугольника (sbound2):
bitbucket.org/rampeer/image-classifier/src/HEAD/feature_sbound2.py [8]
Применив полученные знания, я сгенерировал изображения и собрал статистику. В сгенерированные изображения я добавил изображения, представляющие собой потенциальные “плохие случаи” для метрик:
Этим случаям я дал большой вес при построении дерева. Дело в том, что вероятность случайной генерации таких картинок мала, а эти случаи являются граничными для некоторых метрик, и для их корректной классификации надо их добавить в выборку с большим весом.
Процедуру фильтрации изображения и сбора метрик я вынес в отдельный файл – она также понадобится для анализа. Кстати, в дереве решений наши метрики называют “входными параметрами” или “фичами” (feature).
bitbucket.org/rampeer/image-classifier/src/HEAD/process.py [3]
bitbucket.org/rampeer/image-classifier/src/HEAD/stats.py [9]
Затем я построил график, чтобы убедиться, что метрики достаточно хорошо различают фигурки. Для этого подойдет график типа “коробки с усиками”:

“Усики” пересекаются, а это означает, что возможны неточности при анализе. Как было написано выше, именно для точности нам нужны несколько метрик, а не одна. Дальше я попробовал убедиться, что «плохие случаи» метрик не пересекаются. Для этого я построил зависимость одной метрики от другой. Если получится, что они монотонно зависят друг от друга, то их “плохие случаи” также совпадут.

*Как мы видим по графикам, “облака” точек сильно пересекаются. Значит, при классификации возможна большая ошибка. Также, метрики не зависимы монотонно друг от друга.
По собранным данным можно построить дерево решений:
bitbucket.org/rampeer/image-classifier/src/HEAD/analyze.py [10]
Я попробовал визуализировать дерево. Получилось вот такая схема:

Из неё следует, что некоторые метрики остались не использованными. Мы не могли с самого начала предсказать, какие метрики окажутся “лучше” других.
Точность предсказания составляет примерно 91 процент, что неплохо, учитывая искажения квадратов и помехи в выборке:

Попробуем нарисовать изображения самостоятельно и проанализировать их:
![]() Circle |
![]() Rectangle |
![]() Triangle |
Попробуем повысить напряжение.
Будем искажать фигуры до тех пор, пока они не станут неправильно определяться.
![]() Rectangle |
![]() Rectangle |
![]() Triangle |
![]() Rectangle |
Вот и всё.
В последнем изображении углы треугольника сильно скруглены, edges не может верно работать, а perimeter дает слишком большую погрешность. Треугольник неудачно повернут: при построении ограничивающего прямоугольника только две вершины будут касаться его, и sbound и sbound2 не выдадут ничего разумного. Только mirror мог бы выдать корректный результат, но он не включен в дерево. Да и если из 5 метрик только одна укажет на треугольник, то её указание можно трактовать как ошибочное.
Методы машинного обучения позволили нам построить систему, которая хорошо справляется с поставленной задачей – она достаточно хорошо распознает фигурки на изображении.
Графики были построены в R:
bitbucket.org/rampeer/image-classifier/src/HEAD/charts.R [11]
Автор: dmstudent
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/data-mining/50828
Ссылки в тексте:
[1] bitbucket.org/rampeer/image-classifier/src/HEAD/picture_generator.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/picture_generator.py
[2] bitbucket.org/rampeer/image-classifier/src/HEAD/filter_median.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/filter_median.py
[3] bitbucket.org/rampeer/image-classifier/src/HEAD/process.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/process.py
[4] bitbucket.org/rampeer/image-classifier/src/HEAD/feature_edges.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/feature_edges.py
[5] bitbucket.org/rampeer/image-classifier/src/HEAD/feature_mirror.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/feature_mirror.py
[6] bitbucket.org/rampeer/image-classifier/src/HEAD/feature_perimeter.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/feature_perimeter.py
[7] bitbucket.org/rampeer/image-classifier/src/HEAD/feature_sbound.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/feature_sbound.py
[8] bitbucket.org/rampeer/image-classifier/src/HEAD/feature_sbound2.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/feature_sbound2.py
[9] bitbucket.org/rampeer/image-classifier/src/HEAD/stats.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/stats.py
[10] bitbucket.org/rampeer/image-classifier/src/HEAD/analyze.py: https://bitbucket.org/rampeer/image-classifier/src/HEAD/analyze.py
[11] bitbucket.org/rampeer/image-classifier/src/HEAD/charts.R: https://bitbucket.org/rampeer/image-classifier/src/HEAD/charts.R
[12] Источник: http://habrahabr.ru/post/205842/
Нажмите здесь для печати.