Face recognition with Golang
Tags: ukrainian, go, facerecognition
Published: 2020-11-14 10:28:52

Привіт, мене звати Ярослав. Ця стаття буде про практичне використання face recognition в Golang.

<h3>Стандартна задача — розпізнати людей на фотографії</h3>
У нас є фотографія та функція <strong>Recognize</strong>, яка знаходить обличчя на фотографії і для кожного знайденого обличчя формує вектор — 128-розмірний масив чисел <strong>[128]float</strong>, що в коді називається <strong>descriptor</strong>.
```golang
type Descriptor [128]float32
```
функція <strong>Recognize</strong> отримує картинку і повертає знайдені обличчя
```golang
import "image"

// Descriptor holds 128-dimensional feature vector.
type Descriptor [128]float32

type Face struct {
	Rectangle  image.Rectangle
	Descriptor Descriptor
	Shapes     []image.Point
}

func Recognize(imgData []byte) (faces []Face, err error) {
	// logic

	return
}
```
між двома векторами можна порахувати відстань (<a href="https://uk.wikipedia.org/wiki/%D0%95%D0%B2%D0%BA%D0%BB%D1%96%D0%B4%D0%BE%D0%B2%D0%B0_%D0%B2%D1%96%D0%B4%D1%81%D1%82%D0%B0%D0%BD%D1%8C" target="_blank">Евклідову відстань</a> вивчають у школі)
а ось так розрахунок відстані виглядає на Golang:
```golang
import (
	"math"
)

type Descriptor [128]float32

func SquaredEuclideanDistance(d1 Descriptor, d2 Descriptor) (sum float64) {
	for i := range d1 {
		sum = sum + math.Pow(float64(d2[i]-d1[i]), 2)
	}

	return sum
}
```
Чим більше схожі обличчя, тим менша відстань між їх векторами.

Для розробки системи розпiзнавання обличь відомих людей, необхiдно завантажити вiдповiднi фотогорафії та перетворити їх у вектори.

Тепер, коли ви захочете дізнатися, хто на фотографії — завантажте фотографію, система сформує вектор, порівняє вектор з кожним збереженим вектором, знайде найближчий-найближчі та поверне вам імена (i фотографії) тих, кому належать найближчі вектори.

Вищевказаного буде достатньо для розуміння як працює розпізнавання.

Далі у статті буде йтися про підключення Golang бібліотеки <a href="https://github.com/Kagami/go-face" target="_blank">github.com/Kagami/go-face</a> з прикладами та поясненнями, як почати використовувати.

<h3>Вибір бібліотеки та її підключення</h3>
Пошук Google <a href="https://www.google.com/search?q=golang+face+recognition" target="_blank">golang face recognition</a> повернув дві бібліотеки:
<ul>
<li><a href="https://github.com/esimov/pigo" target="_blank">github.com/esimov/pigo</a> (2900+ stars)</li>
<li><a href="https://github.com/Kagami/go-face" target="_blank">github.com/Kagami/go-face</a> (500+ stars)</li>
</ul>
Я переглянув обидві і вибрав <a href="https://github.com/Kagami/go-face" target="_blank">github.com/Kagami/go-face</a> найбiльш зрозумiлу для себе документацію, а також ознайомився зi статтею <a href="https://hackernoon.com/face-recognition-with-go-676a555b8a7e" target="_blank">Face recognition with Go</a>.

Бібліотека <a href="https://github.com/Kagami/go-face" target="_blank">github.com/Kagami/go-face</a> є обгорткою над C++ бібліотекою <a href="https://github.com/davisking/dlib" target="_blank">dlib</a>.

Щоб Golang міг використовувати dlib — його треба встановити.
Для Ubuntu:
```bash
sudo apt-get install libdlib-dev libblas-dev liblapack-dev libjpeg-turbo8-dev
```
Як встановити dlib для macOS та Winodws зазначено в документації <a href="https://github.com/Kagami/go-face/blob/master/README.md" target="_blank">README.md</a>.

Також dlib потребує файлів з натренованими моделями, ці моделі доступні на офіційному репозиторії <a href="https://github.com/davisking/dlib-models" target="_blank">github.com/davisking/dlib-models</a> або <a href="https://github.com/Kagami/go-face-testdata" target="_blank">github.com/Kagami/go-face-testdata</a>.
Тепер завантажимо моделі:
```bash
mkdir -p ./testdata/models
wget https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat -P ./testdata/models
wget https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat -P ./testdata/models
wget https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat -P ./testdata/models
```
```bash
tree ./testdata/models/
```
```text
./testdata/models/
├── dlib_face_recognition_resnet_model_v1.dat
├── mmod_human_face_detector.dat
└── shape_predictor_5_face_landmarks.dat
```
Я завантажив з інтернету <strong>jennifer.jpg</strong> і написав приклад, який виводить дескриптори.
```golang
package main

import (
	"github.com/Kagami/go-face"
	"io/ioutil"
	"log"
	"time"
)

func main() {
	var recognizerInitStartTime = time.Now()

	// Init the recognizer.
	rec, err := face.NewRecognizer("./testdata/models")
	if err != nil {
		log.Fatalf("Can't init face recognizer: %v", err)
	}
	// Free the resources when you're finished.
	defer rec.Close()

	log.Printf("recognizer init by %s", time.Since(recognizerInitStartTime))

	var jenniferImageBytes, readFileErr = ioutil.ReadFile("./jennifer.jpg")
	if readFileErr != nil {
		// log.Fatalf call os.Exit(1)
		// so use log.Printf to defer rec.Close()
		log.Printf("Can't read file: %v", readFileErr)

		return
	}

	var recognizeStartTime = time.Now()
	var faces, recognizeErr = rec.Recognize(jenniferImageBytes)
	log.Printf("recognize faces by %s", time.Since(recognizeStartTime))

	if recognizeErr != nil {
		log.Printf("Can't recognize: %v", recognizeErr)

		return
	}

	log.Printf("found %d faces", len(faces))

	for i, face := range faces {
		log.Printf("face %d with descriptor %+v", i, face.Descriptor)
	}
}
```
```bash
go run main.go
```
```text
17:22:00 recognizer init by 371.305232ms
17:22:00 recognize faces by 316.697968ms
17:22:00 found 1 faces
17:22:00 face 0 with descriptor [-0.07678388 0.15807864 0.14382923 -0.07904527 -0.11428883 -0.029490437 0.058661476 -0.0933703 0.23498747 -0.054770716 0.24722381 -0.05858693 -0.28161827 -0.00729264 0.053299718 0.15644383 -0.15317412 -0.09491905 -0.11324714 0.015937451 0.017232804 0.02061552 0.09107592 0.08736397 -0.17142092 -0.3387704 -0.040354054 -0.0375154 0.068538606 -0.13365544 0.03331705 0.09273607 -0.17582977 -0.06855342 0.028308533 0.027136123 -0.10719579 -0.11425108 0.23959376 -0.025176954 -0.25584993 -0.08564242 0.07274324 0.28109437 0.1977994 0.0743362 0.04528319 -0.15180072 0.11090667 -0.3346209 0.03862732 0.14867145 -0.052644238 0.0695322 0.056489225 -0.16142687 0.031142544 0.091048405 -0.24351647 0.071151696 0.13612525 -0.1243305 -0.00016226899 -0.090171695 0.28420573 0.05841827 -0.12638493 -0.11757094 0.14316459 -0.1568535 -0.057871066 0.09184233 -0.09624884 -0.18405873 -0.2898923 -0.03938524 0.351529 0.14347278 -0.14661624 0.037677716 -0.1709492 -0.030889995 0.019944552 0.12754735 0.011989551 -0.060295634 -0.10107601 0.022347813 0.2718173 -0.11187582 0.040455934 0.29762152 0.057943583 -0.06804431 -0.051001664 0.012894538 -0.20557034 0.024265196 -0.18289387 -0.057812028 0.023129724 0.035144385 0.05509291 0.1335184 -0.27161613 0.2431183 0.043914706 -0.059160654 0.07033479 -0.074724026 -0.106148675 -0.10262138 0.15065585 -0.2624043 0.22501433 0.15340629 0.11548248 0.12753347 0.05119327 0.093256816 0.044141423 0.019079404 -0.08248548 -0.03165059 0.09458799 -0.055271063 0.061118975 0.023323089]
```

Тепер я завантажив ще й фотографії <strong>jennifer-aniston.jpg</strong> та <strong>jennifer-love-hewitt.jpg</strong>.
Напишемо код, який порівняє попередньо завантежену <strong>jennifer.jpg</strong> з <strong>jennifer-aniston.jpg</strong> та <strong>jennifer-love-hewitt.jpg</strong>.
```golang
package main

import (
	"fmt"
	"github.com/Kagami/go-face"
	"io/ioutil"
	"log"
	"time"
)

func main() {
	rec, err := face.NewRecognizer("./testdata/models")
	if err != nil {
		log.Fatalf("Can't init face recognizer: %v", err)
	}
	defer rec.Close()

	var (
		jenniferFace           = mustRecognizeSingleFile(rec, "./jennifer.jpg")
		jenniferAnistonFace    = mustRecognizeSingleFile(rec, "./jennifer-aniston.jpg")
		jenniferLoveHewittFace = mustRecognizeSingleFile(rec, "./jennifer-love-hewitt.jpg")
	)

	var (
		jenniferAnistonDistance    = face.SquaredEuclideanDistance(jenniferAnistonFace.Descriptor, jenniferFace.Descriptor)
		jenniferLoveHewittDistance = face.SquaredEuclideanDistance(jenniferLoveHewittFace.Descriptor, jenniferFace.Descriptor)
	)

	log.Printf("Jennifer with Jennifer Aniston     = %.8f", jenniferAnistonDistance)
	log.Printf("Jennifer with Jennifer Love Hewitt = %.8f", jenniferLoveHewittDistance)
}

func mustRecognizeSingleFile(rec *face.Recognizer, filename string) face.Face {
	var imageBytes, readFileErr = ioutil.ReadFile(filename)
	if readFileErr != nil {
		panic(fmt.Sprintf("Can't read file %s: %v", filename, readFileErr))
	}

	var recognizeStartTime = time.Now()
	var faces, recognizeErr = rec.Recognize(imageBytes)
	log.Printf("recognize faces on %s by %s", filename, time.Since(recognizeStartTime))

	if recognizeErr != nil {
		panic(fmt.Sprintf("Can't recognize %s: %v", filename, recognizeErr))
	}

	var length = len(faces)
	if length != 1 {
		panic(fmt.Sprintf("Expected 1 face on photo %s, got %d faces", filename, length))
	}

	return faces[0]
}
```
```bash
go run main.go
```
```text
18:12:00 recognize faces on ./jennifer.jpg by 313.49215ms
18:12:00 recognize faces on ./jennifer-aniston.jpg by 342.010394ms
18:12:00 recognize faces on ./jennifer-love-hewitt.jpg by 294.416138ms
18:12:00 Jennifer with Jennifer Aniston     = 0.35406203
18:12:00 Jennifer with Jennifer Love Hewitt = 0.64027529
```

<h3>Збереження 128-розмірного вектору в БД або файл</h3>
Оскiльки розпізнання кожної фотографії це 200-300 мс — то буде правильно завчасно підготувати базу дескрипторів.

Майже усі SQL бази даних можуть зберегти масив байтів, а дескриптор <strong>[128]float32</strong> можна перетворити в <strong>[512]byte</strong> за допомогою функції math.Float32bits.
Ось приклади функцій, які перетворюють дискриптор в масив байтів і назад:
```golang
func DescriptorToBytes(descriptor [128]float32) [512]byte {
	var result [512]byte

	var buffer = result[:0]

	for i := 0; i < 128; i++ {
		var bits uint32 = math.Float32bits(descriptor[i])

		buffer = append(
			buffer,
			byte(bits),
			byte(bits>>8),
			byte(bits>>16),
			byte(bits>>24),
		)
	}

	return result
}

func BytesToDescriptor(bytes [512]byte) [128]float32 {
	var result [128]float32

	var i = 0

	for j := 0; j < 512; j += 4 {
		result[i] = math.Float32frombits(
			uint32(bytes[j]) +
				uint32(bytes[j+1])<<8 +
				uint32(bytes[j+2])<<16 +
				uint32(bytes[j+3])<<24,
		)

		i += 1
	}

	return result
}
```
Або можете інакше серіалізувати в масив байтів, наприклад, через protobuf.

<h3>Використання бази дескрипторів</h3>
В нас є функція, яка читає з бази дескриптори і повертає їх:
```golang
type UserDescriptor struct {
	ID         uint32
	UserID     uint32
	PhotoPath  string
	Descriptor [128]float32
}

func FetchUserDescriptors() ([]UserDescriptor, error) {
	var result []UserDescriptor

	// ...

	return result, nil
}
```
звісно, у користувача може бути багато фотографій

Якщо нам треба знайти тільки <strong>одного</strong> користувача по фотографії — то можемо скористатись вже готовою функцією Classify яка є в dlib.
```golang
func FindNearestUserID(rec *face.Recognizer, users []UserDescriptor, input [128]float32) uint32 {
	var (
		length     = len(users)
		categories = make([]int32, length)
		samples    = make([]face.Descriptor, length)
	)

	for i, f := range users {
		samples[i] = f.Descriptor
		categories[i] = int32(f.UserID)
	}

	rec.SetSamples(samples, categories)

	var userID = rec.Classify(input)

	return uint32(userID)
}
```

Також є можливість шукати і фільтрувати за максимальною відстанню через ClassifyThreshold:
```golang
// if find return userID, otherwise return -1
func FindThresholdUserID(rec *face.Recognizer, users []UserDescriptor, input [128]float32, tolerance float32) int {
	var (
		length     = len(users)
		categories = make([]int32, length)
		samples    = make([]face.Descriptor, length)
	)

	for i, f := range users {
		samples[i] = f.Descriptor
		categories[i] = int32(f.UserID)
	}

	rec.SetSamples(samples, categories)

	var userID = rec.ClassifyThreshold(input, tolerance)

	return userID
}
```

Глянемо реалізацію C++ функції <a href="https://github.com/Kagami/go-face/blob/master/classify.cc" target="_blank">Classify</a>, яка повертає одного найближчого користувача, та перепишемо на Golang, щоб знаходити найближчих схожих користувачів.
```cpp
#include <unordered_map>
#include <dlib/graph_utils.h>
#include "classify.h"

int classify(
	const std::vector<descriptor>& samples,
	const std::vector<int>& cats,
	const descriptor& test_sample,
	float tolerance
) {
	if (samples.size() == 0)
		return -1;

	std::vector<std::pair<int, float>> distances;
	distances.reserve(samples.size());
	auto dist_func = dlib::squared_euclidean_distance();
	int idx = 0;
	for (const auto& sample : samples) {
		float dist = dist_func(sample, test_sample);
		if (tolerance < 0 || dist <= tolerance) {
			distances.push_back({cats[idx], dist});
		}
		idx++;
	}

	if (distances.size() == 0)
		return -1;

	std::sort(
		distances.begin(), distances.end(),
		[](const auto a, const auto b) { return a.second < b.second; }
	);

	int len = std::min((int)distances.size(), 10);
	std::unordered_map<int, std::pair<int, float>> hits_by_cat;
	for (int i = 0; i < len; i++) {
		int cat_idx = distances[i].first;
		float dist = distances[i].second;
		auto hit = hits_by_cat.find(cat_idx);
		if (hit == hits_by_cat.end()) {
			hits_by_cat[cat_idx] = {1, dist};
		} else {
			hits_by_cat[cat_idx].first++;
		}
	}

	auto hit = std::max_element(
		hits_by_cat.begin(), hits_by_cat.end(),
		[](const auto a, const auto b) {
			auto hits1 = a.second.first;
			auto hits2 = b.second.first;
			auto dist1 = a.second.second;
			auto dist2 = b.second.second;
			if (hits1 == hits2) return dist1 > dist2;
			return hits1 < hits2;
		}
	);
	return hit->first;
}
```
Ось переписана на Golang функція, яка повертає найближчих користувачів по фотографії:
```golang
type UserDescriptorDistance struct {
	UserDescriptor
	Distance float64
}

type UserDescriptorDistanceList []UserDescriptorDistance

func (l UserDescriptorDistanceList) Len() int {
	return len(l)
}

func (l UserDescriptorDistanceList) Less(i, j int) bool {
	return l[i].Distance < l[j].Distance
}

func (l UserDescriptorDistanceList) Swap(i, j int) {
	l[i], l[j] = l[j], l[i]
}

func FindNearestUsers(rec *face.Recognizer, users []UserDescriptor, input [128]float32, tolerance float64) []UserDescriptorDistance {
	var result []UserDescriptorDistance

	if tolerance > 0 {
		for _, user := range users {
			var distance = face.SquaredEuclideanDistance(user.Descriptor, input)

			if distance < tolerance {
				result = append(result, UserDescriptorDistance{
					UserDescriptor: user,
					Distance:       distance,
				})
			}
		}
	} else {
		result = make([]UserDescriptorDistance, 0, len(users))

		for _, user := range users {
			var distance = face.SquaredEuclideanDistance(user.Descriptor, input)

			result = append(result, UserDescriptorDistance{
				UserDescriptor: user,
				Distance:       distance,
			})
		}
	}

	sort.Sort(UserDescriptorDistanceList(result))

	return result
}
```

<h3>Застереження</h3>
<blockquote>dlib supports a lot of image formats (JPEG, PNG, GIF, BMP, DNG) but go-face currently implements only JPEG, would be good to support more.</blockquote>
Щоб працювати з PNG фотографіями, вам треба буде використати стандартну Golang бібліотеку <strong>image</strong> та перетворити фото в JPEG. В мережі повно прикладів як це зробити.
Або ж додати підтримку PNG до <a href="https://github.com/Kagami/go-face" target="_blank">github.com/Kagami/go-face</a>.

Коли пробував розвернути на DigitalOcean, то при першому запуску з'їло усю оперативну пам'ять, тому для першого запуску підняв до 4 GB щоб зібрало і C++, а потім повернув до 1 GB.

<h3>Епілог</h3>
Рекомендую прочитати оригінальну статтю <a href="https://hackernoon.com/face-recognition-with-go-676a555b8a7e" target="_blank">Face recognition with Go</a>.
Все, що було описано в статті, я друзям розповів за пару хвилин, а ось написання тексту — майже робочий день.