Анонимизация изображений с помощью Go
Перевод статьи “Anonymising images with Go and Machine Box”
В стандартной библиотеке Go есть достаточное количество мощных инструментов для работы с изображениями. Это пакеты image, image/* и draw. В этом руководстве мы будем использовать эти инструменты совместно с Machine Box Go SDK для цензурирования изображений.
В рамках этой статьи мы напишем простую консольную утилиту. Но тот же самый код можно будет использовать, например, для написания веб-приложений.
Запускаем Facebox
Facebox это один из образов Machine Box который позволяет распознавать лица с помощью алгоритмов машинного обучения. Все это поставляется в виде Docker контейнера, который можно запускать где угодно, в том числе и локально.
В терминале можно запустить Facebox выполнив следующую команду:
docker run -p 8080:8080 -e "MB_KEY=$MB_KEY" machinebox/facebox
Обратите внимание - вам обязательно нужно указать переменную окружения MB_KEY. Значение для этой переменной можно найти на странице вашего аккаунта https://machinebox.io/account. Естественно, вам нужно зарегистрироваться на machinebox.io.
Как только контейнер запустится, можно перейти по ссылке http://localhost:8080/ для доступа к консоли встроенной в Facebox.
Facebox можно обучить распознавать какие-то конкретные лица и затем использовать для поиска похожих лиц. Но в нашем примере распознавания лиц не будет. Нам достаточно получать информацию о том, где на фотографии расположено лицо.
Скачиваем Machine Box Go SDK
Устанавливаем SDK:
go get -u github.com/machinebox/sdk-go
Используем анонимные функции
Начнем с написания функции, которая принимает на вход исходное изображение и набор объектов facebox.Face. Затем генерирует новый JPEG c зацензуренными лицами.
Создадим папку anon и файл main.go. Теперь можно приступать к написанию функции для анонимизации:
func anonymise(src image.Image, faces []facebox.Face) image.Image {
  dstImage := image.NewRGBA(src.Bounds())
  draw.Draw(dstImage, src.Bounds(), src, image.ZP, draw.Src)
  for _, face := range faces {
    faceRect := image.Rect(
      face.Rect.Left,
      face.Rect.Top,
      face.Rect.Left+face.Rect.Width,
      face.Rect.Top+face.Rect.Height,
    )
    facePos := image.Pt(face.Rect.Left, face.Rect.Top)
    draw.Draw(
      dstImage,
      faceRect,
      &image.Uniform{color.Black},
      facePos,
      draw.Src)
  }
  return dstImage
}
Мы используем image.NewRGBA для создания нового изображения такого же размера, как исходное. Затем с помощью draw.Draw копируем исходное изображение в только что созданное.
Итерируясь по всем объектам Face в слайсе faces мы сохраняем координаты прямоугольников в которых находятся лица в переменной faceRect. Также сохраняем все позиции лиц на изображении в переменной facePos.
Структуры facebox.Face и facebox.Rect определены в таком виде:
type Face struct {
    Rect    Rect
    ID      string
    Name    string
    Matched bool
}
type Rect struct {
    Top, Left     int
    Width, Height int
}
Структура Rect определяет размеры лица на исходном изображении и используется в структуре Face.
Поля ID, Name и Matched используются когда необходимо распознать кто изображен на картинке. Но мы не будем использовать эту возможность в нашем приложении.
facebox.Rect предоставляет значения высоты и ширины прямоугольника, но для image.Rect нам нужно знать значения координат x1, y1 — x2, y2. Поэтому вы выполняем несложное преобразование face.Rect.Left+face.Rect.Width и face.Rect.Top+face.Rect.Height.
После этого опять используем функцию draw.Draw для рисования черных прямоугольников поверх нового изображения. &image.Uniform{color.Black} позволяет получить черное изображение.
В итоге у нас получается новое изображение.
Обратите внимание, что вам нужно будет импортировать github.com/machinebox/sdk-go/facebox и несколько дополнительных пакетов.
Консольная утилита
Чтобы пользоваться всем этим кодом, который мы написали, необходимо добавить несколько функций для загрузки исходных изображений и сохранения результатов. Напишем функцию main в которой объединим все наши наработки.
Использование нашей утилиты должно быть простым. В качестве аргумента будет указываться путь к исходному изображению.
Ниже код, который работает с аргументом строки, открывает картинку и использует defer для закрытия файла.
func main() {
  if len(os.Args) < 2 {
    log.Fatalln("usage: anon <image>")
  }
  filename := os.Args[1]
  f, err := os.Open(filename)
  if err != nil {
    log.Fatalln(err)
  }
  defer f.Close()
Затем создаем клиента с помощью которого можно работать с Facebox сервисом:
fb := facebox.New("http://localhost:8080")
facebox.New возвращает экземпляр facebox.Client у которого есть целый набор методов. С помощью этих методов можно выполнять HTTP запросы к локальному Facebox докер контейнеру.
Для обнаружения лиц нам нужно вызвать метод Check:
log.Println("Detecting faces...")
faces, err := fb.Check(f)
if err != nil {
 log.Fatalln(err)
}
Переменная faces это слайс объектов facebox.Face которые будут использоваться для анонимизации.
Дальше нам нужно преобразовать исходное изображение в экземпляр image.Image. С объектом такого типа мы выполнять все необходимые операции.
Так как метод Check принимает на io.Reader и читает данные из файла, то нам необходимо переместиться в начало файла:
_, err = f.Seek(0, os.SEEK_SET)
if err != nil {
  log.Fatalln(err)
}
Это не самый красивый код. Мы перемещаемся в начало файла указывая позицию 0. os.SEEK_SET означает что мы хотим установить позицию(в том смысле, что это не относительная операция смещения).
Если мы не использовали бы os.File, то нам пришлось бы использовать что-то что поддерживает интерфейс io.ReadSeeker. Как вариант, можно было бы прочитать все содержимое файла в bytes.Buffer.
Теперь нам нужно декодировать набор байтов в объект image.Image:
srcImage, _, err := image.Decode(f)
if err != nil {
  log.Fatalln(err)
}
Функция image.Decode декодирует данные из файла автоматически в нужный формат. Для поддержки большого количества форматов, нам нужно импортировать дополнительные пакеты.
Кроме импорта пакетов ничего больше делать не нужно. Каждый пакет в функции init регистрирует функцию image.Decode. Но в Go нельзя просто так импортировать пакет и не использовать его. Чтобы наша программа компилировалась, используем небольшой хак - укажем в качестве алиасов символ подчеркивания.
В результате секция с импортируемыми пакетами будет выглядеть примерно так:
import (
  _ "image/gif"
  _ "image/jpeg"
  _ "image/png"
)
Теперь у нас есть объект image.Image и мs моем использовать его как аргумент для вызова функции anonymise вместе с слайсом faces:
dstImage := anonymise(srcImage, faces)
Если все прошло хорошо, то необходимо сохранить dstImage на диск в новый файл. Финальный код main нашей утилиты:
// fudge the filename to add the -anon suffix (before the ext)
filename = filepath.Base(filename)
ext := filepath.Ext(filename)
dstFilename := filename[:len(filename)-len(ext)] + "-anon" + ext
dstFile, err := os.Create(dstFilename)
if err != nil {
  log.Fatalln(err)
}
defer dstFile.Close()
log.Println("Saving image to " + dstFilename + "...")
err = jpeg.Encode(dstFile, dstImage, &jpeg.Options{Quality: 100})
if err != nil {
  log.Fatalln(err)
}
log.Println("Done.")
Видно, что добавляется суффикс -anon для всех новых изображений. Вызов jpeg.Encode позволит сохранить все у нас получилось как JPEG изображение. Также, вы можете использовать png.Encode для создания PNG изображений.
Замечу, что мы используем только пакет jpeg и все остальные пакеты нам нужно удалить.
Тестируем программу
Мы закончили с кодом и теперь можем проверить как работает наша программа. Для тестирования нам нужна фотография с лицами людей. Запускаем утилиту и указываем путь к картинке в аргументе:
go run main.go family.jpg
Вывод программы будет примерно таким:
matryer$ go run main.go testdata/thebeatles.jpg
2017/05/06 00:50:53 Detecting faces...
2017/05/06 00:50:54 Saving image to thebeatles-anon.jpg...
2017/05/06 00:50:54 Done.
В сгенерированном файле все лица будут зацезурированны.
Заключение
С использованием MachineBox у нас получилось написать консольную утилиту для анонимизированния фотографий. При этом, у нас совсем немного кода(в основном это шаблонный код загрузки/сохранения/кодирования/декодирования и т.д.).
Что дальше
- 
Посмотреть исходники этого примера.
 - 
Почитать как еще можно использовать Facebox, обучать его и использовать через Go SDK
 - 
Можно зафоловить @matryer в твиттере и следить за новыми статьями. И не забывайте про твиттер @machineboxio