FaceLight — распознавание лиц в реальном времени в Silverlight 4

Опубликовано 24 марта 2010 г. 09:00 | Coding4Fun

В этой статье поясняется, как реализовать простую систему распознавания лиц, используя C# и новую функциональность для веб-камер в Silverlight 4.

Автор: Рене Шульте (René Schulte),
разработчик для .NET и Silverlight,
https://blog.rene-schulte.info
https://flavors.me/rschu
Исходный код: загрузить с CodePlex
Попробовать выполнить: прямо сейчас
Сложность: средняя
Необходимое время: 1–4 часа
Затраты: $0 – $30 (стандартная веб-камера)
ПО: Microsoft Visual Studio 2010 (достаточно редакции Express), Microsoft Silverlight 4 Tools for Visual Studio 2010
Оборудование: компьютер, стандартная веб-камера

Введение

API для веб-камер и микрофонов был первым, с чем я поиграл сразу после появления бета-версии Silverlight 4 на прошлогодней конференции PDC. На мой взгляд, это одно из интереснейших средств. И с его помощью в приложениях Silverlight можно делать много классных вещей.

Когда был выпущен SLARToolkit, у меня наконец появилось время для реализации распознавания лиц в реальном времени с помощью API веб-камер в Silverlight 4.

В этой статье я опишу простой способ распознавания лиц, при котором на снимке веб-камеры осуществляется поиск цветовых областей кожи определенного размера. Эта методика не столь совершенна, как профессиональные библиотеки компьютерного зрения вроде OpenCV и применяемые ими средства, подобные Haar, но она работает в реальном времени и для большинства веб-камер.

Демонстрационное приложение

Для запуска этого примера вам понадобится веб-камера и, как минимум, исполняющая среда Silverlight 4. На данный момент доступен релиз-кандидат для Windows и Mac. Для достижения наилучших результатов лицо должно быть хорошо освещено, а фон должен контрастировать с цветом кожи.

Открыть пример

clip_image002

Какпользоваться

Вы можете включать и выключать веб-камеру кнопкой clip_image003 или загружать изображение с диска кнопкой clip_image004. Используйте поле со списком (ComboBox) для смены режима демонстрации с «Highlight» на «Image». В режиме «Highlight» вокруг распознаваемого лица просто рисуется красный эллипс, а в режиме «Image» на область лица накладывается изображение. По умолчанию в качестве изображения используется голова обезьяны (рис. 11), но можно применить и другое изображение, введя его URI в TextBox. С помощью элементов управления Slider можно изменять пороговые значения цветов кожи в цветовом пространстве YCbCr (см. раздел «Этап 2: фильтрация цвета кожи»). Результат распознавания лица (включая наложенное изображение) сохраняется на диск кнопкой clip_image005.

В первый раз щелкнув кнопку clip_image003[1], вам понадобится разрешение на захват видеопотока. Мое приложение использует устройства захвата в Silverlight по умолчанию. В конфигурации Silverlight можно указать другие видео- и аудиоустройства по умолчанию. Просто щелкните правой кнопкой мыши в окне приложения, выберите Silverlight из контекстного меню и откройте вкладку Webcam/Mic.

Какэтоработает

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

clip_image007

Рис . 1. Шесть этапов

Этап 1: захват изображения с веб-камеры

Silverlight 4 Webcam API прост в использовании. Класс CaptureSource представляет поток данных от веб-камеры, и его можно задействовать как источник для VideoBrush, который в свою очередь заполняет прямоугольник для вывода видео, передаваемого с веб-камеры. Мы будем использовать устройство видеозахвата по умолчанию, но вы можете с помощью класса CaptureDeviceConfiguration перебрать все устройства захвата в системе. В конфигурации Silverlight можно задать видео- и аудиоустройства, применяемые по умолчанию, — достаточно щелкнуть правой кнопкой мыши в окне приложения Silverlight, выбрать Silverlight из контекстного меню и открыть вкладку Webcam/Mic.

Вот как выглядит инициализация веб-камеры:

C#

 // Создаем captureSource и используем устройство видеозахвата по умолчанию

    captureSource = new CaptureSource();

    captureSource.VideoCaptureDevice = CaptureDeviceConfiguration.GetDefaultVideoCaptureDevice();

    captureSource.CaptureImageCompleted += new         EventHandler<CaptureImageCompletedEventArgs>(captureSource_CaptureImageCompleted);

// Начинаем захват

    if (captureSource.State != CaptureState.Started)

    {

// Создаем видеокисть и заполняем с ее помощью прямоугольник WebcamVideo

        var vidBrush = new VideoBrush();

        vidBrush.Stretch = Stretch.Uniform;

        vidBrush.SetSource(captureSource);

        WebcamVideo.Fill = vidBrush;

// Запрашиваем необходимое разрешение для пользователя и начинаем сам процесс захвата

        if (CaptureDeviceConfiguration.RequestDeviceAccess())

        {

            captureSource.Start();

        }

    }

Теперь, подготовив веб-камеру к работе, нам нужно получать снимки из видеопотока для передачи в систему распознавания. Для этой задачи можно использовать собственную реализацию VideoSink, но CaptureSource также предоставляет встроенный метод CaptureImageAsync, который проще в применении. Его быстродействие не намного хуже, чем собственного VideoSink.

C #

 // Часть метода RunUpdate класса MainPage

    var dispatcherTimer = new DispatcherTimer();

    dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 40); // 25 кадров в секунду

    dispatcherTimer.Tick += (s, e) =>

    {

// Обработка снимка камеры, если начат захват

        if (captureSource.State == CaptureState.Started)

        {

// CaptureImageAsync генерирует событие captureSource_CaptureImageCompleted

           captureSource.CaptureImageAsync();

        }

    };

    dispatcherTimer.Start();

// Обработчик события captureSource_CaptureImageCompleted класса MainPage

    private void captureSource_CaptureImageCompleted(object sender, CaptureImageCompletedEventArgs e)

    {

// Обработка снимка с камеры

        Process(e.Result);

    }

Здесь мы используем DispatcherTimer для вызова метода CaptureImageAsync. При каждом выполнении захвата метод CaptureImageAsync генерирует событие CaptureImageCompleted, в EventArgs которого передается WriteableBitmap, содержащий полный снимок. DispatcherTimer инициализируется так, чтобы выполнять эту задачу через каждые 40 мс.

clip_image009

Рис . 2. Результат первого этапа — поток с веб-камеры

Этап 2: фильтрацияцветакожи

После получения изображения с веб-камеры нужно отфильтровать цвет кожи; это позволяет найти лицо на следующем этапе. WriteableBitmap, передаваемый через событие CaptureImageCompleted, использует цветовое пространство RGB для представления пикселей и на деле является простым 32-разрядным целочисленным массивом, где хранятся байтовые компоненты всех пикселей — альфа-значение, а также значения красного, зеленого и синего цветов (ARGB).

Цветовое пространство RGB имеет некоторые серьезные недостатки, когда дело доходит до фильтрации определенных диапазонов цвета. Например, нам нужно фильтровать цвет кожи надежным методом, который не слишком сильно влияет на яркость изображения, но в RGB это потребовало бы большей глубины цветового пространства — из-за этого в нем нельзя использовать простые пороги значений цветов.

К счастью, существуют другие цветовые пространства, которым не свойственны такие проблемы. Цветовое пространство HSV, например, определяет цвет тремя компонентами: Hue (оттенок), Saturation (насыщенность) и Value (значение) или Brightness (яркость), где реальные цвета (Hue) представлены в виде окружности от 0° до 360° и яркость является высотой цилиндра.

Поскольку цвета кожи колеблются в диапазонах от 0° до 60° и от 300° до 360° (что требует дополнительных вычислений), а преобразование RGB в HSV является более дорогостоящим с точки зрения вычислительных ресурсов, чем преобразования остальных цветовых пространств, мы используем цветовое пространство YCbCr для фильтрации цвета кожи. В YCbCr яркость хранится в компоненте Y, а цветовая составляющая (chroma) (сам цвет) — в компоненте Cb как разница по синему и в компоненте Cr как разница по красному. Преобразование RGB–YCbCr можно выполнить простыми операциями добавления и умножения. Компонент Y варьируется от 0 до 1, а Cb и Cr — от –0.5 до 0.5.

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

Y = [0, 1] Cb = [–0.15, 0.05] Cr = [0.05, 0.20]

В приложении-примере также есть элементы управления Slider (ползунки) для динамического изменения этих пороговых значений.

На рис. 3 изображен цветовой диапазон YCbCr, используемый для пороговых значений. Y является константой (0.5); нижнее пороговое значение Cb находится слева, а верхнее — справа; нижнее пороговое значение Cr расположено вверху, а верхнее — внизу.

clip_image010

Рис . 3. Цветовой диапазон YCbCr : Y =0.5 Cb =[-0.15, 0.05] Cr =[0.05, 0.20]

Для генерации битовой карты на рис. 3 использован следующий исходный код:

C#

 // Метод Visualize класса HistogramVisualizer

public void Visualize(WriteableBitmap surface)

{

    var w = surface.PixelWidth;

    var h = surface.PixelHeight;

    var pixels = surface.Pixels;

    var min = this.Min;

    var max = this.Max;

    int i;

    float xf, yf, cb, cr;

// Используем константу Y

    float v = min.Y + (max.Y - min.Y) * YFactor;

// Интерполируем между min и max и задаем пиксель

    for (int y = 0; y < h; y++)

    {

        for (int x = 0; x < w; x++)

        {

            i = y * w + x;

            xf = (float)x / w;

            yf = (float)y / h;

            cb = min.Cb + (max.Cb - min.Cb) * xf;

            cr = min.Cr + (max.Cr - min.Cr) * yf;

            pixels[y * w + x] = new YCbCrColor(v, cb, cr).ToArgbColori();

        }

    }

}

Так как в WriteableBitmap, принятом от камеры, используется цветовое пространство RGB, мы вынуждены конвертировать RGB в YCbCr до применения пороговых значений:

C#

 // Метод FromArgbColori класса YCbCrColor

public static YCbCrColor FromArgbColori(int color)

{

// Извлекаем RGB-компонент из color типа int и преобразуем в диапазон [0, 1]

    const float f = 1f / 255f;

    var r = (byte)(color >> 16) * f;

    var g = (byte)(color >> 8) * f;

    var b = (byte)(color) * f;

// Создаем новый YCbCr-цвет из RGB-цвета

    var y = 0.299f * r + 0.587f * g + 0.114f * b;

    var cb = -0.168736f * r + -0.331264f * g + 0.5f * b;

    var cr = 0.5f * r + -0.418688f * g + -0.081312f * b;

    return new YCbCrColor(y, cb, cr);

}

В ходе процесса создания пороговых значений каждый пиксель WriteableBitmap преобразуется из RGB в YCbCr и проверяется с использованием определенных верхних и нижних порогов:

C#

 // Метод Process класса ColorRangeFilter

public WriteableBitmap Process(WriteableBitmap snapshot)

{

    var p = snapshot.Pixels;

    var result = new WriteableBitmap(snapshot.PixelWidth, snapshot.PixelHeight);

    var rp = result.Pixels;

// Проверяем каждый пиксель

    for (int i = 0; i < p.Length; i++)

    {

        var ycbcr = YCbCrColor.FromArgbColori(p[i]);

        if (ycbcr.Y >= LowerThreshold.Y && ycbcr.Y <= UpperThreshold.Y

&& ycbcr.Cb >= LowerThreshold.Cb && ycbcr.Cb <= UpperThreshold.Cb

&& ycbcr.Cr >= LowerThreshold.Cr && ycbcr.Cr <= UpperThreshold.Cr)

        {

            rp[i] = 0xFFFFFF;

        }

    }

    return result;

}

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

clip_image012

Рис . 4. Результат второго этапа — двоичное изображение, отфильтрованное по цвету кожи

Этап 3: уменьшениешумаразмыванием

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

Размывание (Erosion) — распространенный фильтр изображения, применимый для нашей задачи. Морфологический оператор Erosion уменьшает границы окрашенных областей так, что очень малые области удаляются и остаются лишь более крупные. Он перебирает все пиксели WriteableBitmap и проверяет, являются ли пустыми (нулевыми) соседние пиксели вокруг текущего пикселя c. Если один из соседних пикселей пуст, текущий пиксель cявляется граничным, а значит, должен быть удален (установлен в черный). Сколько соседних пикселей проверяется, зависит от конкретной реализации. Такой набор проверяемых координатных точек обычно называют ядром в обработке изображений.

Так как на предыдущем этапе было получено черно-белое изображение, мы можем использовать простой двоичный оператор Erosion. Как оказалось, в нашем случае оптимально ядро размером 5 × 5. Цикл for, который обычно применяют для реализации универсального оператора Erosion, сводится к 25 операциям проверки пикселей (5 • 5 = 25) для большей производительности.

C#

 // Метод Process класса Erode5x5Filter

public WriteableBitmap Process(WriteableBitmap input)

{

    var p = input.Pixels;

    var w = input.PixelWidth;

    var h = input.PixelHeight;

    var result = new WriteableBitmap(w, h);

    var rp = result.Pixels;

    var empty = CompareEmptyColor; // = 0

    int c, cm;

    int i = 0;

// Размываем каждый пиксель

    for (int y = 0; y < h; y++)

    {

        for (int x = 0; x < w; x++, i++)

        {

// Средний пиксель

            cm = p[y * w + x];

            if (cm == empty) { continue; }

// Строка 0

// Левый пиксель

            if (x - 2 > 0 && y - 2 > 0)

            {

                c = p[(y - 2) * w + (x - 2)];

                if (c == empty) { continue; }

            }

// Средний левый пиксель

            if (x - 1 > 0 && y - 2 > 0)

            {

                c = p[(y - 2) * w + (x - 1)];

                if (c == empty) { continue; }

            }

            if (y - 2 > 0)

            {

                c = p[(y - 2) * w + x];

                if (c == empty) { continue; }

            }

            if (x + 1 < w && y - 2 > 0)

            {

                c = p[(y - 2) * w + (x + 1)];

                if (c == empty) { continue; }

            }

            if (x + 2 < w && y - 2 > 0)

            {

                c = p[(y - 2) * w + (x + 2)];

                if (c == empty) { continue; }

            }

// Строка 1

// Левый пиксель

            if (x - 2 > 0 && y - 1 > 0)

            {

                c = p[(y - 1) * w + (x - 2)];

                if (c == empty) { continue; }

            }

// ... 

// ... обрабатываем остальные 24 соседних пикселя

// ...

// Если все соседние пиксели обработаны, 

// очевидно, что текущий пиксель не является граничным

           rp[i] = cm;

        }

    }

return result;

}

clip_image014

Рис . 5. Результат третьего этапа — изображение с уменьшенным шумом

Этап 4: расширениеспомощьюрастяжения

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

Растяжение увеличивает границы и расширяет область; оно осуществляется перебором всех пикселей в WriteableBitmap. Но на этот раз выполняется проверка на то, что один из пикселей, соседствующих с текущим пикселем c, не является пустым. Если только один соседний пиксель не является пустым, текущий пиксель cполучает белый цвет.

Лучшие результаты достигаются при использовании ядра 5 × 5 и повторении процесса растяжения три раза.

C#

 // Метод Process класса Dilate5x5Filter, который применяется три раза

public WriteableBitmap Process(WriteableBitmap input)

{

    var p = input.Pixels;

    var w = input.PixelWidth;

    var h = input.PixelHeight;

    var result = new WriteableBitmap(w, h);

    var rp = result.Pixels;

    var r = this.ResultColor;

    var empty = CompareEmptyColor; // = 0

    int c, cm;

    int i = 0;

// Растягиваем каждый пиксель

    for (int y = 0; y < h; y++)

    {

        for (int x = 0; x < w; x++, i++)

        {

// Средний пиксель

            cm = p[y * w + x];

// Является ли пиксель пустым? 

// Если нет, мы сохраняем результат и переходим к следующему пикселю

            if (cm != empty) { rp[i] = r; continue; }

// Строка 0

// Левый пиксель

            if (x - 2 > 0 && y - 2 > 0)

            {

                c = p[(y - 2) * w + (x - 2)];

// Если лишь один из соседних пикселей не является пустым,

// сохраняем результат и переходим к следующему пикселю

                if (c != empty) { rp[i] = r; continue; }

            }

// Средний левый пиксель

            if (x - 1 > 0 && y - 2 > 0)

            {

                c = p[(y - 2) * w + (x - 1)];

                if (c != empty) { rp[i] = r; continue; }

            }

            if (y - 2 > 0)

            {

                c = p[(y - 2) * w + x];

                if (c != empty) { rp[i] = r; continue; }

            }

            if (x + 1 < w && y - 2 > 0)

            {

                c = p[(y - 2) * w + (x + 1)];

                if (c != empty) { rp[i] = r; continue; }

            }

            if (x + 2 < w && y - 2 > 0)

            {

                c = p[(y - 2) * w + (x + 2)];

                if (c != empty) { rp[i] = r; continue; }

            }

// Строка 1

// Левый пиксель

            if (x - 2 > 0 && y - 1 > 0)

            {

                c = p[(y - 1) * w + (x - 2)];

                if (c != empty) { rp[i] = r; continue; }

            }

// ... 

// ... обрабатываем остальные 24 соседних пикселя

// ...

        }

    }

    return result;

}

clip_image016

Рис . 6. Результат четвертого этапа после трехкратного применения оператора Dilation

Этап 5: нахождение лица с помощью сегментации на основе гистограммы

После фильтрации цветов кожи размытое и растянутое изображение является отличной отправной точкой для его сегментации — процесса деления изображения на множество наборов пикселей (сегментов). Сегментация изображения обычно используется для поиска местонахождения определенных объектов.

Сегментация выполняется самыми разными способами, но самый быстрый и простой — сегментация на основе гистограммы. Гистограмма изображения — статистическое представление всех пикселей, присутствующих в изображении. Функциональность для создания цветовых гистограмм есть в большинстве редакторов графики, и она обычно реализуется как график, на котором отражается количество определенных цветов в изображении. На рис. 7 показана цветовая гистограмма изображения-примера, где ось x графика представляет яркость каждого компонента цвета от 0 до 255, а ось y — количество пикселей каждой интенсивности.

clip_image017clip_image018

Рис . 7. Цветоваягистограммадляизображения - примера

Поскольку фильтрация цветов кожи дает двоичное изображение, гистограмма содержит только два значения для количества черных и белых пикселей, что не позволяет найти сегмент кожи лица. Чтобы исправить это, нам нужно узнать, где находится максимум белых пикселей по осям y (строки) и x (столбцы). Потом мы раздельно подсчитываем количество белых пикселей для строк и столбцов.

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

clip_image019

Рис . 8. Гистограмма строк и столбцов изображения с веб-камеры

C#

 // Метод FromWriteableBitmap класса Histogram

public static Histogram FromWriteabelBitmap(WriteableBitmap input)

{

    var p = input.Pixels;

    var w = input.PixelWidth;

    var h = input.PixelHeight;

    var histX = new int[w];

    var histY = new int[h];

    var empty = CompareEmptyColor; // = 0

// Создаем статистику строк и столбцов (гистограмму)

    for (int y = 0; y < h; y++)

    {

        for (int x = 0; x < w; x++)

        {

            if (p[y * w + x] != empty)

            {

                histX[x]++;

                histY[y]++;

            }

        }

    }

    return new Histogram(histX, histY);

}

// Конструктор класса Histogram, используемый в методе FromWriteableBitmap

public Histogram(int[] histX, int[] histY)

{

    X = histX;

    Y = histY;

// Находим максимальное значение и индекс (координату) для x

    int ix = 0, iy = 0, mx = 0, my = 01;

    for (int i = 0; i < histX.Length; i++)

    {

        if (histX[i] > mx)

        {

            mx = histX[i];

            ix = i;

        }

    }

// Находим максимальное значение и индекс (координату) для y

    for (int i = 0; i < histY.Length; i++)

    {

        if (histY[i] > my)

        {

            my = histY[i];

            iy = i;

        }

    }

// Сохраняем результаты в переменных-членах

    Max = new Vector(mx, my);

    MaxIndex = new Vector(ix, iy);

}

Код, который рисует гистограмму строк и столбцов, показанную на рис. 8, использует метод DrawLine из библиотеки WriteableBitmapEx:

C#

 // Метод Visualize класса HistogramVisualizer

public void Visualize(WriteableBitmap surface)

{

    var w = surface.PixelWidth;

    var h = surface.PixelHeight;

    var scale = this.Scale;

    var histogram = this.Histogram;

    var histX = histogram.X;

    var histY = histogram.Y;

// Гистограмма по оси X

    for (int x = 0; x < w; x++)

    {

        var hx = histX[x];

        if (hx != 0)

        {

            var norm = (int)(((float)hx / histogram.Max.X) * scale);

            surface.DrawLine(x, h - 1, x, h - norm, Colors.Green);

        }

    }

// Рисуем максимум

    surface.DrawLine(histogram.MaxIndex.X, h - 1, histogram.MaxIndex.X, 0, Colors.Yellow);

// Гистограмма по оси Y

    for (int y = 0; y < h; y++)

    {

        var hy = histY[y];

        if (hy != 0)

        {

            var norm = (int)(((float)hy / histogram.Max.Y) * scale);

            surface.DrawLine(w - 1, y, w - norm, y, Colors.Blue);

        }

    }

// Рисуем максимум

    surface.DrawLine(w - 1, histogram.MaxIndex.Y, 0, histogram.MaxIndex.Y, Colors.Yellow);

}

clip_image021

Рис . 9. Результат определения по гистограмме строк и столбцов

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

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

C#

 // Метод Process класса HistogramMinMaxSegmentator

public IEnumerable<Segment> Process(WriteableBitmap input)

{

    var hx = Histogram.X;

    var hy = Histogram.Y;

    var histUpperThreshold = Histogram.Max * 0.5f;

// Находим зародышевые значения для сегментации:

// все индексы на гистограмме по осям X и Y, где значение выше

// половины максимума и имеет минимальное расстояние

    const int step = 10;

// x

    var ix = GetIndicesAboveThreshold(Histogram.MaxIndex.X, -step, hx, histUpperThreshold.X);

    ix.AddRange(GetIndicesAboveThreshold(Histogram.MaxIndex.X + step, step, hx, histUpperThreshold.X));

// y

    var iy = GetIndicesAboveThreshold(Histogram.MaxIndex.Y, -step, hy, histUpperThreshold.Y);

    iy.AddRange(GetIndicesAboveThreshold(Histogram.MaxIndex.Y + step, step, hy, histUpperThreshold.Y));

// Находим границы для сегментов, определенных зародышевыми значениями

    var segments = new List<Segment>();

    foreach (var y0 in iy)

    {

        foreach (var x0 in ix)

        {

            var segment = new Segment(0, 0, 0, 0);

            segment.Min.X = GetIndexBelowThreshold(x0, -1, hx, ThresholdLuminance.X);

            segment.Max.X = GetIndexBelowThreshold(x0, 1, hx, ThresholdLuminance.X);

            segment.Min.Y = GetIndexBelowThreshold(y0, -1, hy, ThresholdLuminance.Y);

            segment.Max.Y = GetIndexBelowThreshold(y0, 1, hy, ThresholdLuminance.Y);

            segments.Add(segment);

        }

    }

// Упорядочиваем, начиная с самого крупного сегмента

    return segments.OrderByDescending(s => s.DiagonalSq);

}

// Метод GetIndicesAboveThreshold класса HistogramMinMaxSegmentator

private List<int> GetIndicesAboveThreshold(int start, int step, int[] hist, int threshold)

{

    var result = new List<int>();

    int hi;

    for (int i = start; i < hist.Length && i > 0; i += step)

    {

        hi = hist[i];

        if (hi > threshold)

        {

            result.Add(i);

        }

    }

    return result;

}

// Метод GetIndexBelowThreshold класса HistogramMinMaxSegmentator

private int GetIndexBelowThreshold(int start, int step, int[] hist, int threshold)

{

    int result = start, hi;

    for (int i = start; i < hist.Length && i > 0; i += step)

    {

        hi = hist[i];

        result = i;

        if (hi < threshold)

        {

            break;

        }

    }

    return result;

}

Этап 6: наложение распознанного лица

Получив информацию о позиции лица, мы наконец можем сделать что-то полезное и интересное с этими данными.

Выделениеобластилица

Информация, вычисленная на предыдущем этапе, содержит координаты x и y центра и высоты/ширины сегмента. Выделить область лица с помощью этих данных довольно легко (рис. 10): мы используем пустое изображение, которое накладывается на вывод с веб-камеры, и рисуем красный эллипс на битовой карте изображения методом DrawEllipse из библиотеки WriteableBitmapEx.

clip_image022

Рис . 10. Выделенная область лица

C#

 // Метод Overlay класса MainPage

private void Overlay(IEnumerable<Segment> foundSegments, int w, int h)

{

// Обводим найденные сегменты красным эллипсом

    var result = new WriteableBitmap(w, h);

    foreach (var s in foundSegments)

    {

// Используем центр сегмента, а также половину ширины и высоты

        var c = s.Center;

        result.DrawEllipseCentered(c.X, c.Y, s.Width >> 1, s.Height >> 1,         Colors.Red);

    }

    ImgResult.Source = result;

}

Наложение другого изображения на область лица

Выделение лица — это хорошо, хоть и не особенно интересно, но мы можем использовать информацию, полученную при сегментировании, и для наложения другого изображения поверх лица и перемещения его в соответствующую позицию или масштабирования до подходящего размера. В первой версии приложения я взял фото Чака Нориса (Chuck Norris), но потом отказался от него (иначе получилось бы незаконное использование). К счастью, я нашел забавный снимок орангутанга, сделанный Итаном Хейном (Ethan Hein) и опубликованный по лицензии Creative Commons. Накладываемым изображением по умолчанию является голова обезьяны (рис. 11), но вы могли бы применить любую другую картинку, введя ее URI в TextBox.

C#

 // Метод TransformOverlaidImage класса MainPage

private void TransformOverlaidImage(IEnumerable<Segment> foundSegments, int w, int h)

{

// Задаем ширину и высоту изображения, используя информацию о первом сегменте

    var s = foundSegments.First();

    var iw = s.Width;

    var ih = s.Height;

    ImgOverlay.Width = iw;

    ImgOverlay.Height = ih;

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

    var transform = new TransformGroup();

// Масштабируем изображение и перемещаем его в позицию сегмента

    transform.Children.Add(new ScaleTransform { 

    ScaleX = 1.5, 

    ScaleY = 1.5, 

    CenterX = iw >> 1, 

    CenterY = ih >> 1 });

    transform.Children.Add(new TranslateTransform { X = s.Min.X, Y = s.Min.Y + 10 });

// Вычисляем по битовой карте, какой размер нам нужен, и создаем преобразование

    var sx = (GrdContent.ActualWidth / w);

    var sy = (GrdContent.ActualHeight / h);

    transform.Children.Add(new ScaleTransform { ScaleX = sx, ScaleY = sy });

// Применяем преобразование

    ImgOverlay.RenderTransform = transform;

}

Вуаля, вот вам обезьяна в моей одежде:

clip_image024

Рис . 11. Результат шестого этапа после наложения головы орангутанга

Дополнительныевозможности

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

Получение конечного снимка

Получить снимок с конечным результатом (в том числе с наложенным изображением) можно с помощью кнопки clip_image005[1], а затем отправить его своим друзьям или использовать при очередной смене паспорта. Код, реализующий эту функциональность, опирается на метод WriteTga из библиотеки WriteableBitmapEx, с помощью которого он сохраняет результат в файл изображения в формате TGA.

C#

 // Метод BtnSnapshot_Click класса MainPage

private void BtnSnapshot_Click(object sender, RoutedEventArgs e)

{

// Рендеринг

    var wb = new WriteableBitmap(GrdContent, null);

// Инициализация SaveFileDialog

    var saveFileDlg = new SaveFileDialog

    {

        DefaultExt = ".tga",

        Filter = "TGA Image (*tga)|*.tga",

    };

// SaveFileDialog.ShowDialog() можно вызывать только из UI-кода

// (как и обработчик события), иначе будет сгенерировано исключение SecurityException

    if (saveFileDlg.ShowDialog().Value)

    {

        using (var dstStream = saveFileDlg.OpenFile())

        {

            wb.WriteTga(dstStream);

        }

    }

}

Загрузка изображения для распознавания

Если у вас нет веб-камеры или вы просто хотите опробовать clip_image004[1]распознавание лиц на какой-то фотографии, можете загрузить изображение с диска кнопкой . Код, поддерживающий эту кнопку, открывает файловый диалог и заполняет WriteableBitmap данными из этого изображения. Если оно большего размера, вызывается метод Resize из библиотеки WriteableBitmapEx.

C#

 // Метод BtnOpenPic_Click класса MainPage

private void BtnOpenPic_Click(object sender, RoutedEventArgs e)

{

// SaveFileDialog.ShowDialog() можно вызывать только из UI-кода

// (как и обработчик события), иначе будет сгенерировано исключение SecurityException

    var openFileDialog = new OpenFileDialog();

    if (openFileDialog.ShowDialog().Value)

    {

// Открываем поток данных и загружаем изображение

        using (var stream = openFileDialog.File.OpenRead())

        {

// Заполняем WriteableBitmap из этого потока

            var bmpImg = new BitmapImage();

            bmpImg.SetSource(stream);

            loadedPicture = new WriteableBitmap(bmpImg);

// Масштабируем изображение, если оно слишком велико

            var w = (int)GrdContent.Width;

            var h = (int)GrdContent.Height;

            if (loadedPicture.PixelWidth > w || loadedPicture.PixelHeight > h)

            {

                loadedPicture = loadedPicture.Resize(w, h, WriteableBitmapExtensions.Interpolation.Bilinear);

            }

        }

    }

}

Заключение

В этой статье было продемонстрировано, как реализовать простую систему распознавания лиц с помощью новой поддержки веб-камер в Silverlight и базовой сегментации цветов кожи на основе гистограммы. Эта методика несовершенна, но работает в реальном времени и годится для большинства случаев. Здесь, безусловно, есть много возможностей для улучшения метода сегментации и повышения скорости работы.

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

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

Обавторе

clip_image025Рене Шульте (René Schulte) — разработчик ПО для .NET и Silverlight из Дрездена (Германия). Он неравнодушен к компьютерной графике, моделированию физических объектов, искусственного интеллекта и к алгоритмам, а также любит C#, шейдеры, Augmented Reality и WriteableBitmap. Он является инициатором Silverlight-проектов SLARToolkit, WriteableBitmapEx и Matrix3DEx с открытым исходным кодом и создал веб-сайт на основе Silverlight, где используются операции над физическими объектами в реальном времени. Контактную информацию можно найти на его веб-сайте на основе Silverlight, в блоге или на Twitter.