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/
Добавить комментарий