Skip to content
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# these values configure the build of the docker images
BUILD_TAG=latest
DEVTOOLS=false

# the port on which the api will be available
APP_PORT=80

# paths to the directories where data will be persisted on disk
DATA_DIR=./persisted_data/prod/images
DB_DIR=./persisted_data/prod/database
DEVTOOLS=false

# queue configuration
IMAGE_PROCESSOR_QUEUE=faceanalysis

# configuration values for mysql
MYSQL_USER=faceanalysisrw
MYSQL_PASSWORD=some-secure-string
MYSQL_ROOT_PASSWORD=some-very-secure-string
MYSQL_DATABASE=faceanalysis

# allowed values are DEBUG, INFO, WARNING, ERROR and CRITICAL
LOGGING_LEVEL=DEBUG

# separate multiple extensions with underscores
ALLOWED_IMAGE_FILE_EXTENSIONS=JPG_PNG

# api access token configuration
TOKEN_SECRET_KEY=some-long-random-string
DEFAULT_TOKEN_EXPIRATION_SECS=500

# maximum distance between two face vectors for them to be considered the same person
DISTANCE_SCORE_THRESHOLD=0.6

# docker image name of the algorithm to use for face vectorization
FACE_VECTORIZE_ALGORITHM=cwolff/face_recognition
8 changes: 6 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
language: generic

services:
- docker
- docker

before_install:
- openssl aes-256-cbc -K $encrypted_e08e3836e1cc_key -iv $encrypted_e08e3836e1cc_iv
-in ./.travis/secrets.env.enc -out ./secrets.env -d

script:
- ./run-test.sh
- ./run-test.sh
Binary file added .travis/secrets.env.enc
Binary file not shown.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
1. Create an Azure VM (preferably Ubuntu 16.04)
2. Install Docker and Docker Compose
3. Clone this repo
4. Replace default environment variables in environment_variables.yml
4. Replace default configuration values in `.env` and `secrets.env`
5. To run tests type './run-test.sh' from within the top level directory
6. To run in production type './run-prod.sh' from within the top level directory
7. If you would like to clear the production database, run './delete-prod-data.sh'
Expand Down
41 changes: 41 additions & 0 deletions algorithms/face_recognition/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
FROM python:3.6-slim-stretch

RUN apt-get -y update && \
apt-get install -y --fix-missing \
build-essential \
cmake \
gfortran \
git \
wget \
curl \
graphicsmagick \
libgraphicsmagick1-dev \
libatlas-dev \
libavcodec-dev \
libavformat-dev \
libgtk2.0-dev \
libjpeg-dev \
liblapack-dev \
libswscale-dev \
pkg-config \
python3-dev \
python3-numpy \
software-properties-common \
zip && \
apt-get clean && \
rm -rf /tmp/* /var/tmp/*

RUN cd ~ && \
mkdir -p dlib && \
git clone -b 'v19.9' --single-branch https://github.com/davisking/dlib.git dlib/ && \
cd dlib/ && \
python3 setup.py install --yes USE_AVX_INSTRUCTIONS

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY vectorize.py .

ENTRYPOINT ["python3", "vectorize.py"]
1 change: 1 addition & 0 deletions algorithms/face_recognition/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
face-recognition==1.0.0
35 changes: 35 additions & 0 deletions algorithms/face_recognition/vectorize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json
import face_recognition as fr


def get_face_vectors(img_path):
img = fr.load_image_file(img_path)
face_locations = fr.face_locations(img)
face_vectors = []
for top, right, bottom, left in face_locations:
cropped_img = img[top:bottom, left:right]
cropped_features = fr.face_encodings(cropped_img)
if cropped_features:
face_vector = cropped_features[0]
face_vectors.append(face_vector.tolist())
return face_vectors


def _cli():
from argparse import ArgumentParser
from argparse import FileType

parser = ArgumentParser(description=__doc__)
parser.add_argument('image', type=FileType('r'))

args = parser.parse_args()
image = args.image
image.close()
img_path = image.name

vectors = get_face_vectors(img_path)
print(json.dumps({'faceVectors': vectors}))


if __name__ == '__main__':
_cli()
33 changes: 1 addition & 32 deletions app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,9 @@ FROM python:3.5-slim
RUN apt-get -y update \
&& apt-get install -y \
mysql-client \
&& apt-get clean \
&& rm -rf /tmp/* /var/tmp/*

RUN apt-get -y update \
&& apt-get install -y --fix-missing \
build-essential \
cmake \
gfortran \
git \
wget \
curl \
graphicsmagick \
libgraphicsmagick1-dev \
libatlas-dev \
libavcodec-dev \
libavformat-dev \
libboost-all-dev \
libgtk2.0-dev \
libjpeg-dev \
liblapack-dev \
libswscale-dev \
pkg-config \
python3-dev \
python3-numpy \
software-properties-common \
zip \
mysql-client \
&& apt-get clean \
&& rm -rf /tmp/* /var/tmp/* \
&& cd ~ \
&& mkdir -p dlib \
&& git clone -b 'v19.7' --single-branch https://github.com/davisking/dlib.git dlib/ \
&& cd dlib/ \
&& python3 setup.py install --yes USE_AVX_INSTRUCTIONS
&& rm -rf /tmp/* /var/tmp/*

WORKDIR /app

Expand Down
35 changes: 17 additions & 18 deletions app/faceanalysis/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
from http import HTTPStatus
import werkzeug
from werkzeug.utils import secure_filename
from azure.storage.queue import QueueService
from flask_restful import Resource, Api, reqparse
from flask import Flask, g
from .models.models import Match, Image, User, ImageStatus
from .models.database_manager import DatabaseManager
from .models.database_manager import get_database_manager
from .models.image_status_enum import ImageStatusEnum
from .log import get_logger
from .queue_poll import create_queue_service
from .auth import auth
from .settings import IMAGE_PROCESSOR_QUEUE, ALLOWED_EXTENSIONS

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.join(
Expand All @@ -20,10 +21,8 @@
'images')
app.url_map.strict_slashes = False
api = Api(app)
queue_service = QueueService(account_name=os.environ['STORAGE_ACCOUNT_NAME'],
account_key=os.environ['STORAGE_ACCOUNT_KEY'])
queue_service.create_queue(os.environ['IMAGE_PROCESSOR_QUEUE'])
logger = get_logger(__name__, os.environ['LOGGING_LEVEL'])
queue_service = create_queue_service(IMAGE_PROCESSOR_QUEUE)
logger = get_logger(__name__)


class AuthenticationToken(Resource):
Expand All @@ -46,7 +45,7 @@ def post(self):
args = parser.parse_args()
username = args['username']
password = args['password']
db = DatabaseManager()
db = get_database_manager()
session = db.get_session()
query = session.query(User).filter(User.username == username).first()
session.close()
Expand All @@ -71,7 +70,7 @@ def post(self):
help="img_id missing in the post body")
args = parser.parse_args()
img_id = args['img_id']
db = DatabaseManager()
db = get_database_manager()
session = db.get_session()
img_status = session.query(ImageStatus).filter(
ImageStatus.img_id == img_id).first()
Expand All @@ -81,8 +80,7 @@ def post(self):
return ('Image previously placed on queue',
HTTPStatus.BAD_REQUEST.value)
try:
queue_service.put_message(os.environ['IMAGE_PROCESSOR_QUEUE'],
img_id)
queue_service.put_message(IMAGE_PROCESSOR_QUEUE, img_id)
img_status.status = ImageStatusEnum.on_queue.name
db.safe_commit(session)
logger.info('img successfully put on queue')
Expand All @@ -99,7 +97,8 @@ def post(self):

def get(self, img_id):
logger.debug('checking if img has been processed')
session = DatabaseManager().get_session()
db = get_database_manager()
session = db.get_session()
img_status = session.query(ImageStatus).filter(
ImageStatus.img_id == img_id).first()
session.close()
Expand All @@ -111,8 +110,6 @@ def get(self, img_id):

class ImgUpload(Resource):
method_decorators = [auth.login_required]
env_extensions = os.environ['ALLOWED_IMAGE_FILE_EXTENSIONS']
allowed_extensions = env_extensions.lower().split('_')

# pylint: disable=broad-except
def post(self):
Expand All @@ -125,7 +122,7 @@ def post(self):
location='files')
args = parser.parse_args()
img = args['image']
db = DatabaseManager()
db = get_database_manager()
if self._allowed_file(img.filename):
filename = secure_filename(img.filename)
img_id = filename[:filename.find('.')]
Expand All @@ -151,12 +148,12 @@ def post(self):
else:
error_msg = ('Image upload failed: please use one of the '
'following extensions --> {}'
.format(self.allowed_extensions))
.format(ALLOWED_EXTENSIONS))
return error_msg, HTTPStatus.BAD_REQUEST.value

def _allowed_file(self, filename):
return ('.' in filename and
filename.rsplit('.', 1)[1].lower() in self.allowed_extensions)
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS)


class ImgMatchList(Resource):
Expand All @@ -165,7 +162,8 @@ class ImgMatchList(Resource):
# pylint: disable=assignment-from-no-return
def get(self, img_id):
logger.debug('getting img match list')
session = DatabaseManager().get_session()
db = get_database_manager()
session = db.get_session()
query = session.query(Match).filter(Match.this_img_id == img_id)
imgs = []
distances = []
Expand All @@ -182,7 +180,8 @@ class ImgList(Resource):

def get(self):
logger.debug('getting img list')
session = DatabaseManager().get_session()
db = get_database_manager()
session = db.get_session()
query = session.query(Image).all()
imgs = [f.img_id for f in query]
session.close()
Expand Down
5 changes: 3 additions & 2 deletions app/faceanalysis/auth.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from flask_httpauth import HTTPBasicAuth
from flask import g
from .models.models import User
from .models.models import DatabaseManager
from .models.database_manager import get_database_manager

auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(username_or_token, password):
user = User.verify_auth_token(username_or_token)
session = DatabaseManager().get_session()
db = get_database_manager()
session = db.get_session()
if not user:
user = session.query(User).filter(
User.username == username_or_token).first()
Expand Down
39 changes: 39 additions & 0 deletions app/faceanalysis/face_vectorizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
import json
import docker
from .log import get_logger
from .settings import MOUNTED_DATA_DIR
from .settings import HOST_DATA_DIR

logger = get_logger(__name__)


def _format_mount_path(img_path):
return '/{}'.format(os.path.basename(img_path))


def _format_host_path(img_path):
# volume mounts must be absolute
if not img_path.startswith('/'):
img_path = os.path.abspath(img_path)

# adjust the path if it itself is a mount and if we're spawning a
# sibling container
if MOUNTED_DATA_DIR and HOST_DATA_DIR:
img_path = img_path.replace(MOUNTED_DATA_DIR, HOST_DATA_DIR)

return img_path


def get_face_vectors(img_path, algorithm):
img_mount = _format_mount_path(img_path)
img_host = _format_host_path(img_path)
volumes = {img_host: {'bind': img_mount, 'mode': 'ro'}}

logger.debug('Running container %s with image %s', algorithm, img_host)
client = docker.from_env()
stdout = client.containers.run(algorithm, img_mount,
volumes=volumes, auto_remove=True)

face_vectors = json.loads(stdout.decode('ascii')).get('faceVectors', [])
return face_vectors
3 changes: 2 additions & 1 deletion app/faceanalysis/log.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import sys
import logging
from .settings import LOGGING_LEVEL


def get_logger(module_name, logging_level):
def get_logger(module_name, logging_level=LOGGING_LEVEL):
logging_levels = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
Expand Down
Loading