Face recognition with Golang

yaroslavpodorvanov

Yaroslav Podorvanov

Posted on November 14, 2020

Face recognition with Golang

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

Стандартна задача — розпізнати людей на фотографії

У нас є фотографія та функція Recognize, яка знаходить обличчя на фотографії і для кожного знайденого обличчя формує вектор — 128-розмірний масив чисел [128]float, що в коді називається descriptor.

type Descriptor [128]float32
Enter fullscreen mode Exit fullscreen mode

функція 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
}
Enter fullscreen mode Exit fullscreen mode

між двома векторами можна порахувати відстань (Евклідову відстань вивчають у школі)
а ось так розрахунок відстані виглядає на 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
}
Enter fullscreen mode Exit fullscreen mode

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

Для розробки системи розп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
Enter fullscreen mode Exit fullscreen mode

Як встановити 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
Enter fullscreen mode Exit fullscreen mode
tree ./testdata/models/
Enter fullscreen mode Exit fullscreen mode
./testdata/models/
├── dlib_face_recognition_resnet_model_v1.dat
├── mmod_human_face_detector.dat
└── shape_predictor_5_face_landmarks.dat
Enter fullscreen mode Exit fullscreen mode

Я завантажив з інтернету 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)
    }
}
Enter fullscreen mode Exit fullscreen mode
go run main.go
Enter fullscreen mode Exit fullscreen mode
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]
Enter fullscreen mode Exit fullscreen mode

Тепер я завантажив ще й фотографії 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]
}
Enter fullscreen mode Exit fullscreen mode
go run main.go
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Збереження 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
}
Enter fullscreen mode Exit fullscreen mode

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

Використання бази дескрипторів

В нас є функція, яка читає з бази дескриптори і повертає їх:

type UserDescriptor struct {
    ID         uint32
    UserID     uint32
    PhotoPath  string
    Descriptor [128]float32
}

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

    // ...

    return result, nil
}
Enter fullscreen mode Exit fullscreen mode

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

Якщо нам треба знайти тільки одного користувача по фотографії — то можемо скористатись вже готовою функцією 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)
}
Enter fullscreen mode Exit fullscreen mode

Також є можливість шукати і фільтрувати за максимальною відстанню через 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
}
Enter fullscreen mode Exit fullscreen mode

Глянемо реалізацію 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;
}
Enter fullscreen mode Exit fullscreen mode

Ось переписана на 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
}
Enter fullscreen mode Exit fullscreen mode

Застереження

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

💖 💪 🙅 🚩
yaroslavpodorvanov
Yaroslav Podorvanov

Posted on November 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Face recognition with Golang
ukrainian Face recognition with Golang

November 14, 2020