Используем gocv, чтобы определить возраст, эмоции и пол человека по фото

от автора

Opencv предоставляет широкие возможности по обработке изображений и работе с нейросетями. В данной статье мы напишем сервис, который позволит извлекать из изображений ряд параметров человека: пол, возраст, эмоции, а также местонахождение лица на фотографии. Получение данных характеристик бывает полезно для автоматического анализа видео и фото. Например, на конференции мы можем определить средний возраст участников, процентное соотношение мужчин и женщин, а также реакцию на конкретный доклад. Для демонстрации будем использовать модели caffe и onnx. Сервис напишем с использованием golang.

Ниже приведен пример изображения, на котором распознан пол и примерный возраст человека. В нашем сервисе мы будем возвращать JSON, в котором будут указаны возраст, эмоции, пол и местоположение лиц на фотографии.

Для начала нужно установить пакет для работы с opencv в go. Для этого прописываем команду:

go get -u -d gocv.io/x/gocv

Затем переходим в директорию с пакетом и пишем:

make install

Если всё прошло нормально, то в конце будет выведено:

gocv version: 0.37.0 opencv lib version: 4.10.0

В этой статье описана установка пакета для Linux. Если вам нужно установить пакет на другие платформы, то об этом можно почитать тут.

Теперь кратко рассмотрим структуру пакетов в нашем сервисе. Сервис будет разделён на несколько пакетов:

/config — в данной папке располагается файл с конфигом для запуска приложения

/internal — код проекта

/internal/api — код для апи сервис

/internal/config — код для работы с конфигом

/internal/recognizers — код для работы с предобученными моделями для распознавания параметров человека

/models — папка для размещения предобученных моделей (скачать модели можно тут)

/test_image — тестовые изображения

Написание кода начнем с модуля для работы с моделями. В сервисе будем использовать модели нескольких типов — Caffe, Onnx и pb. Для начала напишем код для обнаружения лиц на изображениях. Для этого необходимо использовать модель с расширением .pb. Сперва определим структуры необходимые для распознавания:

type FaceConfig struct {    Ratio      float64    Scalar     gocv.Scalar    swapRGB    bool    Pt         image.Point    Confidence float32 }   type Facebox struct {    Model   string    Config  string    FaceNet gocv.Net    Conf    FaceConfig }

Структура Facebox будет хранить информацию о пути к модели и ее конфигурации, объект, который будет использоваться для распознавания, и отдельный конфиг, который будет определять ряд параметров при распознавании лиц на конкретном изображении.

Далее напишем функции инициализации для Facebox:

func NewFacebox(model, config string) (*Facebox, error) {      faceNet := gocv.ReadNet(model, config)    if faceNet.Empty() {        return nil, fmt.Errorf("reading model error: %v %v\n", model, config)    }      var facebox = Facebox{        Model:   model,        Config:  config,        FaceNet: faceNet,        Conf: FaceConfig{            Ratio:      1,            Scalar:     gocv.Scalar{Val1: 104, Val2: 177, Val3: 123},            swapRGB:    false,            Pt:         image.Pt(300, 300),            Confidence: 0.6,        },    }      err := faceNet.SetPreferableBackend(gocv.NetBackendDefault)    if err != nil {        return nil, err    }    err = faceNet.SetPreferableTarget(gocv.NetTargetCPU)      if err != nil {        return nil, err    }      return &facebox, nil }

В функцию мы передаем пути к модели и её конфигу, далее указываем, что для модели будет использоваться CPU, в случае успешной инициализации возвращаем структуру, в ином случае — ошибку. Также нам нужно задать ряд параметров, которые будут использоваться при конвертации исходного изображения в Blob при работе нейронной сети.

Далее напишем код, который позволит получать координаты лиц на изображении. Для начала создадим структуру, которая будет использоваться для хранения координат лиц, а также функцию ExtractFacesImg — извлекать лица из переданного в неё изображения и возвращать их в виде слайса Mat.

type FaceBoxResult struct {    Left   int    Top    int    Right  int    Bottom int }   func (f *Facebox) ExtractFacesImg(img *gocv.Mat, coord []FaceBoxResult) []gocv.Mat {    var faces = make([]gocv.Mat, 0, len(coord))    for _, faceCoord := range coord {        r := image.Rect(faceCoord.Left, faceCoord.Top, faceCoord.Right, faceCoord.Bottom)        mat := img.Region(r)        faces = append(faces, mat)    }    return faces }

Для получения координат мы конвертируем изображение в Blob, после помещаем его на вход нейросети. После того как нейросеть отработает, мы проходимся по всем точкам, в нашем изображении нужно итерироваться через каждые 7 точек. После для каждого лица проверяем уровень уверенности в том, что перед нами было лицо, если значение ниже порогового (которое мы сами устанавливаем в параметрах), то мы просто отсеиваем изображение, в ином случае получаем координаты, проверяем их на корректность и после этого добавляем в слайс с координатами.

func (f *Facebox) GetFaces(img *gocv.Mat) ([]FaceBoxResult, error) {    if img.Empty() {        return nil, ErrEmptyImage    }      blob := gocv.BlobFromImage(*img, f.Conf.Ratio, f.Conf.Pt,        f.Conf.Scalar, f.Conf.swapRGB, false)      f.FaceNet.SetInput(blob, "data")      var faces []FaceBoxResult    var faceErrors []error    faceImg := f.FaceNet.Forward("detection_out")    for i := 0; i < faceImg.Total(); i += 7 {        confidence := faceImg.GetFloatAt(0, i+2)        if confidence > f.Conf.Confidence {            left := int(faceImg.GetFloatAt(0, i+3) * float32(img.Cols()))            top := int(faceImg.GetFloatAt(0, i+4) * float32(img.Rows()))            right := int(faceImg.GetFloatAt(0, i+5) * float32(img.Cols()))            bottom := int(faceImg.GetFloatAt(0, i+6) * float32(img.Rows()))            r := image.Rect(left, top, right, bottom)            if r.Max.X < img.Cols() && r.Max.Y < img.Rows() && r.Min.X > 0 && r.Min.Y > 0 {                faces = append(faces, FaceBoxResult{                    Left:   left,                    Top:    top,                    Right:  right,                    Bottom: bottom,                })            } else {                faceErrors = append(faceErrors, fmt.Errorf("facebox: bad coordinates rectangle: %v", r))            }          }      }      return faces, errors.Join(faceErrors...) }

Далее мы напишем код для определения эмоций человека. Для этого будем использовать нейронные сети на основе разных архитектур Caffe и ONNX. Для начала рассмотрим нейронную сеть на основе Caffe. В целом инициализация не имеет каких-то больших отличий от инициализации предыдущей модели за исключением того, что тут мы передаём только пути к модели и её конфигу, не задавая дополнительных параметров.

type EmotionCaffe struct {    Model  string    Config string    Net    gocv.Net }   func (e *EmotionCaffe) Close() error {    return e.Net.Close() }   func NewEmotionCaffe(model, config string) (*EmotionCaffe, error) {      emotionNet := gocv.ReadNet(model, config)    if emotionNet.Empty() {        return nil, fmt.Errorf("%v %v %v\n", ErrModelReading, model, config)    }      var emotion = EmotionCaffe{        Model:  model,        Config: config,        Net:    emotionNet,    }      err := emotionNet.SetPreferableBackend(gocv.NetBackendDefault)    if err != nil {        return nil, err    }    err = emotionNet.SetPreferableTarget(gocv.NetTargetCPU)      if err != nil {        return nil, err    }      return &emotion, nil }

Затем напишем код для получения эмоции конкретного лица в функции GetEmotion. Далее мы преобразуем изображение в Blob, после чего модель попытается определить эмоции человека. Модель поддерживает распознавание следующих эмоций:

var EmotionsCaffe = []string{"Angry", "Disgust", "Fear", "Happy", "Neutral", "Sad", "Surprise"}   func (e *EmotionCaffe) GetEmotion(face *gocv.Mat) string {    scalar := gocv.NewScalar(0, 0, 0, 0)    blob := gocv.BlobFromImage(*face, ratio, image.Pt(227, 227), scalar, swapRGB, false)    e.Net.SetInput(blob, "")    emoPreds := e.Net.Forward("")    _, _, _, emoLoc := gocv.MinMaxLoc(emoPreds)    return EmotionsCaffe[emoLoc.X] } 

Далее напишем функцию GetCaffeEmotion, которая будет извлекать все лица из передаваемого изображения и затем помещать результаты в отдельный слайс.

func (r *Recognizer) GetCaffeEmotion(mat *gocv.Mat) ([]EmotionResponse, error) {    faces, errs := r.Facebox.GetFaces(mat)    if len(faces) == 0 && errs != nil {        return nil, errs    }    imgs := r.Facebox.ExtractFacesImg(mat, faces)      var res = make([]EmotionResponse, 0, len(faces))      for i, img := range imgs {        res = append(res, EmotionResponse{            Coordinates: faces[i],            Emotion:     r.EmotionCaffe.GetEmotion(&img),        })    }      return res, errs   } 

Далее добавим распознавание эмоций при помощи другой модели. Работа с моделью ONNX немного отличается от предыдущей. Для инициализации модели нам не нужен конфиг, достаточно самой модели:

type EmotionONNX struct {    Model string    Net   gocv.Net }   func (e *EmotionONNX) Close() error {    return e.Net.Close() }   func NewEmotionONNX(model string) (*EmotionONNX, error) {      emotionNet := gocv.ReadNetFromONNX(model)    if emotionNet.Empty() {        return nil, fmt.Errorf("%v %v\n", ErrModelReading, model)    }      var emotion = EmotionONNX{        Model: model,        Net:   emotionNet,    }      err := emotionNet.SetPreferableBackend(gocv.NetBackendDefault)    if err != nil {        return nil, err    }    err = emotionNet.SetPreferableTarget(gocv.NetTargetCPU)      if err != nil {        return nil, err    }      return &emotion, nil } 

При работе с Onnx есть некоторые особенности. Прежде чем получать Blob из изображения, мы должны конвертировать изображение Mat в другой формат при помощи функций CvtColor и ConvertTo. Если этого не сделать, то при обработке изображения моделью приложение упадет. Также важно создавать переменную с типом Mat при помощи специальной функции gocv.NewMat(). Если создать переменную без этой функции, то такой объект также может привести к падению приложения.

func (e *EmotionONNX) GetEmotion(face *gocv.Mat) string {      meanVal := gocv.Scalar{Val1: 78.4263377603,        Val2: 87.7689143744,        Val3: 114.895847746}      //создавать нужно так    var imgFER = gocv.NewMat()      gocv.CvtColor(*face, &imgFER, gocv.ColorBGRToGray)    imgFER.ConvertTo(&imgFER, gocv.MatTypeCV32FC1)    blobFER := gocv.BlobFromImage(imgFER, ratio, image.Pt(64, 64),        meanVal, false, swapRGB)      e.Net.SetInput(blobFER, "")    output := e.Net.Forward("")      _, _, _, emoONNXLoc := gocv.MinMaxLoc(output)    return EmotionsONNX[emoONNXLoc.X] } 

Код для работы с общим изображением практически такой же, как и для моделей Caffe:

var EmotionsONNX = []string{"Neutral", "Happy", "Surprise", "Sad", "Anger", "Disgust", "Fear", "Contempt"}   func (r *Recognizer) GetOnnxEmotion(mat *gocv.Mat) ([]EmotionResponse, error) {    faces, errs := r.Facebox.GetFaces(mat)    if len(faces) == 0 && errs != nil {        return nil, errs    }    imgs := r.Facebox.ExtractFacesImg(mat, faces)      var res = make([]EmotionResponse, 0, len(faces))      for i, img := range imgs {        res = append(res, EmotionResponse{            Coordinates: faces[i],            Emotion:     r.EmotionONNX.GetEmotion(&img),        })    }      return res, errs   } 

Код для работы с возрастом и полом использует модели Caffe, поэтому их код с работой будет похож на код для определения эмоций, поэтому, думаю, дополнительных пояснений не потребуется. Ниже приведен код для определения пола человека:

type Gender struct {    Model  string    Config string    Net    gocv.Net }   func (g *Gender) Close() error {    return g.Net.Close() }   func NewGender(model, config string) (*Gender, error) {      genderNet := gocv.ReadNet(model, config)    if genderNet.Empty() {        return nil, fmt.Errorf("%v %v %v\n", ErrModelReading, model, config)    }      var emotion = Gender{        Model:  model,        Config: config,        Net:    genderNet,    }      err := genderNet.SetPreferableBackend(gocv.NetBackendDefault)    if err != nil {        return nil, err    }    err = genderNet.SetPreferableTarget(gocv.NetTargetCPU)      if err != nil {        return nil, err    }      return &emotion, nil }   func (g *Gender) GetGender(face *gocv.Mat) string {    scalar := gocv.NewScalar(0, 0, 0, 0)    blob := gocv.BlobFromImage(*face, ratio, image.Pt(227, 227), scalar, swapRGB, false)    g.Net.SetInput(blob, "")    genderPreds := g.Net.Forward("")    _, _, _, ageLoc := gocv.MinMaxLoc(genderPreds)    return Genders[ageLoc.X] }   func (r *Recognizer) GetGender(mat *gocv.Mat) ([]GenderResponse, error) {    faces, errs := r.Facebox.GetFaces(mat)    if len(faces) == 0 && errs != nil {        return nil, errs    }    imgs := r.Facebox.ExtractFacesImg(mat, faces)      var res = make([]GenderResponse, 0, len(faces))      for i, img := range imgs {        res = append(res, GenderResponse{            Coordinates: faces[i],            Gender:      r.Gender.GetGender(&img),        })    }      return res, errs   } 

Далее рассмотрим код для определения возраста человека:

type Age struct {    Model  string    Config string    Net    gocv.Net }   func (a *Age) Close() error {    return a.Net.Close() }   func NewAge(model, config string) (*Age, error) {      ageNet := gocv.ReadNet(model, config)    if ageNet.Empty() {        return nil, fmt.Errorf("%v %v %v\n", ErrModelReading, model, config)    }      var age = Age{        Model:  model,        Config: config,        Net:    ageNet,    }      err := ageNet.SetPreferableBackend(gocv.NetBackendDefault)    if err != nil {        return nil, err    }    err = ageNet.SetPreferableTarget(gocv.NetTargetCPU)      if err != nil {        return nil, err    }      return &age, nil }   func (a *Age) GetAge(face *gocv.Mat) string {    scalar := gocv.NewScalar(0, 0, 0, 0)    blob := gocv.BlobFromImage(*face, ratio, image.Pt(227, 227), scalar, swapRGB, false)    a.Net.SetInput(blob, "")    agePreds := a.Net.Forward("")    _, _, _, ageLoc := gocv.MinMaxLoc(agePreds)    return Ages[ageLoc.X] }   func (r *Recognizer) GetAge(mat *gocv.Mat) ([]AgeResponse, error) {    faces, errs := r.Facebox.GetFaces(mat)    if len(faces) == 0 && errs != nil {        return nil, errs    }    imgs := r.Facebox.ExtractFacesImg(mat, faces)      var res = make([]AgeResponse, 0, len(faces))      for i, img := range imgs {        res = append(res, AgeResponse{            Coordinates: faces[i],            Age:         r.Age.GetAge(&img),        })    }      return res, errs   } 

Единственное, что стоит отметить: возраст определяется не в виде конкретного числа, а как интервал. То есть модель говорит о том, что возраст человека предположительно от 20 до 36 лет. Ниже приведены все интервалы в виде слайса:

var Ages = []string{"0-2", "3-7", "8-12", "13-20", "20-36", "37-47", "48-55", "56-100"}

Теперь рассмотрим основной код для самого сервиса. Прежде всего напишем конфиг и код для работы с ним:

{  "server":{    "host": "127.0.0.1",    "port": 8080  },    "facebox": {    "model":"models/face_detector_uint8.pb",    "config":"models/face_detector.pbtxt"  },    "emotion_caffe": {    "model":"models/emotion.caffemodel",    "config":"models/emotion.prototxt"  },    "emotion_onnx": {    "model":"models/emotion.onnx"  },    "gender": {    "model":"models/gender.caffemodel",    "config":"models/gender.prototxt"  },    "age": {    "model":"models/age.caffemodel",    "config":"models/age.prototxt"  }   } 

В данном конфиге мы определяем путь к нашим моделям, а также адрес и порт, на котором будет работать приложение. Код для работы с конфигом представлен ниже:

package config   import (    "encoding/json"    "os" )   type Config struct {    Server struct {        Host string `json:"host"`        Port int    `json:"port"`    } `json:"server"`    Facebox struct {        Model  string `json:"model"`        Config string `json:"config"`    } `json:"facebox"`    EmotionCaffe struct {        Model  string `json:"model"`        Config string `json:"config"`    } `json:"emotion_caffe"`    EmotionOnnx struct {        Model string `json:"model"`    } `json:"emotion_onnx"`    Gender struct {        Model  string `json:"model"`        Config string `json:"config"`    } `json:"gender"`    Age struct {        Model  string `json:"model"`        Config string `json:"config"`    } `json:"age"` }   func ReadConfig(path string) (*Config, error) {    b, err := os.ReadFile(path)    if err != nil {        return nil, err    }      var cfg Config      err = json.Unmarshal(b, &cfg)    if err != nil {        return nil, err    }      return &cfg, nil   } 

Для написания REST APi мы выбрали fiber. Для начала определим структуру App, которая будет хранить роутер, объект с моделями и конфиг.  

type App struct {    rec       *recognizers.Recognizer    routerApi *fiber.App    cfg       *config.Config }

Затем напишем функцию инициализации основного объекта приложения, в ней будем инициализировать наши модели и API.

func NewApp(cfg *config.Config, routerApi *fiber.App) (*App, error) {      r, err := recognizers.New(cfg)    if err != nil {        return nil, err    }      app := &App{        rec:       r,        routerApi: routerApi,        cfg:       cfg,    }      app.InitApi()      return app, nil   }

API приложения позволит получать информацию при помощи различных моделей, например, если нужно получить информацию только о возрасте, то мы можем отправить запрос на /age. Если нам будет нужна вся возможная информация об изображении, то нам нужно отправить запрос на /full/info.

func (a *App) InitApi() {    a.routerApi.Post("/facepos", a.GetFacePos)    a.routerApi.Post("/emotion/onnx", a.GetEmotionONNX)    a.routerApi.Post("/emotion/caffe", a.GetCaffeEmotion)    a.routerApi.Post("/age", a.GetAge)    a.routerApi.Post("/gender", a.GetGender)    a.routerApi.Post("/full/info", a.GetFullInfo) }

Также напишем функцию для запуска приложения на определенном адресе:

func (a *App) Start() error {    return a.routerApi.Listen(fmt.Sprintf("%s:%d", a.cfg.Server.Host, a.cfg.Server.Port)) } 

Далее рассмотрим несколько конкретных функций, которые реализуют точки API. Для начала разберем функцию, которая возвращает координаты лица. Сперва мы получаем изображение тела запроса в функции extractImageFrom и там же конвертируем его в Mat при помощи функции gocv.IMDecode с флагом gocv.IMReadUnchanged. После отправляем изображение на вход функции для распознавания координат и отправляем ответ пользователю с результатами.

Обратите внимание, что изображение нужно помещать в forms в запросе и указывать у них поле face (ниже будет приведён пример, как отправить запрос в postman).

func (a *App) GetFacePos(c *fiber.Ctx) error {      mat, err := extractImageFrom(c)    if err != nil {       return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{            "error": err.Error(),        })    }      res, err := a.rec.Facebox.GetFaces(mat)    if err != nil {        return err    }      return c.Status(fiber.StatusOK).JSON(fiber.Map{        "facebox": res}) }  func extractImageFrom(c *fiber.Ctx) (*gocv.Mat, error) {    file, err := c.FormFile("face")    if err != nil {        return nil, err    }      f, err := file.Open()    if err != nil {        return nil, err    }    defer f.Close()      b, err := io.ReadAll(f)    if err != nil {        return nil, err    }      mat, err := gocv.IMDecode(b, gocv.IMReadUnchanged)    if err != nil {        return nil, err    }      return &mat, err }

В целом код для остальных функций API не сильно отличается, в основном отличается только функция для анализа изображения, например, функция GetFullInfo, которая отвечает за получение всей возможной информации об изображении.

func (a *App) GetFullInfo(c *fiber.Ctx) error {      mat, err := extractImageFrom(c)    if err != nil {        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{            "error": err.Error(),        })    }      res, err := a.rec.GetFullIno(mat)    if err != nil {        return err    }      return c.Status(fiber.StatusOK).JSON(fiber.Map{        "results": res}) }

Теперь проверим как работает наш сервис, для этого возьмем например такое изображение:

При отправке запроса помещаем изображение в form-data и указывает ключ face. Запрос сделаем  при помощи Postman, в ответе мы видим JSON со всеми необходимыми нам характеристиками.

В данной статье мы разобрали, как использовать opencv в go с моделями Caffe и ONNX, а также написали простой сервис для демонстрации его работы. Исходный код размещен тут.

Автор статьи @yurii_habr


НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.


ссылка на оригинал статьи https://habr.com/ru/articles/833934/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *