├── .gitignore ├── README.md ├── app ├── Dockerfile ├── __init__.py ├── base_app.py ├── endpoints.py ├── entry_point.py ├── requirements.txt ├── service.py └── weights │ ├── onet.npy │ ├── pnet.npy │ └── rnet.npy ├── docker-compose.yml ├── nginx ├── Dockerfile └── nginx.conf └── sample-images └── movie-stars.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *__pycache__* 3 | *.pyc 4 | dist 5 | *egg 6 | *.egg-info 7 | *.ipynb_checkpoints/* 8 | .ipynb_checkpoints 9 | .DS_Store 10 | /data/blood-cells.zip 11 | /data/data 12 | /dataset 13 | /dataset-master 14 | /dataset2-master 15 | /weights 16 | mlruns 17 | *.zip 18 | *.tar 19 | 20 | # Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,jupyternotebooks 21 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,jupyternotebooks 22 | 23 | ### JupyterNotebooks ### 24 | # gitignore template for Jupyter Notebooks 25 | # website: http://jupyter.org/ 26 | 27 | */.ipynb_checkpoints/* 28 | 29 | # IPython 30 | profile_default/ 31 | ipython_config.py 32 | 33 | # Remove previous ipynb_checkpoints 34 | # git rm -r .ipynb_checkpoints/ 35 | 36 | ### PyCharm ### 37 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 38 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 39 | 40 | # User-specific stuff 41 | .idea/**/workspace.xml 42 | .idea/**/tasks.xml 43 | .idea/**/usage.statistics.xml 44 | .idea/**/dictionaries 45 | .idea/**/shelf 46 | 47 | # AWS User-specific 48 | .idea/**/aws.xml 49 | 50 | # Generated files 51 | .idea/**/contentModel.xml 52 | 53 | # Sensitive or high-churn files 54 | .idea/**/dataSources/ 55 | .idea/**/dataSources.ids 56 | .idea/**/dataSources.local.xml 57 | .idea/**/sqlDataSources.xml 58 | .idea/**/dynamic.xml 59 | .idea/**/uiDesigner.xml 60 | .idea/**/dbnavigator.xml 61 | 62 | # Gradle 63 | .idea/**/gradle.xml 64 | .idea/**/libraries 65 | 66 | # Gradle and Maven with auto-import 67 | # When using Gradle or Maven with auto-import, you should exclude module files, 68 | # since they will be recreated, and may cause churn. Uncomment if using 69 | # auto-import. 70 | # .idea/artifacts 71 | # .idea/compiler.xml 72 | # .idea/jarRepositories.xml 73 | # .idea/modules.xml 74 | # .idea/*.iml 75 | # .idea/modules 76 | # *.iml 77 | # *.ipr 78 | 79 | # CMake 80 | cmake-build-*/ 81 | 82 | # Mongo Explorer plugin 83 | .idea/**/mongoSettings.xml 84 | 85 | # File-based project format 86 | *.iws 87 | 88 | # IntelliJ 89 | out/ 90 | 91 | # mpeltonen/sbt-idea plugin 92 | .idea_modules/ 93 | 94 | # JIRA plugin 95 | atlassian-ide-plugin.xml 96 | 97 | # Cursive Clojure plugin 98 | .idea/replstate.xml 99 | 100 | # Crashlytics plugin (for Android Studio and IntelliJ) 101 | com_crashlytics_export_strings.xml 102 | crashlytics.properties 103 | crashlytics-build.properties 104 | fabric.properties 105 | 106 | # Editor-based Rest Client 107 | .idea/httpRequests 108 | 109 | # Android studio 3.1+ serialized cache file 110 | .idea/caches/build_file_checksums.ser 111 | 112 | ### PyCharm Patch ### 113 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 114 | 115 | # *.iml 116 | # modules.xml 117 | # .idea/misc.xml 118 | # *.ipr 119 | 120 | # Sonarlint plugin 121 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 122 | .idea/**/sonarlint/ 123 | 124 | # SonarQube Plugin 125 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 126 | .idea/**/sonarIssues.xml 127 | 128 | # Markdown Navigator plugin 129 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 130 | .idea/**/markdown-navigator.xml 131 | .idea/**/markdown-navigator-enh.xml 132 | .idea/**/markdown-navigator/ 133 | 134 | # Cache file creation bug 135 | # See https://youtrack.jetbrains.com/issue/JBR-2257 136 | .idea/$CACHE_FILE$ 137 | 138 | # CodeStream plugin 139 | # https://plugins.jetbrains.com/plugin/12206-codestream 140 | .idea/codestream.xml 141 | 142 | ### Python ### 143 | # Byte-compiled / optimized / DLL files 144 | __pycache__/ 145 | *.py[cod] 146 | *$py.class 147 | 148 | # C extensions 149 | *.so 150 | 151 | # Distribution / packaging 152 | .Python 153 | build/ 154 | develop-eggs/ 155 | dist/ 156 | downloads/ 157 | eggs/ 158 | .eggs/ 159 | lib/ 160 | lib64/ 161 | parts/ 162 | sdist/ 163 | var/ 164 | wheels/ 165 | share/python-wheels/ 166 | *.egg-info/ 167 | .installed.cfg 168 | *.egg 169 | MANIFEST 170 | 171 | # PyInstaller 172 | # Usually these files are written by a python script from a template 173 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 174 | *.manifest 175 | *.spec 176 | 177 | # Installer logs 178 | pip-log.txt 179 | pip-delete-this-directory.txt 180 | 181 | # Unit test / coverage reports 182 | htmlcov/ 183 | .tox/ 184 | .nox/ 185 | .coverage 186 | .coverage.* 187 | .cache 188 | nosetests.xml 189 | coverage.xml 190 | *.cover 191 | *.py,cover 192 | .hypothesis/ 193 | .pytest_cache/ 194 | cover/ 195 | 196 | # Translations 197 | *.mo 198 | *.pot 199 | 200 | # Django stuff: 201 | *.log 202 | local_settings.py 203 | db.sqlite3 204 | db.sqlite3-journal 205 | 206 | # Flask stuff: 207 | instance/ 208 | .webassets-cache 209 | 210 | # Scrapy stuff: 211 | .scrapy 212 | 213 | # Sphinx documentation 214 | docs/_build/ 215 | 216 | # PyBuilder 217 | .pybuilder/ 218 | target/ 219 | 220 | # Jupyter Notebook 221 | 222 | # IPython 223 | 224 | # pyenv 225 | # For a library or package, you might want to ignore these files since the code is 226 | # intended to run in multiple environments; otherwise, check them in: 227 | # .python-version 228 | 229 | # pipenv 230 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 231 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 232 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 233 | # install all needed dependencies. 234 | #Pipfile.lock 235 | 236 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 237 | __pypackages__/ 238 | 239 | # Celery stuff 240 | celerybeat-schedule 241 | celerybeat.pid 242 | 243 | # SageMath parsed files 244 | *.sage.py 245 | 246 | # Environments 247 | .env 248 | .venv 249 | env/ 250 | venv/ 251 | ENV/ 252 | env.bak/ 253 | venv.bak/ 254 | 255 | # Spyder project settings 256 | .spyderproject 257 | .spyproject 258 | 259 | # Rope project settings 260 | .ropeproject 261 | 262 | # mkdocs documentation 263 | /site 264 | 265 | # mypy 266 | .mypy_cache/ 267 | .dmypy.json 268 | dmypy.json 269 | 270 | # Pyre type checker 271 | .pyre/ 272 | 273 | # pytype static type analyzer 274 | .pytype/ 275 | 276 | # Cython debug symbols 277 | cython_debug/ 278 | 279 | # End of https://www.toptal.com/developers/gitignore/api/python,pycharm,jupyternotebooks 280 | 281 | 282 | node_modules 283 | /.bazelrc.user 284 | /.tf_configure.bazelrc 285 | /bazel-* 286 | /bazel_pip 287 | /tools/python_bin_path.sh 288 | /tensorflow/tools/git/gen 289 | /pip_test 290 | /_python_build 291 | __pycache__ 292 | *.swp 293 | .vscode/ 294 | cmake_build/ 295 | tensorflow/contrib/cmake/_build/ 296 | .idea/** 297 | /build/ 298 | [Bb]uild/ 299 | /tensorflow/core/util/version_info.cc 300 | /tensorflow/python/framework/fast_tensor_util.cpp 301 | /tensorflow/lite/gen/** 302 | /tensorflow/lite/tools/make/downloads/** 303 | /tensorflow/lite/tools/make/gen/** 304 | /api_init_files_list.txt 305 | /estimator_api_init_files_list.txt 306 | *.whl 307 | 308 | # Android 309 | .gradle 310 | *.iml 311 | local.properties 312 | gradleBuild 313 | 314 | # iOS 315 | *.pbxproj 316 | *.xcworkspace 317 | /*.podspec 318 | /tensorflow/lite/**/coreml/**/BUILD 319 | /tensorflow/lite/**/ios/BUILD 320 | /tensorflow/lite/**/objc/BUILD 321 | /tensorflow/lite/**/swift/BUILD 322 | /tensorflow/lite/examples/ios/simple/data/*.tflite 323 | /tensorflow/lite/examples/ios/simple/data/*.txt 324 | Podfile.lock 325 | Pods 326 | xcuserdata 327 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Face-Detection-flask-gunicorn-nginx-docker 2 | 3 | This is a simple implementation of dockerized face-detection restful-API implemented with flask, Nginx, and scaled up with Gunicorn. This web service takes in an image and returns face-box coordinates. 4 | 5 | # Notes 6 | 7 | 1. For face-detection, I used pytorch version of mtcnn from deep_utils library. For more information check 8 | out [deep_utils](https://github.com/pooya-mohammadi/deep_utils). 9 | 2. The service is scaled up using gunicorn. The gunicorn is a simple library with high throughput for scaling python services. 10 | 1. To increase the number workers, increase number of `workers` in the `docker-compose.yml` file. 11 | 2. For more information about gunicorn workers and threads check the following stackoverflow question 12 | 3. [gunicorn-workers-and-threads](https://stackoverflow.com/questions/38425620/gunicorn-workers-and-threads) 13 | 3. nginx is used as a reverse proxy 14 | 15 | # Setup 16 | 17 | 1. The face-detection name in docker-compose can be changed to any of the models available by deep-utils library. 18 | 2. For simplicity, I placed the weights of the mtcnn-torch model in app/weights. 19 | 3. To use different face-detection models in deep_utils, apply the following changes: 20 | 1. Change the value of `FACE_DETECTION_MODEL` in the `docker-compose.yml` file. 21 | 2. Modify configs of a new model in `app/base_app.py` file. 22 | 3. It's recommended to run the new model in your local system and acquire the downloaded weights from `~/.deep_utils` 23 | directory and place it inside `app/weights` directory. This will save you tons of time while working with models with 24 | heavy weights. 25 | 4. If your new model is based on `tensorflow`, comment the `pytorch` installation section in `app/Dockerfile` and 26 | uncomment the `tensorflow` installation lines. 27 | 28 | # RUN 29 | 30 | To run the API, install `docker` and `docker-compose`, execute the following command: 31 | 32 | ## windows 33 | 34 | `docker-compose up --build` 35 | 36 | ## Linux 37 | 38 | `sudo docker-compose up --build` 39 | 40 | # Inference 41 | 42 | To send an image and get back the boxes run the following commands: 43 | `curl --request POST ip:port/endpoint -F image=@img-add` 44 | 45 | If you run the service on your local system the following request shall work perfectly: 46 | 47 | ```terminal 48 | curl --request POST http://127.0.0.1:8000/face -F image=@./sample-images/movie-stars.jpg 49 | ``` 50 | 51 | The output will be as follows: 52 | ```text 53 | { 54 | "face_1":[269,505,571,726], 55 | "face_10":[73,719,186,809], 56 | "face_11":[52,829,172,931], 57 | "face_2":[57,460,187,550], 58 | "face_3":[69,15,291,186], 59 | "face_4":[49,181,185,279], 60 | "face_5":[53,318,205,424], 61 | "face_6":[18,597,144,716], 62 | "face_7":[251,294,474,444], 63 | "face_8":[217,177,403,315], 64 | "face_9":[175,765,373,917] 65 | } 66 | ``` 67 | 68 | # Issues 69 | 70 | If you find something missing, please open an issue or kindly create a pull request. 71 | 72 | # References 73 | 74 | 1.https://github.com/pooya-mohammadi/deep_utils 75 | 76 | # Licence 77 | 78 | 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: http://www.apache.org/licenses/LICENSE-2.0. 79 | 80 | 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. 81 | 82 | See the License for the specific language governing permissions and limitations under the License. 83 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.7-buster 2 | 3 | 4 | LABEL author="Pooya Mohammadi Kazaj " 5 | 6 | # os and opencv libraries 7 | RUN apt-get update -y \ 8 | && apt install libgl1-mesa-glx -y \ 9 | && apt-get install 'ffmpeg' 'libsm6' 'libxext6' -y \ 10 | && python -m pip install --no-cache-dir --upgrade pip 11 | 12 | # fixed python libraries for pytorch models 13 | RUN pip install --no-cache-dir torch==1.10.0+cpu torchvision==0.11.1+cpu \ 14 | -f https://download.pytorch.org/whl/torch_stable.html \ 15 | && pip install --no-cache-dir numpy==1.21.4 \ 16 | && pip install --no-cache-dir opencv-python==4.5.4.58 \ 17 | && pip install --no-cache-dir deep_utils==0.8.8 \ 18 | && pip install --no-cache-dir scikit-learn==1.0.1 \ 19 | && pip install --no-cache-dir matplotlib==3.4.3 \ 20 | && pip install --no-cache-dir pandas==1.3.4 \ 21 | && rm -rf /root/.cache/pip 22 | 23 | # fixed python libraries for tensorflow models 24 | # RUN pip install --no-cache-dir tensorflow==2.7.0 \ 25 | # && pip install --no-cache-dir numpy==1.21.4 \ 26 | # && pip install --no-cache-dir opencv-python==4.5.4.60 \ 27 | # $$ pip install --no-cache-dir deep_utils==0.8.8 \ 28 | # && rm -rf /root/.cache/pip 29 | 30 | # Add new python libraries here or to requirements.txt 31 | # RUN pip install --no-cache-dir 32 | 33 | COPY . /app 34 | WORKDIR /app 35 | 36 | RUN pip install --no-cache-dir -r requirements.txt 37 | 38 | CMD gunicorn --workers=2 -b 0.0.0.0:660 entry_point:app --worker-class sync 39 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pooya-mohammadi/face-detection-flask-nginx-gunicorn-docker/a858febc8b52242b323cb72d1d6eb2b5f9fc0a3c/app/__init__.py -------------------------------------------------------------------------------- /app/base_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from werkzeug.datastructures import FileStorage 3 | from service import Inference 4 | from flask import Flask 5 | from flask_restful import Api, reqparse 6 | 7 | # define the app and the api variables 8 | ENDPOINT = os.getenv('ENDPOINT', '/face') 9 | # HOST = "0.0.0.0" 10 | app = Flask(ENDPOINT) 11 | api = Api(app) 12 | 13 | PORT_NUMBER = int(os.getenv('PORT_NUMBER', 8080)) 14 | 15 | # get debugging mode condition, default is True: 16 | debugging = os.getenv("DEBUGGING", 'True').lower() in ('true', '1', 't') 17 | print(f"[INFO] debugging mode is set to: {debugging}") 18 | # load the model and weights 19 | FACE_DETECTION_MODEL = os.getenv('FACE_DETECTION_MODEL', 'MTCNNTorchFaceDetector') 20 | 21 | # The addresses for weights go here 22 | if FACE_DETECTION_MODEL == "MTCNNTorchFaceDetector": 23 | rnet = '/app/weights/rnet.npy' 24 | onet = '/app/weights/onet.npy' 25 | pnet = '/app/weights/pnet.npy' 26 | model_configs = dict(rnet=rnet, onet=onet, pnet=pnet) 27 | print(f"[INFO] Face detection mode is set to {FACE_DETECTION_MODEL}") 28 | else: 29 | # The configs of models other than mtcnn go here 30 | model_configs = dict() 31 | print( 32 | f"[INFO] the configs for model:{FACE_DETECTION_MODEL} is set to {model_configs}." 33 | f" If it's empty, the deep_utils library will use the defaults configs and most surely will download " 34 | f"the weights each time you run the dockerfile ") 35 | 36 | inference = Inference(FACE_DETECTION_MODEL, **model_configs) 37 | POST_TYPE = os.getenv("POST_TYPE", "FORM") 38 | 39 | # set global variables 40 | app.config['inference'] = inference 41 | app.config['POST_TYPE'] = POST_TYPE 42 | 43 | # file Parser arguments. Only Form is implemented 44 | app.config['PARSER'] = reqparse.RequestParser() 45 | app.config['PARSER'].add_argument('image', 46 | type=FileStorage, 47 | location='files', 48 | required=True, 49 | help='provide an image file') 50 | -------------------------------------------------------------------------------- /app/endpoints.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from flask import jsonify 3 | from flask_restful import Resource 4 | from base_app import app 5 | from deep_utils import b64_to_img 6 | import cv2 7 | import numpy as np 8 | 9 | 10 | class FaceDetection(Resource): 11 | @staticmethod 12 | def post(): 13 | args = app.config['PARSER'].parse_args() 14 | contents = args['image'] 15 | if app.config['POST_TYPE'] == 'JSON': 16 | image = b64_to_img(contents) 17 | elif app.config['POST_TYPE'] == 'FORM': 18 | image = np.array(bytearray(contents.read()), dtype=np.uint8) 19 | image = cv2.imdecode(image, cv2.IMREAD_COLOR) 20 | else: 21 | print(f"[ERROR] POST_TYPE:{app.config['POST_TYPE']} is not valid!, exiting ...") 22 | sys.exit(1) 23 | res = app.config['inference'].infer(image) 24 | return res 25 | 26 | @staticmethod 27 | def get(): 28 | """ 29 | Bug test 30 | :return: some text 31 | """ 32 | return jsonify({"Just": "Fine!"}) 33 | -------------------------------------------------------------------------------- /app/entry_point.py: -------------------------------------------------------------------------------- 1 | from endpoints import FaceDetection 2 | from base_app import app, api, ENDPOINT 3 | 4 | api.add_resource(FaceDetection, ENDPOINT) 5 | 6 | if __name__ == '__main__': 7 | app.run("127.0.0.1", port=3000) 8 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.0.2 2 | gunicorn==20.1.0 3 | pillow==8.4.0 4 | pyyaml==6.0 5 | scipy==1.7.2 6 | flask_restful==0.3.9 7 | scikit-learn==1.0.1 -------------------------------------------------------------------------------- /app/service.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from deep_utils import face_detector_loader, Box, img_to_b64 3 | from flask import jsonify 4 | 5 | 6 | class Inference: 7 | def __init__(self, model_name, **model_config): 8 | self.detector = face_detector_loader(model_name, **model_config) 9 | 10 | @staticmethod 11 | def preprocessing(img) -> np.ndarray: 12 | if type(img) is not np.ndarray: 13 | img = np.array(img).astype(np.uint8) 14 | return img 15 | 16 | def infer(self, img): 17 | img = self.preprocessing(img) 18 | objects = self.detector.detect_faces(img, is_rgb=False) 19 | faces = dict() 20 | boxes = objects['boxes'] 21 | if boxes and len(boxes[0]): 22 | images = Box.get_box_img(img, boxes) 23 | faces = {f"face_{i}": [int(b) for b in box] for i, box in enumerate(boxes, 1)} 24 | return jsonify(faces) 25 | -------------------------------------------------------------------------------- /app/weights/onet.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pooya-mohammadi/face-detection-flask-nginx-gunicorn-docker/a858febc8b52242b323cb72d1d6eb2b5f9fc0a3c/app/weights/onet.npy -------------------------------------------------------------------------------- /app/weights/pnet.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pooya-mohammadi/face-detection-flask-nginx-gunicorn-docker/a858febc8b52242b323cb72d1d6eb2b5f9fc0a3c/app/weights/pnet.npy -------------------------------------------------------------------------------- /app/weights/rnet.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pooya-mohammadi/face-detection-flask-nginx-gunicorn-docker/a858febc8b52242b323cb72d1d6eb2b5f9fc0a3c/app/weights/rnet.npy -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | face: 4 | build: ./app 5 | container_name: face 6 | restart: always 7 | expose: 8 | - 660 9 | environment: 10 | - ENDPOINT=/face 11 | - FACE_DETECTION_MODEL=MTCNNTorchFaceDetector 12 | command: gunicorn --workers=2 --threads 1 -b 0.0.0.0:660 entry_point:app --worker-class sync 13 | 14 | nginx: 15 | build: ./nginx 16 | container_name: nginx 17 | restart: always 18 | ports: 19 | - 8000:80 20 | depends_on: 21 | - face -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.3 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | 5 | COPY nginx.conf /etc/nginx/conf.d -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | charset utf-8; 4 | server_name 0.0.0.0; 5 | 6 | location / { 7 | client_max_body_size 20M; 8 | proxy_pass http://face:660; 9 | proxy_set_header Host $host; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /sample-images/movie-stars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pooya-mohammadi/face-detection-flask-nginx-gunicorn-docker/a858febc8b52242b323cb72d1d6eb2b5f9fc0a3c/sample-images/movie-stars.jpg --------------------------------------------------------------------------------