разработка универсальных приложений Windows
88,8
рейтинг
3 февраля в 18:42

Разработка → Распознаем эмоции в приложении UWP с помощью API Project Oxford tutorial



Скорее всего, вы слышали хоть раз про необычный облачный сервис от Microsoft, который позволяет распознавать по фотографии эмоции человека.

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

Попробовать самостоятельно распознать эмоции вы можете по следующей ссылке: Emotion Recognition
Доступно 8 эмоций: Счастье, Грусть, Страх, Нейтральность, Гнев, Отвращение, Презрение, Удивление.

Предлагаю вам создать C#/XAML приложение Windows 10, которое будет использовать API и распознавать эмоции по снимку с камеры.

Первым делом скачиваем SDK с сайта.
Находим в папке Emotion проект ClientLibrary.
Открываем его и строим. Во время построения восстанавливаются ссылки. Если ссылки не восстановились и возник запрос установить недостающие пакеты из NuGet, то устанавливаем вручную. Закрываем этот проект. Создаем проект универсального приложения Windows. Добавляем проект ClientLibrary в решение нашего UWP приложения, после чего добавляем ссылку на него (ссылку на проект Microsoft.ProjectOxford.Emotion). Строим решение.

Теперь нам необходимо получить ключ подписчика. Вы можете активировать бесплатную подписку под названием «Emotion API – Free», которая имеет следующие ограничения: 20 распознаваний в минуту и 10000 распознаваний в месяц.

Заходим на сайт проекта, входим с аккаунтом Microsoft, переходим на страницу Emotion APIs и нажимаем на кнопку «Try for free», после чего переходим на страницу подписок. Здесь нажимаем «Request New Keys» и выбираем подписку «Emotion API – Free». В результате получаем страницу на которой можем получить Primary Key. Без него приложение создать не получится.



Открываем приложение UWP и добавляем в манифест следующие возможности: Интернет (клиент и сервер), Веб-камера. Добавить можно как в графическом редакторе, так и через код, используя следующие тэги:

  <Capabilities>
    <Capability Name="internetClientServer" />
    <DeviceCapability Name="webcam" />
  </Capabilities>

Добавляем XAML разметку в MainPage:

    <ScrollViewer>
        <StackPanel Orientation="Vertical" HorizontalAlignment="Center">
            
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10,0,10">
            <Button x:Name="btnStartPreview" Background="Gray" 
                                           Click="btnStartPreview_Click" Margin="0,0,0,0" Content="Start preview"/>
            <Button x:Name="btnTakePhoto" Background="Gray" 
                                          Click="btnTakePhoto_Click" Margin="10,0,0,0" Content="Take a photo"/>
            <ProgressRing x:Name="progring" IsActive="False" Width="25" Height="25" Margin="10,0,0,0" />
        </StackPanel>
        <Grid>
          <Image x:Name="captureImage" Width="400" Height="400" Visibility="Visible"/>
          <CaptureElement x:Name="previewElement" Width="400" Height="400" Visibility="Visible"/>
        </Grid>

        </StackPanel>
    </ScrollViewer>

Так как не факт, что на экране устройства уместится все содержимое страницы, то я поместил его в ScrollViewer, чтобы можно было прокрутить при необходимости. В качестве содержимого выступают 2 кнопки и Grid, внутри которого наложены друг на друга 2 элемента – Image и CaptureElement. То есть одновременно может быть виден только один из них. Для того чтобы была возможность показать пользователю, что идет обработка данных, добавлен элемент ProgressRing.

Теперь перейдем к коду C#. Для того, чтобы каждый раз не упоминать пространства имен вынесем их:

using System.Threading.Tasks;
using Windows.Media.Capture;
using Windows.UI.Xaml.Media.Imaging;
using Windows.Media.MediaProperties;
using Windows.Storage.Streams;

using Microsoft.ProjectOxford.Emotion;
using Microsoft.ProjectOxford.Emotion.Contract;

Добавим переменные области видимости класса:

        MediaCapture mediaCapture;
        bool isPreviewing = false;
        Emotion[] emotionResult;
        public System.Collections.ObjectModel.ObservableCollection<Emotion> emo = 
                                           new System.Collections.ObjectModel.ObservableCollection<Emotion>();

С помощью элемента mediaCapture мы будем снимать видео и отображать preview. Переменная isPreviewing будет хранить текущее состояние просмотра. Массив элементов класса Emotion необходим для получения данных с сервиса. Потом первый элемент этого массива мы добавим в ObservableCollection. ObservableCollection нужна для того, чтобы была возможность привязать данные к элементам страницы. Привязывать данные к коллекции такого типа гораздо проще, чем привязывать к массиву.

Добавим в XAML страницу атрибут Loaded и реализуем событие, которому добавим атрибут async (так как инициализация объекта mediaCapture происходит асинхронно):

        private async void Page_Loaded(object sender, RoutedEventArgs e)
        {
            MediaCaptureInitializationSettings set = new MediaCaptureInitializationSettings();
            set.StreamingCaptureMode = StreamingCaptureMode.Video;
            mediaCapture = new MediaCapture();
            await mediaCapture.InitializeAsync(set);
        }

Так как мы не добавляли в манифест приложения возможность использования микрофона, то режимом захвата mediaCapture нужно установить только захват картинки StreamingCaptureMode.Video.

В коде XAML у нас 2 кнопки. Добавим реализацию события клика для одной из них:

        private async void btnStartPreview_Click(object sender, RoutedEventArgs e)
        {
            if (isPreviewing == false)
            {
                previewElement.Source = mediaCapture;
                await mediaCapture.StartPreviewAsync();
                isPreviewing = true;
            }
            previewElement.Visibility = Visibility.Visible;
        }

Здесь все должно быть понятно – показываем картинку с камеры в элементе previewElement. Опять же, добавляем async к событию, так как оно содержит асинхронную операцию. Перед тем как реализуем событие нажатия на вторую кнопку, хочу напомнить, что результат у нас будет в коллекции emo. Эту коллекцию мы привяжем к нашему XAML в такой вот разметке:

Длинный однотипный XAML код с биндингами
                <Grid HorizontalAlignment="Center">
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Row="0" Grid.Column="0" Foreground="Black" FontSize="14" TextAlignment="Right" 
                           Margin="0,0,5,0">Happiness</TextBlock>
                <ProgressBar Grid.Row="0" Grid.Column="1" IsIndeterminate="False" Width="200" Maximum="1" 
                             SmallChange="0.0001" LargeChange="0.1" Value="{Binding Scores.Happiness, Mode=OneWay}"  />

                <TextBlock Grid.Row="1" Grid.Column="0" Foreground="Black" FontSize="14" TextAlignment="Right" 
                           Margin="0,0,5,0">Anger</TextBlock>
                <ProgressBar Grid.Row="1" Grid.Column="1" IsIndeterminate="False" Width="200" Maximum="1" 
                             SmallChange="0.0001" LargeChange="0.1" Value="{Binding Scores.Anger, Mode=OneWay}"  />

                <TextBlock Grid.Row="2" Grid.Column="0" Foreground="Black" FontSize="14" TextAlignment="Right" 
                           Margin="0,0,5,0">Contempt</TextBlock>
                <ProgressBar Grid.Row="2" Grid.Column="1" IsIndeterminate="False" Width="200" Maximum="1" 
                             SmallChange="0.0001" LargeChange="0.1" Value="{Binding Scores.Contempt, Mode=OneWay}"  />

                <TextBlock Grid.Row="3" Grid.Column="0" Foreground="Black" FontSize="14" TextAlignment="Right" 
                           Margin="0,0,5,0">Disgust</TextBlock>
                <ProgressBar Grid.Row="3" Grid.Column="1" IsIndeterminate="False" Width="200" Maximum="1" 
                             SmallChange="0.0001" LargeChange="0.1" Value="{Binding Scores.Disgust, Mode=OneWay}"  />

                <TextBlock Grid.Row="4" Grid.Column="0" Foreground="Black" FontSize="14" TextAlignment="Right" 
                           Margin="0,0,5,0">Fear</TextBlock>
                <ProgressBar Grid.Row="4" Grid.Column="1" IsIndeterminate="False" Width="200" Maximum="1" 
                             SmallChange="0.0001" LargeChange="0.1" Value="{Binding Scores.Fear, Mode=OneWay}"  />

                <TextBlock Grid.Row="5" Grid.Column="0" Foreground="Black" FontSize="14" TextAlignment="Right" 
                           Margin="0,0,5,0">Neutral</TextBlock>
                <ProgressBar Grid.Row="5" Grid.Column="1" IsIndeterminate="False" Width="200" Maximum="1" 
                             SmallChange="0.0001" LargeChange="0.1" Value="{Binding Scores.Neutral, Mode=OneWay}"  />

                <TextBlock Grid.Row="6" Grid.Column="0" Foreground="Black" FontSize="14" TextAlignment="Right" 
                           Margin="0,0,5,0">Sadness</TextBlock>
                <ProgressBar Grid.Row="6" Grid.Column="1" IsIndeterminate="False" Width="200" Maximum="1" 
                             SmallChange="0.0001" LargeChange="0.1" Value="{Binding Scores.Sadness, Mode=OneWay}"  />

                <TextBlock Grid.Row="7" Grid.Column="0" Foreground="Black" FontSize="14" TextAlignment="Right" 
                           Margin="0,0,5,0">Surprise</TextBlock>
                <ProgressBar Grid.Row="7" Grid.Column="1" IsIndeterminate="False" Width="200" Maximum="1" 
                             SmallChange="0.0001" LargeChange="0.1" Value="{Binding Scores.Surprise, Mode=OneWay}"  />

            </Grid>


8 однотипных пар элементов внутри Grid-а. Значения ProgressBar-ов привязано с помощью {Binding Scores.Surprise} к значению коллекции emo.

Теперь давайте разберем по шагам код события btnTakePhoto_Click
Первым делом блокируем кнопки и считываем картинку с потока камеры, после чего отображаем картинку пользователю и скрываем предпросмотр видео. В переменную st получаем поток, который необходимо будет отправить сервису на анализ:

            btnTakePhoto.IsEnabled = false;
            btnStartPreview.IsEnabled = false;

            InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
            await mediaCapture.CapturePhotoToStreamAsync(ImageEncodingProperties.CreateJpeg(), stream);

            stream.Seek(0); // текущую позицию потока устанавливаем в 0
            BitmapImage bitmap = new BitmapImage();
            bitmap.SetSource(stream);
            captureImage.Source = bitmap;

            stream.Seek(0); // еще раз перегоняем позицию потока в 0
            Stream st = stream.AsStream(); // из InMemoryRandomAccessStream получаем System.IO.Stream

            if (isPreviewing == true) await mediaCapture.StopPreviewAsync();
            isPreviewing = false;
           previewElement.Visibility = Visibility.Collapsed;

Отправляем поток и получаем результат (асинхронно, разумеется):

             progring.IsActive = true; 
                try
                {
                    EmotionServiceClient emotionServiceClient = 
                            new EmotionServiceClient("12345678901234567890123456789012");
                    emotionResult = await emotionServiceClient.RecognizeAsync(st);
                }
                catch { }
            progring.IsActive = false;

Здесь вместо «12345678901234567890123456789012» указываем свой ключ, который получили ранее на сайте.
Далее считываем результат (если он есть) в коллекцию emo и активируем кнопочки:

            if ((emotionResult != null) && (emotionResult.Length > 0))
            {
                emo.Clear();
                emo.Add(emotionResult[0]);
                this.DataContext = emo.ElementAt(0);
            }
            btnStartPreview.IsEnabled = true;
           btnTakePhoto.IsEnabled = true;

После того, как DataContext будет установлен в первый элемент коллекции emo, значения ProgressBar-ов обновится автоматически. Кстати, обратите еще раз внимание, что в данном случае мы используем только первый элемент массива. Лиц на фото может быть несколько и соответственно элементов в массиве emotionResult[0] тоже несколько. Кроме эмоций, содержащихся в Score, массив содержит еще и параметры координат/размера лиц класса Rectangle.

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

Я опробовал приложение в действии и вот что у меня получилось:



Кстати, есть альтернатива сохранять снимок с камеры в файл вот таким образом:

// в манифесте необходимо разрешение на библиотеку изображений
            Windows.Storage.StorageFile photoFile = await Windows.Storage.KnownFolders.PicturesLibrary.CreateFileAsync(
                     "lastphoto.jpeg", Windows.Storage.CreationCollisionOption.ReplaceExisting);

            await mediaCapture.CapturePhotoToStorageFileAsync(ImageEncodingProperties.CreateJpeg(), photoFile);

            IRandomAccessStream photoStream = await photoFile.OpenReadAsync();
            BitmapImage bitmap = new BitmapImage();
            bitmap.SetSource(photoStream);
            captureImage.Source = bitmap;

            photoStream.Seek(0); // еще раз перегоняем позицию потока в 0
            Stream st = photoStream.AsStream(); // из IRandomAccessStream получаем System.IO.Stream

На основании этого примера легко написать код, который будет открывать файл, сохраненный на устройстве, и отправлять его на сервис определения эмоций.

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

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

Сервис облачный и кроссплатформенный, поэтому можно разрабатывать приложения не только под UWP. В Microsoft Garage даже разработали Android приложение под названием Mimicker Alarm, которое выключит звонок будильника только в случае если вы изобразите необходимую эмоцию.
Хотя мне, например, было бы сложно изобразить счастье в 6 утра.
Алексей Соммер @asommer
карма
41,2
рейтинг 88,8
разработка универсальных приложений Windows
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +2
    Если не секрет, то почему вы не делаете вот так:
    progring.IsActive = true; 
    try
    {
      EmotionServiceClient emotionServiceClient = new EmotionServiceClient("12345678901234567890123456789012");
      emotionResult = await emotionServiceClient.RecognizeAsync(st);
    }
    catch { }
    progring.IsActive = false;
    

    Зачем создавать лишний Task?
    • +1
      Хм. Действительно, зачем… Наверное, изначально WPF приложение создавал для пробы. Сча подправлю. Спасибо.
  • +1
    Тема определения половозрастных признаков, эмоций не новая. Над ней трудятся уже не один год. Есть реальные внедрения. Одним из лидеров является www.quividi.com

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