Розпізнавання облич за допомогою Golang
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті
Привіт, мене звати Ярослав. Ця стаття буде про практичне використання face recognition в Golang.
Стандартна задача — розпізнати людей на фотографії
У нас є фотографія та функція Recognize, яка знаходить обличчя на фотографії і для кожного знайденого обличчя формує вектор —
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 є обгорткою над C++ бібліотекою dlib.
Щоб Golang міг використовувати dlib — його треба встановити.
Для Ubuntu:
sudo apt-get install libdlib-dev libblas-dev liblapack-dev libjpeg-turbo8-dev
Також 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льки розпізнання кожної фотографії це
Майже усі 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.
Все, що було описано в статті, я друзям розповів за пару хвилин, а ось написання тексту — майже робочий день.
29 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів