Прийшов час осідлати справжнього Буцефала🏇🏻Приборкай норовливого коня разом з Newxel🏇🏻Умови на сайті
×Закрыть

Розпізнавання облич за допомогою Golang

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт, мене звати Ярослав. Ця стаття буде про практичне використання 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/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.
Все, що було описано в статті, я друзям розповів за пару хвилин, а ось написання тексту — майже робочий день.

LinkedIn

Похожие статьи

Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Додали блок зі схожими статтями:
i.imgur.com/p4JLvRv.png

Питаннячко, а чи існує якесь рішення без залежень до коду з інших мов програмування, на чистому Go ?

Якщо знайду то відпишу в коментарях, як вже відписав про face-api.js (тому просто підпишись на коментарі до цієї теми)

Ну, в век докера не проблема доставить в контнейнер бинари))

YouTube порекомендував Build Real Time Face Detection With JavaScript з використанням github.com/justadudewhohacks/face-api.js

face-api.js це

JavaScript face recognition API for the browser and nodejs implemented on top of tensorflow.js core

face-api.js так само повертає дескриптор Float32Array

Красиво все расписано, то все это можно было бы сократить до использования библиотеки GoCV — всеми любимый OpenCV с обвязкой под Go. Работает по тому же принципу классификатора Хаар (темные-белые сегменты лица).

github.com/hybridgroup/gocv

З GoCV знайшов тільки приклади face detection Face Detection in Go using OpenCV

Якщо знайдеш саме face recognition з GoCV то відпиши будь-ласка

type Face struct {
Rectangle image.Rectangle
.....

Дальше не читал

Тоді треба було ще дочитати до shapes:

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

Невероятно! На других языках это поди вообще невозможно сделать. Спасибо golang что он есть.

В оригіналі назвав статтю Face recognition with Golang

Як думаєш яка ціль написання статті?

В оригіналі назвав статтю Face recognition with Golang

В чём идейный смысл писать название/заголовок статьи на английском, а текст статьи на украинском?

Щоб україномовні розробники коли шукали англійською Face recognition with Golang знаходили також і мою статтю

Або я хотів написати статтю англійською мовою але редактори перевели на українську мову

Прикладу в роботі частіше бачу full-text search ніж повнотекстовий пошук

Это всё чтобы подготовить читателя к тому, что фейс рекогнишн там ваще на плюсах

Golang це якась нова версія Go?

> The language’s name is just plain Go, regardless.
Я того й питаю, що таке Golang.

Класна стаття!
Є ще одна класна штука... але трохи працює інакше...
Колись грався із Raspberry Pi 4 із різними сенсорами тощо... та з github.com/hybridgroup/gocv
3,6K stars ;))

github.com/hybridgroup/gocv теж дивився але знайшов тільки приклади з face detection

Нужно больше Golang

Добавил тут jobs.dou.ua/...​acancies/?category=golang справа блок с Golang статьями раздела dou.ua/forums/tags/tech

Отличная статья, спасибо !

Подписаться на комментарии