Face recognition with Golang
Yaroslav Podorvanov
Posted on November 14, 2020
Привіт, мене звати Ярослав. Ця стаття буде про практичне використання face recognition в Golang.
Стандартна задача — розпізнати людей на фотографії
У нас є фотографія та функція Recognize, яка знаходить обличчя на фотографії і для кожного знайденого обличчя формує вектор — 128-розмірний масив чисел [128]float, що в коді називається descriptor.
type Descriptor [128]float32
функція Recognize отримує картинку і повертає знайдені обличчя
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
}
між двома векторами можна порахувати відстань (Евклідову відстань вивчають у школі)
а ось так розрахунок відстані виглядає на 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 бібліотеки github.com/Kagami/go-face з прикладами та поясненнями, як почати використовувати.
Вибір бібліотеки та її підключення
Пошук Google golang face recognition повернув дві бібліотеки:
- github.com/esimov/pigo (2900+ stars)
- github.com/Kagami/go-face (500+ stars)
Я переглянув обидві і вибрав github.com/Kagami/go-face найбiльш зрозумiлу для себе документацію, а також ознайомився зi статтею Face recognition with Go.
Бібліотека github.com/Kagami/go-face є обгорткою над C++ бібліотекою dlib.
Щоб Golang міг використовувати dlib — його треба встановити.
Для Ubuntu:
sudo apt-get install libdlib-dev libblas-dev liblapack-dev libjpeg-turbo8-dev
Як встановити dlib для macOS та Winodws зазначено в документації README.md.
Також dlib потребує файлів з натренованими моделями, ці моделі доступні на офіційному репозиторії github.com/davisking/dlib-models або github.com/Kagami/go-face-testdata.
Тепер завантажимо моделі:
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
tree ./testdata/models/
./testdata/models/
├── dlib_face_recognition_resnet_model_v1.dat
├── mmod_human_face_detector.dat
└── shape_predictor_5_face_landmarks.dat
Я завантажив з інтернету jennifer.jpg і написав приклад, який виводить дескриптори.
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)
}
}
go run main.go
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]
Тепер я завантажив ще й фотографії jennifer-aniston.jpg та jennifer-love-hewitt.jpg.
Напишемо код, який порівняє попередньо завантежену jennifer.jpg з jennifer-aniston.jpg та jennifer-love-hewitt.jpg.
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]
}
go run main.go
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
Збереження 128-розмірного вектору в БД або файл
Оскiльки розпізнання кожної фотографії це 200-300 мс — то буде правильно завчасно підготувати базу дескрипторів.
Майже усі SQL бази даних можуть зберегти масив байтів, а дескриптор [128]float32 можна перетворити в [512]byte за допомогою функції math.Float32bits.
Ось приклади функцій, які перетворюють дискриптор в масив байтів і назад:
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.
Використання бази дескрипторів
В нас є функція, яка читає з бази дескриптори і повертає їх:
type UserDescriptor struct {
ID uint32
UserID uint32
PhotoPath string
Descriptor [128]float32
}
func FetchUserDescriptors() ([]UserDescriptor, error) {
var result []UserDescriptor
// ...
return result, nil
}
звісно, у користувача може бути багато фотографій
Якщо нам треба знайти тільки одного користувача по фотографії — то можемо скористатись вже готовою функцією Classify яка є в dlib.
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:
// 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++ функції Classify, яка повертає одного найближчого користувача, та перепишемо на Golang, щоб знаходити найближчих схожих користувачів.
#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 функція, яка повертає найближчих користувачів по фотографії:
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
}
Застереження
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.
Щоб працювати з PNG фотографіями, вам треба буде використати стандартну Golang бібліотеку image та перетворити фото в JPEG. В мережі повно прикладів як це зробити.
Або ж додати підтримку PNG до github.com/Kagami/go-face.
Коли пробував розвернути на DigitalOcean, то при першому запуску з'їло усю оперативну пам'ять, тому для першого запуску підняв до 4 GB щоб зібрало і C++, а потім повернув до 1 GB.
Епілог
Рекомендую прочитати оригінальну статтю Face recognition with Go.
Все, що було описано в статті, я друзям розповів за пару хвилин, а ось написання тексту — майже робочий день.
Posted on November 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.