Перевод поста Matthias Odisio "Seeing Skin with Mathematica".
Скачать файл, содержащий текст статьи, интерактивные модели и весь код, приведенный в статье, можно здесь.
Выражаю огромную благодарность Кириллу Гузенко за помощь в переводе.

Детекция кожи может быть довольно полезной — это один из основных шагов к более совершенным системам, нацеленным на обнаружение людей, распознавание жестов, лиц, фильтрации на основе содержания и прочего. Несмотря на всё вышеперечисленное, моя мотивация при создании приложения заключалась в другом. Отдел разработки и исследований в Wolfram Research, в котором я работаю, подвергся небольшой реорганизации. С моими коллегами, которые занимаются вероятностями и статистикой, которые стали находиться ко мне значительно ближе, я решил разработать небольшое приложение, которое использовало бы как функционал по обработке изображений в Mathematica, так и статистические функции. Детекция кожи — первое, что пришло мне в голову.

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

Skin detection model

Давайте рассмотрим эту задачу с точки зрения теории вероятностей. Мы хотели бы оценить вероятность того, что пиксель принадлежит области с кожей заданного цвета. Используя формулу Байеса, мы можем выразить это так (назовём это уравнением 1):

Equation 1

Обратите внимание на то, что в этом посте вероятности обозначаются заглавной P[.].

Три условия в правой части уравнения могут быть представлены в вычислимой форме. Тут нам пригодятся вероятностный подход и эти два обучающих набора данных: первый состоит из пикселей, принадлежащих областям с кожей, а второй — из пикселей не принадлежащих. Мы обучим статистическую модель и получим формулу для P[color|skin]. Оценка априорной P[skin] может быть произвольной в том смысле, что она зависит от конечного приложения, для которого разрабатывается детектор кожи. Мы пойдём простым путём и определим априорную вероятность через отношение количества пикселей из двух обучающих наборов данных. С P[color] сначала будет больше проблем, потому что надежное моделирование вероятностей для каждого возможного цвета потребует огромного обучающего набора данных. К счастью, формула полной вероятности позволяет нам обойти эту проблему путём разложения этого члена следующим образом:



Наконец, наш вероятностный детектор кожи будет реализован формулой (назовём её уравнением 2):

Equation 2

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

Мне не понаслышке известно, что создание подобных подборок — нечто вроде искусства, и требует большого количества усилий. Однако на этот раз мы не будем особо усердствовать и соберём вместо этого лишь с дюжину изображений, на которых изображена кожа. Конечно, для серьёзных статистически значимых моделей потребовались бы сотни изображений и сложные модели кожи.

Используя меню Graphics в Mathematica, я могу вручную выбрать области с кожей в изображении и повторить процесс для подборок с изображениями — честно говоря, не самая интересная часть моего дня:

Selecting regions of skin

Есть сведения, что стандартное цветовое пространство RGB не является лучшим цветовым пространством для реализации моделей с разделением цветов по критерию кожа/не кожа. Большинство проблем может возникать из-за того, что при разном освещении и разных чертах лица кожа выглядит по разному.

Как можно увидеть, цвета кожи и всего помимо неё (изображённые в красном и зелёном цветах соответственно) не сильно перекрываются в изображениях в выборке, но при этом охватывают большую часть трёхмерного пространства RGB:

3D space of skin and non-skin colors

Давайте поэкспериментируем с другим цветовым пространством, где подобные изменения в том, как выглядит кожа, моделируются, пожалуй, более надёжно.

В качестве цветового пространства можно выбрать CIE XYZ, а цвета кожи представить в двух цветовых и одной яркостной координатах. Чтобы разработать модели, более устойчивые к изменениям в условиях освещения, мы будем работать лишь с цветовыми координатами x и y, определяемыми как x = X / (X + Y + Z) и y = Y / (X + Y + Z):
Функция colorConvertxy дает двухканальное xy изображение:

colorConvertxy[img_] :=    ImageApply[Most[#]/(Total[#] + $MachineEpsilon) &,     ColorConvert[img, "XYZ"]];

Извлечение списка пар xy регионов с кожей осуществляется с использованием самого изображения, маски областей кожи и функций PixelValuePositions и PixelValue. Список пар xy областей изображения без кожи можно получить точно таким же способом.

Extracting the list of non-skin xy pairs

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

Building a 3D histogram to illustrate where skin and non-skin regions are in the dataset

3D histogram of skin and non-skin regions

Работа в двумерном xy цветовом пространстве представляется многообещающей. Давайте вернёмся назад и представим основанные на данных статистические модели для уравнения 2. Для удобства читателя приведём формулу повторно:

Equation 2

Доля пикселей с кожей среди миллиона пикселей обучающего набора данных составляет примерно 13%. Сохраним это эмпирическое значение в нашей модели для априорной вероятности P[skin]:

pskin = N[Length[skinxy]] / (Length[skinxy] + Length[nonskinxy])

0.12706

Для моделирования функций плотности вероятностей P[color|skin] и P[color|nonskin] приходит на ум использовать некую смесь распределений Гаусса. Эти распределения вполне могут подойти для данных пар xy, представленных выше. Однако на моем ноутбуке вычисления совершаются немного быстрее при выборе модели, основанной на распределениях со сглаженным ядром ядерном (smooth kernel distributions), которые строятся с помощью функции SmoothKernelDistribution:

pcolorskin = SmoothKernelDistribution[skinxy];

pcolornonskin = SmoothKernelDistribution[nonskinxy];

Наконец, вероятность того, что данный цвет в xy соответствует коже, можно представить через следующую функцию:

probabilityskin =    Function[{x, y},     Evaluate[(pskin PDF[         pcolorskin, {x, y}])/((1 - pskin) PDF[pcolornonskin, {x, y}] +         pskin PDF[pcolorskin, {x, y}] + $MachineEpsilon)]];

Plot3D[probabilityskin[x, y], {x, 0, 1}, {y, 0, 1}]

3D plot of the probability that a given xy color corresponds to skin

Как будет работать представленная модель? Давайте применим функцию к нескольким тестовым изображениям:

skinness[image_] :=      ImageApply[probabilityskin[Sequence @@ #] &, colorConvertxy[image]];

Applying the skin detection function

Final result with skin detected

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

Running the skin detection app

Example image with skin being detected

Всё почти готово. По сути, детекция кожи требует принятия решения о том, принадлежит ли пиксель коже. Для создания подобного бинарного изображения мы могли бы установить порог для полученных нами изображений. Однако какой порог выбрать? Если выбрать слишком высокий, то пиксели кожи могут не обнаружиться. Если выбрать слишком низкий, то велико будет количество определённых как кожа пикселей без кожи. Всё это ведёт к построениям и анализу ROC-кривых. Да, было бы здорово воссоздать подобный функционал с Mathematica, однако я считаю, что данная тема заслуживает большего внимания. В общем, в другой раз, в другом посте.

В альтернативной стратегии предлагается сравнивать вероятности для пикселя быть частью области с кожей или нет. Как и в прошлый раз, давайте воспользуемся вероятностным подходом и пересмотрим уравнение 1: P[skin|color] == P[color|skin]*P[skin]/P[color]. Точно так же можно выразить апостериорную вероятность того, что данный пиксель не принадлежит области с кожей:

P[nonskin|color] ==   P[color|nonskin]*P[nonskin]/P[color]

Чтобы отсортировать этот цвет по критерию кожа/не кожа, нам нужно просто сравнить полученные апостериорные вероятности и выбрать большую. P[color] можно отбросить, и тогда пиксель будет определяться как принадлежащий области с кожей, если P[color|skin]*P[skin] > P[color|nonskin]*P[nonskin].

Вот как работает на данный момент детектор кожи:

Building the skin detector

Быстрая оценка тестовых изображений позволяет заключить, что система вполне справляется с поставленной задачей:

Performing the analysis on two test images

Two test images with skin detected

Давайте завершим эту тему приложением по детекции кожи:

Showing the skin detection app

Example images with skin being highlighted

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

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


  1. Arastas
    29.06.2015 23:45

    Роман, помнится, Вы обещали, что на смену переводам придут авторские тексты по мотивам реальных проектов. Как обстоят дела на этом направлении?


    1. OsipovRoman Автор
      30.06.2015 00:54

      Все кто могут их писать, и я в том числе, в настоящий момент очень сильно заняты в своих проектах. Написание одного поста отнимает очень много времени, а писать отписки на пол странички не хочется.

      От себя могу сказать, что я планирую написать несколько постов: один о применении технологий Wolfram в нашем издательстве Баласс, второй о работе с компанией Эвика над разработкой самообучающихся систем для умных домов, реализованной на связке Wolfram Cloud + LogicMachine + Lua, третий о своих предыдущих работах, скажем, в кинокомпании и с фармкомпанией.

      Леонид Шифрин говорил мне о том, что в скором времени возьмет отпуск и напишет несколько статей. Возможно, мы займемся переводом его первоклассного учебника по Wolfram Language.

      Также есть несколько статей, которые ждут своего оформления и публикации различных участников Русскоязычной поддержки, а также скоро мы опубликуем 16 презентаций и столько же часов видео-лекций с 3-й Российской конференции Технологии Wolfram, на которой выступал в том числе директор Wolfram|Alpha — Майкл Тротт.


    1. OsipovRoman Автор
      30.06.2015 00:58

      Переводов все равно будет, конечно, больше. В этом году по крайней мере.


  1. imwode
    30.06.2015 17:30
    +1

    Адский язык и адские туториалы. Часа три вчера разбирался, пока смог простейший график нарисовать. Ну что это за интуитивность:

    & /@


    И что они говорят про натуральный язык, когда ему пишешь 5 largest cities in Russia, и он этого не понимает? Что, правда надо писать

    Take[CountryData[«Russia», «LargestCities»], 5]


    ??? А как быть со страной, где меньше пяти городов, как обрабатывать ошибку?


    1. OsipovRoman Автор
      30.06.2015 18:13

      Мне трудно прокомментировать ваши действия и я не знаю, какие обучающие материалы вы использовали.

      Что касается приведенной вами конструкции, то это просто применение чистой функции к списку. Символ & без тела чистой функции не существует.

      Скажем:

      #^2&/@{1, 2, 3} == {1, 4, 9}

      Конструкция /@ это сокращенная форма оператора Map:

      Map[#^2&, {1,2,3}] == {1, 4, 9}

      #^2& — объекты такого вида, это чистые функции, как, скажем, в Lisp. Их можно записать иначе:

      #^2& == Function[#^2] == Function[x, x^2]

      Таким образом, можно совсем интуитивно записать выражение:

      Map[Function[x, x^2], {1,2,3}] == {1, 4, 9}

      По поводу вашего второго вопроса. Если вы используете интеграцию с Wolfram|Alpha, то да, все будет работать очень просто.

      Если же вы обращаетесь программно к встроенной базе, то возможна такая конструкция, с обработкой возможной ошибки:

      With[{cities = CountryData[«Russia», «LargestCities»]}, If[Length[cities>=5], cities[[1;;5]], cities]]


      1. buriy
        02.07.2015 08:11
        +1

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