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 9, 2020
1 parent ffe8da7 commit 59f4b80
Show file tree
Hide file tree
Showing 24 changed files with 774 additions and 78 deletions.
4 changes: 3 additions & 1 deletion embedding-calculator/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ 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
ARG GENDERAGE_MODEL="genderage_v1"
ENV DETECTION_MODEL=$DETECTION_MODEL CALCULATION_MODEL=$CALCULATION_MODEL \
GENDERAGE_MODEL=$GENDERAGE_MODEL
COPY srcext srcext
COPY pytest.ini .
COPY *.sh ./
Expand Down
6 changes: 1 addition & 5 deletions embedding-calculator/prepare_scanners.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
if [[ "$SCANNER" == "InsightFace" ]]; then
MODELS_PATH=~/.insightface/models
mkdir -p $MODELS_PATH
for MODEL in $DETECTION_MODEL $CALCULATION_MODEL
for MODEL in $DETECTION_MODEL $CALCULATION_MODEL $GENDERAGE_MODEL
do
# trying to find a pre-downloaded model
DIR=~/srcext/insightface/models/$MODEL
Expand All @@ -19,12 +19,8 @@ if [[ "$SCANNER" == "InsightFace" ]]; then
# https://github.com/deepinsight/insightface/issues/764
sed -i 's/limited_workspace/None/g' $MODELS_PATH/$MODEL/*.json
done
else
echo " --ignore=src/services/facescan/scanner/insightface" >> pytest.ini
fi

if [[ "$SCANNER" == "Facenet2018" ]]; then
pip install --no-cache-dir tensorflow~=1.15.4
else
echo " --ignore=src/services/facescan/scanner/facenet" >> pytest.ini
fi
20 changes: 11 additions & 9 deletions embedding-calculator/sample_images/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@
class Row:
img_name: str
noses: List[Tuple[int, int]]
is_male: bool = None
age: int = None
include_to_tests: bool = True


SAMPLE_IMAGES = [
Row('000_5.jpg', [(219, 105), (300, 252), (392, 220), (469, 309), (600, 294)]),
Row('001_A.jpg', [(2109, 2261)]),
Row('002_A.jpg', [(2146, 2424)]),
Row('003_A.jpg', [(3210, 1382)]),
Row('004_A.jpg', [(1312, 1969)]),
Row('005_A.jpg', [(2092, 2871)]),
Row('006_A.jpg', [(1864, 3041)]),
Row('007_B.jpg', [(210, 292)]),
Row('008_B.jpg', [(225, 256)]),
Row('009_C.jpg', [(166, 236)]),
Row('001_A.jpg', [(2109, 2261)]), # wrong gender recognition
Row('002_A.jpg', [(2146, 2424)]), # wrong gender recognition
Row('003_A.jpg', [(3210, 1382)], False, 30),
Row('004_A.jpg', [(1312, 1969)], False, 30),
Row('005_A.jpg', [(2092, 2871)], False, 30),
Row('006_A.jpg', [(1864, 3041)], False, 30),
Row('007_B.jpg', [(210, 292)], False, 23),
Row('008_B.jpg', [(225, 256)]), # wrong gender recognition
Row('009_C.jpg', [(166, 236)], True, 32),
Row('010_2.jpg', [(354, 232), (505, 258)]),
Row('011_3.jpg', [(295, 266), (484, 245), (385, 216)]),
Row('012_4.jpg', [(208, 205), (318, 241), (454, 228), (641, 240)]),
Expand Down
38 changes: 30 additions & 8 deletions embedding-calculator/src/_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
# 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 import helpers
from src.services.facescan.scanner import facescanner as scanners
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 +30,37 @@
def endpoints(app):
@app.route('/status')
def status_get():
availiable_plugins = {p.name: p.backend for p in helpers.get_plugins()}
return jsonify(status='OK', build_version=ENV.BUILD_VERSION,
calculator_version=ENV.SCANNER)
calculator_version=scanner.ID,
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(_get_det_plugin_name())
face_plugins = helpers.get_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.name: p.backend 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 +70,20 @@ def _get_det_prob_threshold(request):
return det_prob_threshold


def _get_det_plugin_name() -> Optional[str]:
if ARG.DET_PLUGIN not in request.values:
return
return request.values.get(ARG.DET_PLUGIN)


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


def _limit(faces: List, limit: str = None) -> List:
"""
>>> _limit([1, 2, 3], None)
Expand Down
23 changes: 23 additions & 0 deletions embedding-calculator/src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,30 @@ class ENV(Constants):
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')
GENDERAGE_MODEL = get_env('GENDERAGE_MODEL', 'genderage_v1')


LOGGING_LEVEL = logging._nameToLevel[ENV.LOGGING_LEVEL_NAME]
ENV_MAIN = ENV


MXNET_FACE_DETECTORS = (
'.plugins.insightface.FaceDetector',
)
MXNET_FACE_PLUGINS = (
'.plugins.insightface.Calculator',
'src.services.facescan.plugins.insightface.GenderAgeDetector',
)

TF_FACE_DETECTORS = (
'.plugins.facenet.FaceDetector',
)
TF_FACE_PLUGINS = (
'.plugins.facenet.Calculator',
'.plugins.rude_carnie.AgeDetector',
'.plugins.rude_carnie.GenderDetector',
# '.plugins.insightface.GenderAgeDetector',
)

FACE_DETECTORS = TF_FACE_DETECTORS
FACE_PLUGINS = TF_FACE_PLUGINS
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
7 changes: 3 additions & 4 deletions embedding-calculator/src/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ 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"

Expand Down Expand Up @@ -74,3 +70,6 @@ class CouldNotConnectToDatabase(InternalServerError):
class NotEnoughUniqueFacesError(BadRequest):
description = "Not enough unique faces to start training a new classifier model. " \
"Deleting existing classifiers, if any."

class InvalidFaceDetectorPlugin(BadRequest):
description = "Invalid face detector plugin"
44 changes: 44 additions & 0 deletions embedding-calculator/src/services/dto/plugin_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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
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/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):
_, last_path = os.path.split(path)
for module in modules:
if last_path.startswith(module):
return True
Loading

0 comments on commit 59f4b80

Please sign in to comment.