Анонимизация изображений с помощью Go

23 minute read

Перевод статьи “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 у нас получилось написать консольную утилиту для анонимизированния фотографий. При этом, у нас совсем немного кода(в основном это шаблонный код загрузки/сохранения/кодирования/декодирования и т.д.).

Что дальше