Пишем веб-приложение для распознавания лиц за час

Рассказывает Деван Сабаратнам, разработчик с 30-летним стажем


В минувшие выходные, пролистывая Amazon Web Services, я заметил новый сервис под названием «Rekognition». Я предположил, что это опечатка (recognition — англ. распознавание), но она привлекла мое внимание. Я заинтересовался: что это за сервис? Amazon привык добавлять новые сервисы в свою платформу с пугающей регулярностью, и этот я пропустил.

Я узнал, что в конце 2016 года Amazon выпустила свой собственный сервис для распознавания изображений на базе глубокого обучения на их платформе. Он может распознавать не только лица, но и объекты на фото. Так как сервис довольно новый, подробностей о нём было немного, но мне захотелось немедленно попробовать его. В общем, в течение часа я написал пример веб-страницы, которая может получать фотографии с моей веб-камеры и выполнять элементарное распознавания лица на нем.

Прим. перев. Чтобы не теряться в многочисленных сервисах Amazon, советуем вам прочитать нашу шпаргалку по AWS.

Раньше я занимался технологией распознавания лиц, используя сторонние библиотеки, а также Microsoft Face API, но все попытки создания подобного приложения не увенчались успехом. Но, просматривая документацию «Rekognition», я понял, что AWS API на самом деле очень прост в использовании. Я немедленно принялся за работу.

Цель

Мне была нужна простая веб-страница, которая позволила бы делать фотографию с помощью камеры моего iMac и выполнять распознавание на фотографии. В частности, я хотел бы определять пользователя, сидящего перед компьютером.

Сервис Amazon Rekognition позволяет создавать одну или несколько коллекций. Коллекция — это набор лицевых векторов для фотографий, которые вы хотите сохранить.

Примечание: Сервис сохраняет не фотографии, а их JSON-представление.

После создания коллекции вы можете сфотографировать предмет, сравнить его свойства c сохранёнными и вернуть ближайшее соответствие. Звучит просто, не так ли? По правде говоря, разработка фронтенда веб-страницы для получения данных с камеры заняла больше времени, чем написание бэкенда для распознавания.

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

Фронтенд

Я не знал, какую библиотеку использовать для захвата изображения с помощью камеры iMac. В итоге я нашел на GitHub библиотеку JPEG Camera, которая позволяет использовать HTML5 Canvas или Flash для съёмки фото. Я решил использовать её и настроил под себя написанный на JavaScript образец.

Бэкенд

Для бэкенда я использовал Ruby-библиотеку Sinatra, которая может выполнять всю тяжёлую работу с помощью AWS. Я часто использовал Sinatra (на самом деле Padrino) в своих проектах и настоятельно рекомендую эту платформу.

Примечание: Amazon Rekognition предлагает сначала загружать исходные фотографии, которые использует его API, в Amazon S3, а затем обрабатывать их. Я хотел избежать этого ненужного шага и вместо этого отправлять изображения непосредственно в API, что мне в итоге удалось сделать.

Я сумел сделать то же самое с их приветствием Polly . Вместо того, чтобы сохранять аудио в MP3-файл и проигрывать его, у меня получилось закодировать данные MP3 непосредственно в тег <audio> на странице и воспроизвести их оттуда.

Код

Я разместил весь код этого проекта на моей странице на GitHub. Не стесняйтесь использовать и изменять его. Ниже я постараюсь объяснить код более подробно.

Пишем приложение

Прежде всего, вам понадобится учетная запись Amazon AWS. Я не буду вдаваться в подробности, поскольку это несложно, а в случае затруднений можно легко найти информацию в Интернете.

Создание пользователя AWS IAM

Как только вы создали учетную запись AWS, первое, что нам нужно сделать, — создать пользователя Amazon IAM (Identity & Access Management), который имеет права на использование службы Rekognition. Мы также зададим права для Amazon Polly.

В консоли Amazon нажмите на кнопку «Сервисы» в верхнем левом углу, затем выберите «IAM». В меню слева выберите «Пользователи». Вы должны увидеть список существующих пользователей IAM, которых вы создали в консоли, если вы делали это в прошлом.

Нажмите кнопку «Добавить пользователя», расположенную в верхней части списка, чтобы добавить нового пользователя IAM.

Дайте пользователю имя и убедитесь, что вы отметили пункт «Programmatic Access», так как вы будете использовать этот IAM в вызове API.

Далее приведены настройки разрешений. Убедитесь, что вы щелкаете по третьему квадратику на экране с надписью «Attach existing policies directly». Затем в поле поиска «Filter: Policy Type» введите «rekognition». Выберите «AmazonRekognitionFullAccess» из списка, поставив рядом с ним галочку.

Затем измените фильтр поиска на «polly» и поместите галочку рядом с «AmazonPollyFullAccess».

Теперь у этого IAM есть права, достаточные для Amazon Rekognition и Amazon Polly. Нажмите «Next: Review» в правом нижнем углу.

На странице просмотра вы должны увидеть 2 политики использования, дающие вам полный доступ к Rekognition и Polly. Если вы их не видите, вернитесь и повторите предыдущий шаг. Затем нажмите кнопку «Создать пользователя» в правом нижнем углу.

Эта страница важна. Запишите ключи AWS Key и AWS Secret, которые вы указали на этой странице, поскольку нам необходимо включить их в наше приложение ниже.

Это единственный раз, когда вам будут показаны эти ключи, поэтому сохраните их и файл CSV с этой страницы в надёжном месте.

Загрузка кода

Теперь загрузите пример кода с моей страницы GitHub, чтобы вы могли изменить его по мере необходимости. Загрузите код в виде ZIP-файла, либо склонируйте его в вашу рабочую папку.

Первое, что вам нужно сделать, — это создать файл с именем .env в рабочей папке и ввести эти две строки, заменив в них ключи Amazon IAM на свои:

export AWS_KEY=A1B2C3D4E5J6K7L10

export AWS_SECRET=T/9rt344Ur+ln89we3552H5uKp901

Теперь, если у вас установлен Ruby (не Ruby on Rails), то для установки зависимостей запустите команду:

bundle install

Для запуска приложения введите эту команду:

ruby faceapp.rb

Она должна запустить веб-браузер на порте 4567, чтобы вы могли увидеть веб-страницу и начать тестирование:

http://localhost:4567

Использование приложения

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

Сперва создайте коллекцию, щелкнув по ссылке в самом нижнем левом углу страницы. Это создаст пустую коллекцию на серверах Amazon для хранения ваших изображений. Обратите внимание: имя по умолчанию для этой коллекции — faceapp_test, но вы можете изменить его в коде faceapp.rb (строка 17).

Затем, чтобы начать добавление лиц в свою коллекцию, попросите нескольких людей сесть перед вашим компьютером или телефоном и убедиться, что их лицо находится только в рамке для фотографий (несколько лиц сделают сканирование неудачным). Когда всё будет готово, введите их имя в текстовое поле и нажмите кнопку «Добавить в коллекцию». Должно появиться сообщение о том, что данные о лице были добавлены в базу данных.

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

Обратите внимание, что обычный способ работы Amazon Rekognition заключается в том, чтобы загружать фотографию в Amazon S3 Bucket, а затем обрабатывать его оттуда, но я хотел обойти этот шаг и фактически отправить данные фотографии непосредственно в Rekognition как поток байтов, закодированный в Base64. К счастью, aws-sdk для Ruby позволяет использовать оба метода.

Разбор кода

Прежде всего, давайте взглянем на HTML-страницу:

<html>
  <head>
    <title>Face Recognition Test</title>
    <link href="css/camera.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <h1>Face Recognition Test</h1>
    <div id='camera'>
      <div id='placeholder'>
        <p>Your browser does not support a camera!</p>
      </div>
    </div>
    <br>
    <p>
      <button id='compare_image'>Compare Image</button>
    </p>
    <br>
    <input id='photo_id' placeholder='Enter name for photo' type='text'>
    <button id='add_to_collection'>Add To Collection</button>
    <br>
    <div id='upload_status'></div>
    <div id='upload_result'></div>
    <br>
    <img height='100' id='loading_img' src='images/loading.gif' width='100'>
    <audio id='audio_speech' src='#'></audio>
    <br>
    <br>
    <p>
      <a href="/collection/create">Create Collection</a> | <a href="/collection/delete">Delete Collection</a>
    </p>
    <script src='https://code.jquery.com/jquery-3.1.1.min.js'></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js"></script>
    <script src="js/jpeg_camera/jpeg_camera_with_dependencies.min.js" type="text/javascript"></script>
    <script src="js/faceapp.js" type="text/javascript"></script>
  </body>
</html>

Структура страницы проста: всего несколько блоков, кнопок и ссылок. Обратите внимание, что мы используем jQuery, а также Moment.js для настраиваемого приветствия. Следует обратить внимание на код faceapp.js, который выполняет все сложные задачи и ссылки на библиотеку камер JPEG.

Вы также можете заметить теги <audio> в нижней части файла — это аудио-приветствие, которое мы отправляем пользователю.

Теперь разберем JS-файл приложения:

$(document).ready(function() {

  if (window.JpegCamera) {

    var camera; 

    // Добавить фотографии к текущей коллекции Rekognition для последующего сравнения
    var add_to_collection = function() {
      var photo_id = $("#photo_id").val();
      if (!photo_id.length) {
        $("#upload_status").html("Please provide name for the upload");
        return;
      }
      var snapshot = camera.capture();
      var api_url = "/upload/" + photo_id;
      $("#loading_img").show();
      snapshot.upload({api_url: api_url}).done(function(response) {
        $("#upload_result").html(response);
        $("#loading_img").hide();
        this.discard();
      }).fail(function(status_code, error_message, response) {
        $("#upload_status").html("Upload failed with status " + status_code + " (" + error_message + ")");
        $("#upload_result").html(response);
        $("#loading_img").hide();
      });
    };


    // Сравнить фотографию с текущей коллекцией Rekognition
    var compare_image = function() {
      var snapshot = camera.capture();
      var api_url = "/compare";
      $("#loading_img").show();
      snapshot.upload({api_url: api_url}).done(function(response) {
        var data = JSON.parse(response);
        if (data.id !== undefined) {
          $("#upload_result").html(data.message + ": " + data.id + ", Confidence: " + data.confidence);
          // создание звукового отчета
          $.post("/speech", {tosay: "Good " + greetingTime(moment()) + " " + data.id}, function(response) {
            $("#audio_speech").attr("src", "data:audio/mpeg;base64," + response);
            $("#audio_speech")[0].play();
          });
        } else {
          $("#upload_result").html(data.message);
        }
        $("#loading_img").hide();
        this.discard();
      }).fail(function(status_code, error_message, response) {
        $("#upload_status").html("Upload failed with status " + status_code + " (" + error_message + ")");
        $("#upload_result").html(response);
        $("#loading_img").hide();
      });
    };

    var greetingTime = function(moment) {
      var greet = null;
      
      if(!moment || !moment.isValid()) { return; } // Если мы не можем найти действительный момент, мы возвращаем пустое значение
            var split_afternoon = 12 // Определение дня
      var split_evening = 17 // Определение вечера
      var currentHour = parseFloat(moment.format("HH"));
      
      if(currentHour >= split_afternoon && currentHour <= split_evening) {
        greet = "afternoon";
      } else if(currentHour >= split_evening) {
        greet = "evening";
      } else {
        greet = "morning";
      }      
      return greet;
    }

    // Определение кнопок
    $("#add_to_collection").click(add_to_collection);
    $("#compare_image").click(compare_image);

    // Инициализация виджета камеры на экране
    var options = {
      shutter_ogg_url: "js/jpeg_camera/shutter.ogg",
      shutter_mp3_url: "js/jpeg_camera/shutter.mp3",
      swf_url: "js/jpeg_camera/jpeg_camera.swf"
    }
    

    camera = new JpegCamera("#camera", options).ready(function(info) {
      $("#loading_img").hide();
    });

  }

});

Это настраивает библиотеку JPEG Camera, чтобы отображать канал на экране и обрабатывать загрузку изображений.

Функция add_to_collection() захватывает изображение с камеры, а затем выполняет запись в конечную точку /upload вместе с именем пользователя в качестве параметра. Функция проверяет, действительно ли вы ввели имя, которое нужно в качестве уникального идентификатора этих данных.

Функция загрузки проверяет вызов и выводит сообщение об успешном завершении или ошибке.

Функция compare_image() вызывается, когда вы нажимаете кнопку «Сравнить изображение». Она захватывает кадр из камеры и передает POST-данные в /compare. Эта конечная точка вернет либо ошибку, либо структуру JSON, содержащую id (имя) найденного лица, а также процент схожести.

Если лицо совпадёт с данными из коллекции, функция отправит имя найденного лица в /speech. Эта конечная точка вызывает службу Amazon Polly, чтобы преобразовать приветствие в файл MP3, который можно воспроизвести пользователю.

Служба Amazon Polly возвращает приветствие в виде бинарного потока MP3, поэтому мы берем этот поток ввода-вывода, шифруем его в формате Base64 и помещаем в качестве закодированной исходной ссылки в теги <audio> на нашей веб-странице. Затем мы можем вызвать .play() для воспроизведения MP3 через динамики пользователя с помощью HTML5 Web Audio API.

Прим. перев. Вы можете узнать о Web Audio побольше, прочитав нашу серию статей по этой теме.

Наконец, в JS-файле приложения есть функция greetingTime(). Она решает, стоит ли говорить «доброе утро / день / вечер» в зависимости от времени суток пользователя.

Теперь взглянем на код Ruby:

# faceapp.rb
require 'rubygems'
require 'bundler'
Bundler.require
require 'sinatra'


Dotenv.load

# Настройка проверки подлинности  aws в этом приложении
Aws.config.update({
        :region => 'us-east-1',
        :credentials => Aws::Credentials.new(ENV['AWS_KEY'],ENV['AWS_SECRET'])
    })

# Название коллекции по умолчанию
FACE_COLLECTION = "faceapp_test"

# Путь
get '/' do
  # Показать на главной странице 
  erb :faceapp
end


post '/upload/:photoid' do
  client = Aws::Rekognition::Client.new()
  response = client.index_faces({
    collection_id: FACE_COLLECTION,
    external_image_id: params[:photoid],
    image: {
      bytes: request.body.read.to_s
    }
  })
  "Image uploaded safely!"
end


post '/compare' do
  content_type :json
  client = Aws::Rekognition::Client.new()
  response = client.search_faces_by_image({
    collection_id: FACE_COLLECTION,
    max_faces: 1,
    face_match_threshold: 95,
    image: {
      bytes: request.body.read.to_s
    }
  })
  if response.face_matches.count > 1
    {:message => "Too many faces found"}.to_json
  elsif response.face_matches.count == 0
    {:message => "No face detected!"}.to_json
  else
    # "Сравнение закончил - обнаружил #{ response.face_matches[0].face.external_image_id } with #{ response.face_matches[0].face.confidence } accuracy."
    {:id => response.face_matches[0].face.external_image_id, :confidence => response.face_matches[0].face.confidence, :message => "Face found!"}.to_json
  end
end


post '/speech' do
  client = Aws::Polly::Client.new()
  response = client.synthesize_speech({
    output_format: "mp3",
    voice_id: "Joanna",
    text: params[:tosay]
  })
  Base64.encode64(response.audio_stream.string)
end


get '/collection/:action' do
  client = Aws::Rekognition::Client.new()
  collections = client.list_collections({}).collection_ids
  case params[:action]
    when 'create'
      if !(collections.include? FACE_COLLECTION)
        response = client.create_collection({ collection_id: FACE_COLLECTION })
      end
    when 'delete'
      if (collections.include? FACE_COLLECTION)
        response = client.delete_collection({ collection_id: FACE_COLLECTION })
      end
  end
  redirect '/'
end

Здесь задан блок настройки конфигурации аутентификации AWS и имени коллекции по умолчанию, которое мы будем использовать (вы можете его свободно менять).

Остальная часть кода — это конечные точки, которые Sinatra будет слушать. Он прослушивает GET в /, чтобы отобразить фактическую веб-страницу конечному пользователю, а также слушает вызовы POST к /upload, /compare и /speech, которым JS-файл отправляет данные. Только 3–4 строки кода для каждой из этих конечных точек фактически выполняют задачи распознавания лиц и речи. Подробнее о них можно узнать из документации AWS SDK.

Перевод статьи «Building a face recognition web app in under an hour»