Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EFSR-885 gender age plugins #281

Merged
merged 5 commits into from
Dec 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
34 changes: 11 additions & 23 deletions embedding-calculator/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,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 @@ -26,35 +24,25 @@ ENV PYTHONUNBUFFERED=0
ENV JOBLIB_MULTIPROCESSING=0

# download ML models
ARG DETECTION_MODEL
ARG CALCULATION_MODEL
ENV DETECTION_MODEL=${DETECTION_MODEL:-retinaface_r50_v1}
ENV CALCULATION_MODEL=${CALCULATION_MODEL:-arcface_r100_v1}
ARG INTEL_OPTIMIZATION=false
ARG GPU_IDX=-1
ENV GPU_IDX=$GPU_IDX INTEL_OPTIMIZATION=$INTEL_OPTIMIZATION
ARG FACE_DETECTION_PLUGIN="facenet.FaceDetector"
ARG CALCULATION_PLUGIN="facenet.Calculator"
ARG EXTRA_PLUGINS=""
ENV FACE_DETECTION_PLUGIN=$FACE_DETECTION_PLUGIN CALCULATION_PLUGIN=$CALCULATION_PLUGIN \
EXTRA_PLUGINS=$EXTRA_PLUGINS
COPY src src
COPY srcext srcext
COPY pytest.ini .
COPY *.sh ./
RUN chmod +x *.sh
RUN ./prepare_scanners.sh

# install InsightFace packages
ARG INTEL_OPTIMIZATION
ARG GPU_IDX
ENV GPU_IDX=$GPU_IDX
ENV MXNET_MOD=${GPU_IDX:+cu101}${INTEL_OPTIMIZATION:+mkl}
ENV MXNET_LIB=mxnet${MXNET_MOD:+-$MXNET_MOD}
ENV MXNET_VER="<1.7"

RUN if [[ "$SCANNER" == "InsightFace" ]]; then \
pip --no-cache-dir install "$MXNET_LIB$MXNET_VER" -e srcext/insightface/python-package; \
fi
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
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -66,7 +67,7 @@ RUN ln -s $(which $PYTHON) /usr/local/bin/python


# Variables for MXNET
ENV MXNET=mxnet_cu101mkl MXNET_CPU_WORKER_NTHREADS=24
ENV MXNET_CPU_WORKER_NTHREADS=24
ENV MXNET_ENGINE_TYPE=ThreadedEnginePerDevice MXNET_CUDNN_AUTOTUNE_DEFAULT=0

# No access to GPU devices in the build stage, so skip tests
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
35 changes: 27 additions & 8 deletions embedding-calculator/src/_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,59 @@
# 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 managers
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
from src.services.imgtools.read_img import read_img
from src.services.utils.pyutils import Constants


def endpoints(app):
@app.route('/status')
def status_get():
available_plugins = {p.slug: str(p)
for p in managers.plugin_manager.plugins}
calculator = managers.plugin_manager.calculator
return jsonify(status='OK', build_version=ENV.BUILD_VERSION,
calculator_version=ENV.SCANNER)
calculator_version=str(calculator),
available_plugins=available_plugins)

@app.route('/find_faces', methods=['POST'])
@needs_attached_file
def find_faces_post():
faces = scanner.find_faces(
detector = managers.plugin_manager.detector
face_plugins = managers.plugin_manager.filter_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.slug: 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 +73,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 Constants.split(request.values[ARG.FACE_PLUGINS])
]


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

import logging

from src.services.utils.pyutils import get_env, Constants
from src.services.utils.pyutils import get_env, get_env_split, get_env_bool, 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', '')

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_IDX = int(get_env('GPU_IDX', '-1'))
DETECTION_MODEL = get_env('DETECTION_MODEL', 'retinaface_r50_v1')
CALCULATION_MODEL = get_env('CALCULATION_MODEL', 'arcface_r100_v1')
INTEL_OPTIMIZATION = get_env_bool('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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this swagger file? Then I don't see descriptions of gender and age plugins

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a fix of wrong name. Swagger description updated in PR #302

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)
Loading