Skip to content

Commit

Permalink
EFRS-885 Add age/gender recognition as an independent plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
z268 committed Dec 16, 2020
1 parent 9a16c5e commit c7126c4
Show file tree
Hide file tree
Showing 333 changed files with 830 additions and 88,694 deletions.
26 changes: 9 additions & 17 deletions embedding-calculator/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ WORKDIR /app/ml
COPY requirements.txt .
RUN pip --no-cache-dir install -r requirements.txt

ARG SCANNER=Facenet2018
ENV SCANNER=$SCANNER
ARG BE_VERSION
ARG APP_VERSION_STRING
ENV BE_VERSION=$BE_VERSION
Expand All @@ -25,30 +23,24 @@ ENV PYTHONUNBUFFERED=0
ENV JOBLIB_MULTIPROCESSING=0

# download ML models
ARG DETECTION_MODEL="retinaface_r50_v1"
ARG CALCULATION_MODEL="arcface_r100_v1"
ENV DETECTION_MODEL=$DETECTION_MODEL CALCULATION_MODEL=$CALCULATION_MODEL
COPY srcext srcext
COPY pytest.ini .
COPY *.sh ./
RUN chmod +x *.sh
RUN ./prepare_scanners.sh

# install InsightFace packages
ARG MXNET
ARG GPU_ID
ENV GPU_ID=$GPU_ID
RUN if [[ "$SCANNER" == "InsightFace" ]]; then \
pip --no-cache-dir install ${MXNET:-mxnet-mkl==1.6.0} -e srcext/insightface/python-package; \
fi
ARG FACE_DETECTION_PLUGIN
ARG CALCULATION_PLUGIN
ARG EXTRA_PLUGINS
ENV FACE_DETECTION_PLUGIN=$FACE_DETECTION_PLUGIN CALCULATION_PLUGIN=$CALCULATION_PLUGIN \
EXTRA_PLUGINS=$EXTRA_PLUGINS
COPY src src
COPY srcext srcext
RUN python -m src.services.facescan.plugins.setup

# copy rest of the code
COPY src src
COPY tools tools
COPY sample_images sample_images

# run tests
ARG SKIP_TESTS
COPY pytest.ini .
RUN if [ -z $SKIP_TESTS ]; then pytest -m "not performance" /app/ml/src; fi

EXPOSE 3000
Expand Down
1 change: 1 addition & 0 deletions embedding-calculator/gpu.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ARG CUDNN_MAJOR_VERSION=7
ARG LIB_DIR_PREFIX=x86_64
ARG LIBNVINFER=6.0.1-1
ARG LIBNVINFER_MAJOR_VERSION=6
ENV CUDA=$CUDA

# Needed for string substitution
SHELL ["/bin/bash", "-c"]
Expand Down
30 changes: 0 additions & 30 deletions embedding-calculator/prepare_scanners.sh

This file was deleted.

1 change: 1 addition & 0 deletions embedding-calculator/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ cached-property==1.5.2
colour==0.1.5
flasgger==0.9.5
Flask==1.1.2
gdown~=3.12
Werkzeug==1.0.1

# tests
Expand Down
31 changes: 23 additions & 8 deletions embedding-calculator/src/_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing
# permissions and limitations under the License.
from typing import List
from typing import List, Optional

from flask import request
from flask.json import jsonify
from werkzeug.exceptions import BadRequest

from src.constants import ENV
from src.exceptions import NoFaceFoundError
from src.services.facescan.plugins import helpers
from src.services.facescan.scanner.facescanners import scanner
from src.services.flask_.constants import ARG
from src.services.flask_.needs_attached_file import needs_attached_file
Expand All @@ -28,31 +29,37 @@
def endpoints(app):
@app.route('/status')
def status_get():
availiable_plugins = {p.type: str(p) for p in helpers.get_face_plugins()}
calculator = helpers.get_calculator()
return jsonify(status='OK', build_version=ENV.BUILD_VERSION,
calculator_version=ENV.SCANNER)
calculator_version=str(calculator),
availiable_plugins=availiable_plugins)

@app.route('/find_faces', methods=['POST'])
@needs_attached_file
def find_faces_post():
faces = scanner.find_faces(
detector = helpers.get_detector()
face_plugins = helpers.get_face_plugins(_get_face_plugin_names())
faces = detector(
img=read_img(request.files['file']),
det_prob_threshold=_get_det_prob_threshold(request),
det_prob_threshold=_get_det_prob_threshold(),
face_plugins=face_plugins
)
faces = _limit(faces, request.values.get(ARG.LIMIT))
return jsonify(calculator_version=scanner.ID, result=faces)
plugins_versions = {p.type: str(p) for p in [detector] + face_plugins}
return jsonify(results=faces, plugins_versions=plugins_versions)

@app.route('/scan_faces', methods=['POST'])
@needs_attached_file
def scan_faces_post():
faces = scanner.scan(
img=read_img(request.files['file']),
det_prob_threshold=_get_det_prob_threshold(request)
det_prob_threshold=_get_det_prob_threshold()
)
faces = _limit(faces, request.values.get(ARG.LIMIT))
return jsonify(calculator_version=scanner.ID, result=faces)


def _get_det_prob_threshold(request):
def _get_det_prob_threshold():
det_prob_threshold_val = request.values.get(ARG.DET_PROB_THRESHOLD)
if det_prob_threshold_val is None:
return None
Expand All @@ -62,6 +69,14 @@ def _get_det_prob_threshold(request):
return det_prob_threshold


def _get_face_plugin_names() -> Optional[List[str]]:
if ARG.FACE_PLUGINS not in request.values:
return
return [
name for name in filter(None, request.values[ARG.FACE_PLUGINS].split(','))
]


def _limit(faces: List, limit: str = None) -> List:
"""
>>> _limit([1, 2, 3], None)
Expand Down
12 changes: 7 additions & 5 deletions embedding-calculator/src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,26 @@

import logging

from src.services.utils.pyutils import get_env, Constants
from src.services.utils.pyutils import get_env, get_env_split, Constants

_DEFAULT_SCANNER = 'Facenet2018'


class ENV(Constants):
ML_PORT = int(get_env('ML_PORT', '3000'))
SCANNER = get_env('SCANNER', _DEFAULT_SCANNER)
SCANNERS = [SCANNER]
IMG_LENGTH_LIMIT = int(get_env('IMG_LENGTH_LIMIT', '640'))

FACE_DETECTION_PLUGIN = get_env('FACE_DETECTION_PLUGIN', 'facenet.FaceDetector')
CALCULATION_PLUGIN = get_env('CALCULATION_PLUGIN', 'facenet.Calculator')
EXTRA_PLUGINS = get_env_split('EXTRA_PLUGINS', 'rude_carnie.AgeDetector,rude_carnie.GenderDetector')

LOGGING_LEVEL_NAME = get_env('LOGGING_LEVEL_NAME', 'debug').upper()
IS_DEV_ENV = get_env('FLASK_ENV', 'production') == 'development'
BUILD_VERSION = get_env('APP_VERSION_STRING', 'dev')

GPU_ID = int(get_env('GPU_ID', '-1'))
DETECTION_MODEL = get_env('DETECTION_MODEL', 'retinaface_r50_v1')
CALCULATION_MODEL = get_env('CALCULATION_MODEL', 'arcface_r100_v1')
CUDA = get_env('CUDA', '')
INTEL_OPTIMIZATION = get_env('INTEL_OPTIMIZATION', '')


LOGGING_LEVEL = logging._nameToLevel[ENV.LOGGING_LEVEL_NAME]
Expand Down
2 changes: 1 addition & 1 deletion embedding-calculator/src/docs/find_faces_post.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ tags:
- Core
summary: 'Find faces in the given image and return their bounding boxes.'
description: 'Returns bounding boxes of detected faces on the image.'
operationId: scanFacesPost
operationId: findFacesPost
consumes:
- multipart/form-data
produces:
Expand Down
10 changes: 0 additions & 10 deletions embedding-calculator/src/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,10 @@ class OneDimensionalImageIsGivenError(BadRequest):
description = "Given image has only one dimension"


class MoreThanOneFaceFoundError(BadRequest):
description = "Found more than one face in the given image"


class ClassifierIsAlreadyTrainingError(Locked):
description = "Classifier training is already in progress"


class NoTrainedEmbeddingClassifierFoundError(BadRequest):
description = "No classifier model is yet trained, please train a classifier first. If the problem persists, " \
"check the amount of unique faces saved, and whether all face embeddings have been migrated to " \
f"version '{ENV.SCANNER}'"


class NoFileFoundInDatabaseError(InternalServerError):
description = "File is not found in the database"

Expand Down
50 changes: 50 additions & 0 deletions embedding-calculator/src/services/dto/plugin_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import attr
from typing import Tuple, List, Optional, Dict

from src.services.dto.bounding_box import BoundingBoxDTO
from src.services.dto.json_encodable import JSONEncodable
from src.services.imgtools.types import Array1D, Array3D


class PluginResultDTO(JSONEncodable):
def to_json(self) -> dict:
""" Serialize only public properties """
return {k: v for k, v in self.__dict__.items() if not k.startswith('_')}


@attr.s(auto_attribs=True, frozen=True)
class EmbeddingDTO(PluginResultDTO):
embedding: Array1D


@attr.s(auto_attribs=True, frozen=True)
class GenderDTO(PluginResultDTO):
gender: str
gender_probability: float = attr.ib(converter=float, default=None)


@attr.s(auto_attribs=True, frozen=True)
class AgeDTO(PluginResultDTO):
age: Tuple[int, int]
age_probability: float = attr.ib(converter=float, default=None)


@attr.s(auto_attribs=True)
class FaceDTO(PluginResultDTO):
box: BoundingBoxDTO
_img: Optional[Array3D]
_face_img: Optional[Array3D]
_plugins_dto: List[PluginResultDTO] = attr.Factory(list)
execution_time: Dict[str, float] = attr.Factory(dict)

def to_json(self):
data = super().to_json()
for plugin_dto in self._plugins_dto:
data.update(plugin_dto.to_json())
return data

@property
def embedding(self):
for dto in self._plugins_dto:
if isinstance(dto, EmbeddingDTO):
return dto.embedding
23 changes: 14 additions & 9 deletions embedding-calculator/src/services/dto/scanned_face.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# or implied. See the License for the specific language governing
# permissions and limitations under the License.

from typing import Union
from typing import Union, Optional

import attr

Expand All @@ -22,23 +22,27 @@
from src.services.imgtools.types import Array1D, Array3D


@attr.s(auto_attribs=True)
class Face(JSONEncodable):
_img: Optional[Array3D]
_face_img: Optional[Array3D]
box: BoundingBoxDTO


@attr.s(auto_attribs=True, frozen=True)
class ScannedFaceDTO(JSONEncodable):
box: BoundingBoxDTO
embedding: Array1D


class ScannedFace(JSONEncodable):
def __init__(self, box: BoundingBoxDTO, embedding: Array1D, img: Union[Array3D, None], face_img: Array3D = None):
self.box = box
self.embedding = embedding
self.img = img
self._face_img = face_img
@attr.s(auto_attribs=True)
class ScannedFace(Face):
embedding: Array1D

@property
def face_img(self):
if not self._face_img:
self._face_img = crop_img(self.img, self.box)
self._face_img = crop_img(self._img, self.box)
return self._face_img

@property
Expand All @@ -53,4 +57,5 @@ def from_request(cls, result):
y_min=box_result['y_min'],
y_max=box_result['y_max'],
probability=box_result['probability']),
embedding=result['embedding'], img=None)
embedding=result['embedding'],
img=None, face_img=None)
32 changes: 32 additions & 0 deletions embedding-calculator/src/services/facescan/plugins/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright (c) 2020 the original author or authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing
# permissions and limitations under the License.

import os
from importlib.util import find_spec

modules_by_lib = {
'tensorflow': ('facenet', 'rude_carnie'),
'mexnet': ('insightface',)
}
modules_to_skip = []
for lib, modules in modules_by_lib.items():
if find_spec(lib) is None:
modules_to_skip.extend(modules)


def pytest_ignore_collect(path):
_, tail = os.path.split(path)
for module in modules:
if tail.startswith(module):
return True
Loading

0 comments on commit c7126c4

Please sign in to comment.