DEV Community

Yaroslav Podorvanov
Yaroslav Podorvanov

Posted on • Updated on

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

Top comments (0)