├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── doc ├── after_model_update.png ├── after_update_info.png ├── detection_ex1.png ├── detection_mob.png └── examples_capture.png ├── docker ├── Dockerfile ├── Dockerfile.gpu ├── download.py ├── download_vggace2.py └── requirements.txt ├── notebooks └── experiments_with_classification.ipynb ├── server ├── server.py ├── server.sh └── static │ ├── detect.html │ ├── detect.js │ └── local.js ├── tensorface ├── __init__.py ├── classifier.py ├── const.py ├── detection.py ├── embedding.py ├── model.py ├── mtcnn.py ├── recognition.py └── recognition_sklearn.py └── test ├── __init__.py ├── test_embedding.py ├── test_examples ├── faces │ ├── Bartek.png │ ├── CoverGirl.png │ ├── StudBoy.png │ ├── StudGirl.png │ ├── unknown.png │ └── unknown_1.png ├── test_1523794239.922389.json └── test_1523794239.922389.png └── train_examples ├── train_Bartek_160_10.png ├── train_CoverGirl_160_10.png ├── train_StudBoy_160_10.png └── train_StudGirl_160_10.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | conda_venv/ 3 | 4 | # Rope project settings 5 | .ropeproject 6 | 7 | ### Vim ### 8 | # swap 9 | .sw[a-p] 10 | .*.sw[a-p] 11 | # session 12 | Session.vim 13 | # temporary 14 | .netrwhist 15 | *~ 16 | # auto-generated tag files 17 | tags -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NS := btwardow 2 | REPO := tf-face-recognition 3 | #VERSION := $(shell git describe) 4 | VERSION := 1.0.0 5 | 6 | .PHONY: build push 7 | 8 | build: 9 | docker build -t $(NS)/$(REPO):$(VERSION) -f docker/Dockerfile . 10 | 11 | push: build 12 | docker push $(NS)/$(REPO):$(VERSION) 13 | 14 | default: build 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Real-Time Face Recognition and Detection using Tensorflow 2 | ================================================================= 3 | 4 | The idea is to build application for a real-time face detection and 5 | recognition using Tensorflow and a notebook's webcam. The model for 6 | face prediction should be easy to update online to add new targets. 7 | 8 | ## Project assumptions 9 | - Tensorflow 1.7 and python 3 10 | - **Everything should be dockerized and easy to reproduce!** 11 | 12 | ## How to run it? 13 | 14 | ### Run it right away from the pre-build image 15 | 16 | Just type: 17 | 18 | ```bash 19 | docker run -it --rm -p 5000:5000 btwardow/tf-face-recognition:1.0.0 20 | ``` 21 | 22 | Then got to [https://localhost:5000/](https://localhost:5000/) or type 23 | it in your browser to get face detection (without recognition for now). 24 | 25 | _Note: HTTPS is required from many modern browsers to transfer video outside the localhost, 26 | without making any unsafe settings to your browser._ 27 | 28 | 29 | ### Building docker 30 | 31 | Type this in the root project's directory in order to: 32 | 33 | #### Create docker image 34 | 35 | Use main target from Makefile of main directory: 36 | 37 | ```bash 38 | make 39 | ``` 40 | 41 | #### Run project 42 | 43 | ```bash 44 | docker run --rm -it -p 5000:5000 -v /$(pwd):/workspace btwardow/tf-face-recognition:dev 45 | ``` 46 | 47 | This volume mapping is very convenient for the development and testing purposes. 48 | 49 | To use GPU power - there is dedicated [Dockerfile.gpu](./docker/Dockerfile.gpu). 50 | 51 | ### Run it without docker (development) 52 | 53 | Running application without docker is useful for development. Below is quick how to for *nix environments. 54 | 55 | Creating virtual env (with Conda) and installing requirements: 56 | 57 | ```bash 58 | conda create -y -n face_recognition_36 python=3.6 59 | source activate face_recognition_36 60 | pip install -r requirements_dev.txt 61 | ``` 62 | 63 | Downloading pre-build models: 64 | ```bash 65 | mkdir ~/pretrained_models 66 | cp docker/download*.py ~/pretrained_models 67 | cd ~/pretrained_models 68 | python download.py 69 | python download_vggace2.py 70 | ``` 71 | 72 | the `~/pretrained_models` directory should look like that: 73 | 74 | ```bash 75 | (face_recognition_36) b.twardowski@172-16-170-27:~/pretrained_models » tree 76 | . 77 | ├── 20180402-114759 78 | │   ├── 20180402-114759.pb 79 | │   ├── model-20180402-114759.ckpt-275.data-00000-of-00001 80 | │   ├── model-20180402-114759.ckpt-275.index 81 | │   └── model-20180402-114759.meta 82 | ├── 20180402-114759.zip 83 | ├── det1.npy 84 | ├── det2.npy 85 | ├── det3.npy 86 | ├── download.py 87 | └── download_vggace2.py 88 | ``` 89 | 90 | Then, to start a server, go to `./server` directory and type: 91 | ```bash 92 | PYTHONPATH=".." python server.py 93 | ``` 94 | 95 | 96 | ## Why making a web application for this? 97 | 98 | _Everything should be dockerized and easy to reproduce_. This makes things 99 | interesting even for a toy project from the computer vision area. Why? 100 | 101 | - building model/playing around in Jupyter/Python - that's easy... inference 102 | - on data grabbed from the host box camera inside docker - that's tricky! 103 | 104 | Why is hard to grab data from camera device from docker? You can read 105 | [here](https://apple.stackexchange.com/questions/265281/using-webcam-connected-to-macbook-inside-a-docker-container). 106 | The main reason - docker is not build for such things, so it's not making life 107 | easier for here. Of course few possibilities are mentioned, like streaming from 108 | the host MBP using `ffmpeg` or preparing custom Virtualbox 109 | `boot2docker.iso` image and making the MBP [webcam pass 110 | through](https://www.virtualbox.org/manual/ch09.html#webcam-passthrough). But 111 | all of them dosn't sound right. All requires additiona effort of installing sth 112 | from `brew` or Virualbox configuration (assuming you have docker installed on 113 | your OSX). 114 | 115 | The good side of having this as a webapp is fact that **you can try it out on your mobile phone!** 116 | What is very convenient for testing and demos. 117 | 118 | ![](./doc/detection_mob.png) 119 | 120 | 121 | ## Face detection 122 | 123 | Face detection is done to find faces from the video and mark it boundaries. These are 124 | areas that can be future use for the face recognition task. To detect faces the 125 | pre-trained MTCNN network is being used. 126 | 127 | ![](./doc/detection_ex1.png) 128 | 129 | ## Face recognition 130 | 131 | The face detection is using embedding from the VGGFace2 network + KNN model implemented in Tensorflow. 132 | 133 | In order to get your face recognized first a few examples have to be provided to our algorithm (now - at least 10). 134 | 135 | When you see the application working and correctly detecting faces just click the _**Capture Examples**_ button. 136 | 137 | **While capturing examples for the face detection there have to be single face in video!** 138 | 139 | ![](./doc/examples_capture.png) 140 | 141 | After 10 examples are collected, we can type the name of the person and upload them to server. 142 | 143 | As a result we see the current status of classification examples: 144 | 145 | ![](./doc/after_update_info.png) 146 | 147 | ![](./doc/after_model_update.png) 148 | 149 | And from now on, the new person is recognized. For this example it's _CoverGirl_. 150 | 151 | ### One more example 152 | 153 | ![](./doc/face_recog_ex1.png) 154 | 155 | 156 | ### Running Jupyter Notebook and reproducing analysis 157 | 158 | If you are interested about the classification, please check out this [notebook](./notebooks/experiments_with_classification.ipynb) which will explain in details how it works (e.g. threshold for the recognition). 159 | 160 | You can run jupyter notebook from the docker, just type: 161 | 162 | ```bash 163 | docker run --rm -it -p 8888:8888 btwardow/tf-face-recognition:1.0.0 /run_jupyter.sh --allow-root 164 | ``` 165 | 166 | 167 | ## TODOs 168 | 169 | - [x] face detection with a pre-trained MTCNN network 170 | - [x] training face recognition classifier (use pre-trained embedding + classifier) based on provided examples 171 | - [x] model updates directly from the browser 172 | - [ ] save & clear classification model from the browser 173 | - [ ] check if detection can be done faster, if so re-implement it (optimize MTCNN for inference?) 174 | - [ ] try out port it to Trensorflow.js (as skeptical as I am of crunching numbers in JavaScript...) 175 | 176 | 177 | ## Thanks 178 | 179 | Many thanks to creators of `facenet` project, which provides pre trained models for VGGFace2. Great job! 180 | -------------------------------------------------------------------------------- /doc/after_model_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/doc/after_model_update.png -------------------------------------------------------------------------------- /doc/after_update_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/doc/after_update_info.png -------------------------------------------------------------------------------- /doc/detection_ex1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/doc/detection_ex1.png -------------------------------------------------------------------------------- /doc/detection_mob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/doc/detection_mob.png -------------------------------------------------------------------------------- /doc/examples_capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/doc/examples_capture.png -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tensorflow/tensorflow:1.7.0-py3 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y git libfontconfig1 libxrender1 libsm6 libxext6 apt-utils 5 | RUN apt-get clean 6 | 7 | RUN pip --version 8 | RUN pip install --upgrade pip 9 | 10 | COPY docker/requirements.txt /server-requirements.txt 11 | RUN pip install -r /server-requirements.txt 12 | 13 | RUN mkdir /root/pretrained_models 14 | COPY docker/download.py /root/pretrained_models/ 15 | COPY docker/download_vggace2.py /root/pretrained_models/ 16 | WORKDIR /root/pretrained_models 17 | RUN python download.py 18 | RUN python download_vggace2.py 19 | RUN ls -l 20 | 21 | 22 | COPY . /workspace 23 | 24 | WORKDIR /workspace/ 25 | CMD cd server && ./server.sh -------------------------------------------------------------------------------- /docker/Dockerfile.gpu: -------------------------------------------------------------------------------- 1 | FROM tensorflow/tensorflow:1.7.0-gpu-py3 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y git libfontconfig1 libxrender1 libsm6 libxext6 apt-utils 5 | RUN apt-get clean 6 | 7 | RUN pip --version 8 | RUN pip install --upgrade pip 9 | 10 | COPY requirements.txt /server-requirements.txt 11 | RUN pip install -r /server-requirements.txt 12 | 13 | RUN mkdir /root/pretrained_models 14 | COPY download.py /root/pretrained_models/ 15 | COPY download_vggace2.py /root/pretrained_models/ 16 | WORKDIR /root/pretrained_models 17 | RUN python download.py 18 | RUN python download_vggace2.py 19 | RUN ls -l 20 | 21 | WORKDIR /workspace/ 22 | CMD ./server/server.sh 23 | -------------------------------------------------------------------------------- /docker/download.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | if __name__ == '__main__': 5 | 6 | print("Downloading pretrained model for MTCNN...") 7 | 8 | for i in range(1, 4): 9 | f_name = 'det{}.npy'.format(i) 10 | print("Downloading: ", f_name) 11 | url = "https://github.com/davidsandberg/facenet/raw/" \ 12 | "e9d4e8eca95829e5607236fa30a0556b40813f62/src/align/det{}.npy".format(i) 13 | session = requests.Session() 14 | response = session.get(url, stream=True) 15 | 16 | CHUNK_SIZE = 32768 17 | 18 | with open(f_name, "wb") as f: 19 | for chunk in response.iter_content(CHUNK_SIZE): 20 | if chunk: # filter out keep-alive new chunks 21 | f.write(chunk) 22 | -------------------------------------------------------------------------------- /docker/download_vggace2.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import zipfile 3 | import os 4 | 5 | model_dict = { 6 | 'lfw-subset': '1B5BQUZuJO-paxdN8UclxeHAR1WnR_Tzi', 7 | '20170131-234652': '0B5MzpY9kBtDVSGM0RmVET2EwVEk', 8 | '20170216-091149': '0B5MzpY9kBtDVTGZjcWkzT3pldDA', 9 | '20170512-110547': '0B5MzpY9kBtDVZ2RpVDYwWmxoSUk', 10 | '20180402-114759': '1EXPBSXwTaqrSC0OhUdXNmKSh9qJUQ55-' 11 | } 12 | 13 | 14 | def download_and_extract_file(model_name, data_dir): 15 | file_id = model_dict[model_name] 16 | destination = os.path.join(data_dir, model_name + '.zip') 17 | if not os.path.exists(destination): 18 | print('Downloading file to %s' % destination) 19 | download_file_from_google_drive(file_id, destination) 20 | with zipfile.ZipFile(destination, 'r') as zip_ref: 21 | print('Extracting file to %s' % data_dir) 22 | zip_ref.extractall(data_dir) 23 | 24 | 25 | def download_file_from_google_drive(file_id, destination): 26 | URL = "https://drive.google.com/uc?export=download" 27 | 28 | session = requests.Session() 29 | 30 | response = session.get(URL, params={'id': file_id}, stream=True) 31 | token = get_confirm_token(response) 32 | 33 | if token: 34 | params = {'id': file_id, 'confirm': token} 35 | response = session.get(URL, params=params, stream=True) 36 | 37 | save_response_content(response, destination) 38 | 39 | 40 | def get_confirm_token(response): 41 | for key, value in response.cookies.items(): 42 | if key.startswith('download_warning'): 43 | return value 44 | 45 | return None 46 | 47 | 48 | def save_response_content(response, destination): 49 | CHUNK_SIZE = 32768 50 | 51 | with open(destination, "wb") as f: 52 | for chunk in response.iter_content(CHUNK_SIZE): 53 | if chunk: # filter out keep-alive new chunks 54 | f.write(chunk) 55 | 56 | 57 | # download VGGFace2 58 | download_and_extract_file('20180402-114759', '.') 59 | 60 | -------------------------------------------------------------------------------- /docker/requirements.txt: -------------------------------------------------------------------------------- 1 | #tensorflow==1.7.0 2 | #tensorflowjs 3 | #tensorflow-hub 4 | opencv-python 5 | matplotlib 6 | sklearn 7 | 8 | flask 9 | Pillow 10 | pyopenssl 11 | requests 12 | 13 | jupyter -------------------------------------------------------------------------------- /server/server.py: -------------------------------------------------------------------------------- 1 | import json 2 | from time import time 3 | 4 | from PIL import Image 5 | from flask import Flask, request, Response 6 | 7 | # assuming that script is run from `server` dir 8 | import sys, os 9 | sys.path.append(os.path.realpath('..')) 10 | 11 | from tensorface import detection 12 | from tensorface.recognition import recognize, learn_from_examples 13 | 14 | # For test examples acquisition 15 | SAVE_DETECT_FILES = False 16 | SAVE_TRAIN_FILES = False 17 | 18 | app = Flask(__name__) 19 | app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 20 | 21 | 22 | # for CORS 23 | @app.after_request 24 | def after_request(response): 25 | response.headers.add('Access-Control-Allow-Origin', '*') 26 | response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization') 27 | response.headers.add('Access-Control-Allow-Methods', 'GET,POST') # Put any other methods you need here 28 | return response 29 | 30 | 31 | @app.route('/') 32 | def index(): 33 | return Response(open('./static/detect.html').read(), mimetype="text/html") 34 | 35 | 36 | @app.route('/detect', methods=['POST']) 37 | def detect(): 38 | try: 39 | image_stream = request.files['image'] # get the image 40 | image = Image.open(image_stream) 41 | 42 | # Set an image confidence threshold value to limit returned data 43 | threshold = request.form.get('threshold') 44 | if threshold is None: 45 | threshold = 0.5 46 | else: 47 | threshold = float(threshold) 48 | 49 | faces = recognize(detection.get_faces(image, threshold)) 50 | 51 | j = json.dumps([f.data() for f in faces]) 52 | print("Result:", j) 53 | 54 | # save files 55 | if SAVE_DETECT_FILES and len(faces): 56 | id = time() 57 | with open('test_{}.json'.format(id), 'w') as f: 58 | f.write(j) 59 | 60 | image.save('test_{}.png'.format(id)) 61 | for i, f in enumerate(faces): 62 | f.img.save('face_{}_{}.png'.format(id, i)) 63 | 64 | return j 65 | 66 | except Exception as e: 67 | import traceback 68 | traceback.print_exc() 69 | print('POST /detect error: %e' % e) 70 | return e 71 | 72 | 73 | @app.route('/train', methods=['POST']) 74 | def train(): 75 | try: 76 | # image with sprites 77 | image_stream = request.files['image'] # get the image 78 | image_sprite = Image.open(image_stream) 79 | 80 | # forms data 81 | name = request.form.get('name') 82 | num = int(request.form.get('num')) 83 | size = int(request.form.get('size')) 84 | 85 | # save for debug purposes 86 | if SAVE_TRAIN_FILES: 87 | image_sprite.save('train_{}_{}_{}.png'.format(name, size, num)) 88 | 89 | info = learn_from_examples(name, image_sprite, num, size) 90 | return json.dumps([{'name': n, 'train_examples': s} for n, s in info.items()]) 91 | 92 | except Exception as e: 93 | import traceback 94 | traceback.print_exc() 95 | print('POST /image error: %e' % e) 96 | return e 97 | 98 | 99 | if __name__ == '__main__': 100 | app.run(debug=False, host='0.0.0.0', ssl_context='adhoc') 101 | # app.run(host='0.0.0.0') 102 | -------------------------------------------------------------------------------- /server/server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PYTHONPATH='/workspace:/workspace/server' 4 | python server.py 5 | -------------------------------------------------------------------------------- /server/static/detect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ral-Time Tensor Flow Face Detection and Recognition 6 | 7 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 | 72 | 73 | 74 |
75 | 76 | 77 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /server/static/detect.js: -------------------------------------------------------------------------------- 1 | // elements 2 | const s = document.getElementById('objDetect'); 3 | const rInfo = document.getElementById('captureInfo'); 4 | const rAlert = document.getElementById('recAlert'); 5 | 6 | // attribs 7 | const sourceVideo = s.getAttribute("data-source"); //the source video to use 8 | const uploadWidth = s.getAttribute("data-uploadWidth") || 640; //the width of the upload file 9 | const mirror = s.getAttribute("data-mirror") || false; //mirror the boundary boxes 10 | const scoreThreshold = s.getAttribute("data-scoreThreshold") || 0.5; 11 | const detectUrl = window.location.origin + '/detect'; 12 | const updateUrl = window.location.origin + '/train'; 13 | 14 | //Video element selector 15 | v = document.getElementById(sourceVideo); 16 | 17 | //for starting events 18 | let isPlaying = false, 19 | gotMetadata = false; 20 | let isCaptureExample = false; 21 | let examplesNum = 0; 22 | let maxExamples = 10; 23 | let exampleSize = 160; 24 | 25 | //Canvas setup 26 | 27 | //create a canvas to grab an image for upload 28 | let imageCanvas = document.createElement('canvas'); 29 | let imageCtx = imageCanvas.getContext("2d"); 30 | 31 | //let exCanvas = document.createElement('canvas'); 32 | let exCanvas = document.getElementById("exCanvas") 33 | let exCtx = exCanvas.getContext("2d"); 34 | //document.getElementById('examplesPic').appendChild(exCtx); 35 | 36 | //create a canvas for drawing object boundaries 37 | let drawCanvas = document.createElement('canvas'); 38 | 39 | var rect = v.getBoundingClientRect(); 40 | console.log("Video box:", rect.top, rect.right, rect.bottom, rect.left); 41 | 42 | document.getElementById('videoDiv').appendChild(drawCanvas); 43 | //document.getElementById('buttons').style.top = rect.bottom + 10; 44 | let drawCtx = drawCanvas.getContext("2d"); 45 | 46 | function uploadScale() { 47 | return v.videoWidth > 0 ? uploadWidth / v.videoWidth : 0; 48 | } 49 | 50 | 51 | //draw boxes and labels on each detected object 52 | function drawBoxes(objects) { 53 | 54 | //clear the previous drawings 55 | drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height); 56 | 57 | //filter out objects that contain a class_name and then draw boxes and labels on each 58 | objects.forEach(face => { 59 | let scale = uploadScale(); 60 | let _x = face.x / scale; 61 | let y = face.y / scale; 62 | let width = face.w / scale; 63 | let height = face.h / scale; 64 | //flip the x axis if local video is mirrored 65 | if (mirror) { 66 | x = drawCanvas.width - (_x + width) 67 | } else { 68 | x = _x 69 | } 70 | 71 | let rand_conf = face.confidence.toFixed(2); 72 | let title = "" + rand_conf + ""; 73 | if (face.name != "unknown") { 74 | drawCtx.strokeStyle = "magenta"; 75 | drawCtx.fillStyle = "magenta"; 76 | title += ' - ' + face.name 77 | if (face.predict_proba > 0.0 ) { 78 | title += "[" + face.predict_proba.toFixed(2) + "]"; 79 | } 80 | } else { 81 | drawCtx.strokeStyle = "cyan"; 82 | drawCtx.fillStyle = "cyan"; 83 | } 84 | drawCtx.fillText(title , x + 5, y - 5); 85 | drawCtx.strokeRect(x, y, width, height); 86 | 87 | if(isCaptureExample && examplesNum < maxExamples) { 88 | console.log("capure example: ", examplesNum) 89 | 90 | //Some styles for the drawcanvas 91 | exCtx.drawImage(imageCanvas, 92 | face.x, face.y, face.w, face.h, 93 | examplesNum * exampleSize, 0, 94 | exampleSize, exampleSize); 95 | 96 | examplesNum += 1; 97 | 98 | if(examplesNum == maxExamples) { 99 | stopCaptureExamples(); 100 | } 101 | } 102 | 103 | }); 104 | } 105 | 106 | //Add file blob to a form and post 107 | function postFile(file) { 108 | 109 | //Set options as form data 110 | let formdata = new FormData(); 111 | formdata.append("image", file); 112 | formdata.append("threshold", scoreThreshold); 113 | 114 | let xhr = new XMLHttpRequest(); 115 | xhr.open('POST', detectUrl, true); 116 | xhr.onload = function () { 117 | if (this.status === 200) { 118 | let objects = JSON.parse(this.response); 119 | 120 | //draw the boxes 121 | drawBoxes(objects); 122 | 123 | //Save and send the next image 124 | imageCtx.drawImage(v, 0, 0, v.videoWidth, v.videoHeight, 0, 0, uploadWidth, uploadWidth * (v.videoHeight / v.videoWidth)); 125 | imageCanvas.toBlob(postFile, 'image/jpeg'); 126 | } 127 | else { 128 | console.error(xhr); 129 | } 130 | }; 131 | xhr.send(formdata); 132 | } 133 | 134 | function postExamplesFile(file) { 135 | //Set options as form data 136 | let formdata = new FormData(); 137 | formdata.append("image", file); 138 | formdata.append("num", examplesNum); 139 | formdata.append("size", exampleSize); 140 | name = document.getElementById('inputName').value; 141 | formdata.append("name", name); 142 | 143 | 144 | let xhr = new XMLHttpRequest(); 145 | xhr.open('POST', updateUrl, true); 146 | xhr.onload = function () { 147 | if (this.status === 200) { 148 | let objects = JSON.parse(this.response); 149 | console.log(objects); 150 | alert( 151 | 'Model updated with person: ' + name + ' \n' + 152 | 'Now model have examples for: \n\n' + 153 | objects.map( i => '' + i.name + ' - train examples: ' + i.train_examples ).join('\n\n') 154 | ) 155 | } 156 | else { 157 | console.error(xhr); 158 | } 159 | }; 160 | xhr.send(formdata); 161 | } 162 | 163 | //Start object detection 164 | function startObjectDetection() { 165 | 166 | console.log("starting object detection"); 167 | 168 | //Set canvas sizes base don input video 169 | drawCanvas.width = v.videoWidth; 170 | drawCanvas.height = v.videoHeight; 171 | 172 | imageCanvas.width = uploadWidth; 173 | imageCanvas.height = uploadWidth * (v.videoHeight / v.videoWidth); 174 | 175 | //Some styles for the drawcanvas 176 | drawCtx.lineWidth = 4; 177 | drawCtx.strokeStyle = "cyan"; 178 | drawCtx.font = "20px Verdana"; 179 | drawCtx.fillStyle = "cyan"; 180 | 181 | //Save and send the first image 182 | imageCtx.drawImage(v, 0, 0, v.videoWidth, v.videoHeight, 0, 0, uploadWidth, uploadWidth * (v.videoHeight / v.videoWidth)); 183 | imageCanvas.toBlob(postFile, 'image/jpeg'); 184 | 185 | } 186 | 187 | //Capture examples for training 188 | function captureExamples() { 189 | console.log("staring capturing sprites...") 190 | rInfo.hidden = false; 191 | rAlert.hidden = false; 192 | isCaptureExample = true; 193 | examplesNum = 0; 194 | exCtx.clearRect(0, 0, exCanvas.width, exCanvas.height); 195 | document.getElementById('updateModel').hidden = true; 196 | 197 | } 198 | 199 | function stopCaptureExamples() { 200 | rAlert.hidden = true; 201 | document.getElementById('updateModel').hidden = false; 202 | } 203 | 204 | function updateModel() { 205 | console.log("updating model...") 206 | //Save and send the next image 207 | exCanvas.toBlob(postExamplesFile, 'image/jpeg'); 208 | } 209 | 210 | // EVENTS 211 | 212 | //check if metadata is ready - we need the video size 213 | v.onloadedmetadata = () => { 214 | console.log("video metadata ready"); 215 | gotMetadata = true; 216 | if (isPlaying) 217 | startObjectDetection(); 218 | }; 219 | 220 | //see if the video has started playing 221 | v.onplaying = () => { 222 | console.log("video playing"); 223 | isPlaying = true; 224 | if (gotMetadata) { 225 | startObjectDetection(); 226 | } 227 | }; 228 | 229 | 230 | window.onload = () => { 231 | document.getElementById("buttonCapture").onclick = () => { 232 | captureExamples(); 233 | }; 234 | document.getElementById("updateModel").onclick = () => { 235 | let n = document.getElementById('inputName') 236 | if (n.value) { 237 | updateModel(); 238 | } else { 239 | alert("Please provide name!"); 240 | } 241 | return false; 242 | }; 243 | 244 | }; 245 | 246 | 247 | -------------------------------------------------------------------------------- /server/static/local.js: -------------------------------------------------------------------------------- 1 | //Get camera video 2 | const constraints = { 3 | audio: false, 4 | video: { 5 | // width: {min: 640, ideal: 1280, max: 1920}, 6 | // height: {min: 480, ideal: 720, max: 1080} 7 | width: {min: 640, ideal: 640, max: 640}, 8 | 9 | height: {min: 480, ideal: 480, max: 480} 10 | // width: {min: 640, ideal: 1280, max: 1920}, 11 | // height: {min: 480, ideal: 720, max: 1080} 12 | } 13 | }; 14 | 15 | navigator.mediaDevices.getUserMedia(constraints) 16 | .then(stream => { 17 | document.getElementById("myVideo").srcObject = stream; 18 | console.log("Got local user video"); 19 | 20 | }) 21 | .catch(err => { 22 | console.log('navigator.getUserMedia error: ', err) 23 | }); 24 | -------------------------------------------------------------------------------- /tensorface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/tensorface/__init__.py -------------------------------------------------------------------------------- /tensorface/classifier.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from typing import List, Tuple 3 | 4 | import tensorflow as tf 5 | import numpy as np 6 | 7 | from tensorface.const import EMBEDDING_SIZE, UNKNOWN_CLASS 8 | 9 | 10 | class KNN: 11 | def __init__(self, K=5, dist_threshold=14): 12 | ''' 13 | Why such dist_threshold value? 14 | See notebook: notebooks/experiments_with_classification.ipynb 15 | :param K: 16 | :param dist_threshold: 17 | ''' 18 | 19 | # current training data 20 | self.X_train = None 21 | self.y_train = None 22 | self.idx_to_lbl = None 23 | self.lbl_to_idx = None 24 | self.y_train_idx = None 25 | 26 | # main params 27 | self.dist_threshold_value = dist_threshold 28 | self.K = K 29 | 30 | # placeholders 31 | self.xtr = tf.placeholder(tf.float32, [None, EMBEDDING_SIZE], name='X_train') 32 | self.ytr = tf.placeholder(tf.float32, [None], name='y_train') 33 | self.xte = tf.placeholder(tf.float32, [EMBEDDING_SIZE], name='x_test') 34 | self.dist_threshold = tf.placeholder(tf.float32, shape=(), name="dist_threshold") 35 | 36 | ############ build model ############ 37 | 38 | # model 39 | distance = tf.reduce_sum(tf.abs(tf.subtract(self.xtr, self.xte)), axis=1) 40 | values, indices = tf.nn.top_k(tf.negative(distance), k=self.K, sorted=False) 41 | nn_dist = tf.negative(values) 42 | self.valid_nn_num = tf.reduce_sum(tf.cast(nn_dist < self.dist_threshold, tf.float32)) 43 | nn = [] 44 | for i in range(self.K): 45 | nn.append(self.ytr[indices[i]]) # taking the result indexes 46 | 47 | # saving list in tensor variable 48 | nearest_neighbors = nn 49 | # this will return the unique neighbors the count will return the most common's index 50 | self.y, idx, self.count = tf.unique_with_counts(nearest_neighbors) 51 | self.pred = tf.slice(self.y, begin=[tf.argmax(self.count, 0)], size=tf.constant([1], dtype=tf.int64))[0] 52 | 53 | def predict(self, X) -> List[Tuple[str, float, List[str], List[float]]]: 54 | if self.X_train is None: 55 | # theres nothing we can do than just mark all faces as unknown... 56 | return [(UNKNOWN_CLASS, None, None, None) for _ in range(X.shape[0])] 57 | 58 | result = [] 59 | if self.X_train is not None and self.X_train.shape[0] > 0: 60 | with tf.Session() as sess: 61 | for i in range(X.shape[0]): 62 | _valid_nn_num, _pred, _lbl_idx, _counts = sess.run( 63 | [self.valid_nn_num, self.pred, self.y, self.count], 64 | feed_dict={ 65 | self.xtr: self.X_train, 66 | self.ytr: self.y_train_idx, 67 | self.xte: X[i, :], 68 | self.dist_threshold: self.dist_threshold_value}) 69 | 70 | if _valid_nn_num == self.K: 71 | s = _counts.sum() 72 | c_lbl = [] 73 | c_prob = [] 74 | prob = None 75 | for i, c in zip(_lbl_idx, _counts): 76 | c_lbl.append(self.idx_to_lbl[i]) 77 | c_prob.append(float(c/s)) 78 | if _pred == i: 79 | prob = float(c/s) 80 | 81 | result.append(( 82 | self.idx_to_lbl[int(_pred)], 83 | float(prob), 84 | c_lbl, 85 | c_prob 86 | )) 87 | else: 88 | result.append((UNKNOWN_CLASS, None, None, None)) 89 | 90 | return result 91 | 92 | def update_training(self, train_X, train_y): 93 | self.X_train = np.array(train_X) 94 | self.y_train = train_y 95 | self.idx_to_lbl = dict(enumerate(set(train_y))) 96 | self.lbl_to_idx = {v: k for k, v in self.idx_to_lbl.items()} 97 | self.y_train_idx = [self.lbl_to_idx[l] for l in self.y_train] 98 | 99 | 100 | def init(): 101 | global X, y, model 102 | X = [] 103 | y = [] 104 | model = KNN() 105 | 106 | 107 | init() 108 | 109 | 110 | def add(new_X, new_y): 111 | global X, y, model 112 | X.extend(new_X) 113 | y.extend(new_y) 114 | model.update_training(X, y) 115 | 116 | def predict(X): 117 | global model 118 | return model.predict(X) 119 | 120 | def training_data_info(): 121 | global y 122 | return Counter(y) 123 | -------------------------------------------------------------------------------- /tensorface/const.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | 4 | 5 | # this size is required for embedding 6 | FACE_PIC_SIZE = 160 7 | 8 | EMBEDDING_SIZE = 512 9 | 10 | PRETREINED_MODEL_DIR = os.path.join(str(Path.home()), 'pretrained_models') 11 | 12 | UNKNOWN_CLASS = "unknown" -------------------------------------------------------------------------------- /tensorface/detection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | 4 | from tensorface.const import PRETREINED_MODEL_DIR 5 | from tensorface.mtcnn import detect_face, create_mtcnn 6 | import tensorflow as tf 7 | 8 | from tensorface.model import Face 9 | 10 | 11 | def _setup_mtcnn(): 12 | with tf.Graph().as_default(): 13 | sess = tf.Session() 14 | with sess.as_default(): 15 | return create_mtcnn(sess, PRETREINED_MODEL_DIR) 16 | 17 | 18 | pnet, rnet, onet = _setup_mtcnn() 19 | 20 | 21 | def img_to_np(image): 22 | (im_width, im_height) = image.size 23 | return np.array(image.getdata()).reshape( 24 | (im_height, im_width, 3)).astype(np.uint8) 25 | 26 | 27 | def get_faces(image, threshold=0.5, minsize=20): 28 | img = img_to_np(image) 29 | # face detection parameters 30 | threshold = [0.6, 0.7, 0.7] # three steps's threshold 31 | factor = 0.709 # scale factor 32 | faces = [] 33 | 34 | bounding_boxes, _ = detect_face(img, minsize, pnet, rnet, onet, 35 | threshold, factor) 36 | for bb in bounding_boxes: 37 | img = image.crop(bb[:4]) 38 | bb[2:4] -= bb[:2] 39 | faces.append(Face(*bb, img)) 40 | 41 | return faces 42 | -------------------------------------------------------------------------------- /tensorface/embedding.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import tensorflow as tf 5 | from tensorflow.python.platform import gfile 6 | 7 | from tensorface.const import PRETREINED_MODEL_DIR 8 | 9 | MODEL_FILE_NAME = '20180402-114759/20180402-114759.pb' 10 | 11 | # to get Flask not complain 12 | global tf 13 | _tf = tf 14 | global sess 15 | sess = None 16 | 17 | def load_model(pb_file, input_map=None): 18 | global _tf 19 | global sess 20 | if sess is None: 21 | sess = _tf.Session() 22 | print('Model filename: %s' % pb_file) 23 | with gfile.FastGFile(pb_file, 'rb') as f: 24 | graph_def = _tf.GraphDef() 25 | graph_def.ParseFromString(f.read()) 26 | _tf.import_graph_def(graph_def, input_map=input_map, name='') 27 | 28 | 29 | load_model(os.path.join(PRETREINED_MODEL_DIR, MODEL_FILE_NAME)) 30 | 31 | 32 | # inception net requires this 33 | def prewhiten(x): 34 | mean = np.mean(x) 35 | std = np.std(x) 36 | std_adj = np.maximum(std, 1.0 / np.sqrt(x.size)) 37 | y = np.multiply(np.subtract(x, mean), 1 / std_adj) 38 | return y 39 | 40 | 41 | def embedding(face_np): 42 | global sess 43 | images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0") 44 | embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0") 45 | phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0") 46 | x = prewhiten(face_np) 47 | feed_dict = {images_placeholder: [x], phase_train_placeholder: False} 48 | result = sess.run(embeddings, feed_dict=feed_dict)[0] 49 | return result 50 | 51 | 52 | def input_shape(): 53 | return _tf.get_default_graph().get_tensor_by_name("input:0").get_shape() 54 | 55 | 56 | def embedding_size(): 57 | return _tf.get_default_graph().get_tensor_by_name("embeddings:0").get_shape()[1] 58 | -------------------------------------------------------------------------------- /tensorface/model.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Face: 4 | # face bounding boxes 5 | def __init__(self, x, y, w, h, confidence, img): 6 | self.img = img 7 | self.x = x 8 | self.y = y 9 | self.w = w 10 | self.h = h 11 | self.confidence = confidence 12 | 13 | self.predict_proba = None 14 | self.predict_candidates = None 15 | self.predict_candidates_proba = None 16 | 17 | def data(self): 18 | return { k:v for k, v in self.__dict__.items() if k != 'img'} 19 | 20 | -------------------------------------------------------------------------------- /tensorface/mtcnn.py: -------------------------------------------------------------------------------- 1 | """ Tensorflow implementation of the face detection / alignment algorithm found at 2 | https://github.com/kpzhang93/MTCNN_face_detection_alignment 3 | """ 4 | # MIT License 5 | # 6 | # Copyright (c) 2016 David Sandberg 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | from __future__ import absolute_import 27 | from __future__ import division 28 | from __future__ import print_function 29 | from six import string_types, iteritems 30 | 31 | import numpy as np 32 | import tensorflow as tf 33 | import cv2 34 | import os 35 | 36 | 37 | def layer(op): 38 | """Decorator for composable network layers.""" 39 | 40 | def layer_decorated(self, *args, **kwargs): 41 | # Automatically set a name if not provided. 42 | name = kwargs.setdefault('name', self.get_unique_name(op.__name__)) 43 | # Figure out the layer inputs. 44 | if len(self.terminals) == 0: 45 | raise RuntimeError('No input variables found for layer %s.' % name) 46 | elif len(self.terminals) == 1: 47 | layer_input = self.terminals[0] 48 | else: 49 | layer_input = list(self.terminals) 50 | # Perform the operation and get the output. 51 | layer_output = op(self, layer_input, *args, **kwargs) 52 | # Add to layer LUT. 53 | self.layers[name] = layer_output 54 | # This output is now the input for the next layer. 55 | self.feed(layer_output) 56 | # Return self for chained calls. 57 | return self 58 | 59 | return layer_decorated 60 | 61 | 62 | class Network(object): 63 | 64 | def __init__(self, inputs, trainable=True): 65 | # The input nodes for this network 66 | self.inputs = inputs 67 | # The current list of terminal nodes 68 | self.terminals = [] 69 | # Mapping from layer names to layers 70 | self.layers = dict(inputs) 71 | # If true, the resulting variables are set as trainable 72 | self.trainable = trainable 73 | 74 | self.setup() 75 | 76 | def setup(self): 77 | """Construct the network. """ 78 | raise NotImplementedError('Must be implemented by the subclass.') 79 | 80 | def load(self, data_path, session, ignore_missing=False): 81 | """Load network weights. 82 | data_path: The path to the numpy-serialized network weights 83 | session: The current TensorFlow session 84 | ignore_missing: If true, serialized weights for missing layers are ignored. 85 | """ 86 | data_dict = np.load(data_path, encoding='latin1').item() # pylint: disable=no-member 87 | 88 | for op_name in data_dict: 89 | with tf.variable_scope(op_name, reuse=True): 90 | for param_name, data in iteritems(data_dict[op_name]): 91 | try: 92 | var = tf.get_variable(param_name) 93 | session.run(var.assign(data)) 94 | except ValueError: 95 | if not ignore_missing: 96 | raise 97 | 98 | def feed(self, *args): 99 | """Set the input(s) for the next operation by replacing the terminal nodes. 100 | The arguments can be either layer names or the actual layers. 101 | """ 102 | assert len(args) != 0 103 | self.terminals = [] 104 | for fed_layer in args: 105 | if isinstance(fed_layer, string_types): 106 | try: 107 | fed_layer = self.layers[fed_layer] 108 | except KeyError: 109 | raise KeyError('Unknown layer name fed: %s' % fed_layer) 110 | self.terminals.append(fed_layer) 111 | return self 112 | 113 | def get_output(self): 114 | """Returns the current network output.""" 115 | return self.terminals[-1] 116 | 117 | def get_unique_name(self, prefix): 118 | """Returns an index-suffixed unique name for the given prefix. 119 | This is used for auto-generating layer names based on the type-prefix. 120 | """ 121 | ident = sum(t.startswith(prefix) for t, _ in self.layers.items()) + 1 122 | return '%s_%d' % (prefix, ident) 123 | 124 | def make_var(self, name, shape): 125 | """Creates a new TensorFlow variable.""" 126 | return tf.get_variable(name, shape, trainable=self.trainable) 127 | 128 | def validate_padding(self, padding): 129 | """Verifies that the padding is one of the supported ones.""" 130 | assert padding in ('SAME', 'VALID') 131 | 132 | @layer 133 | def conv(self, inp, k_h, k_w, c_o, s_h, s_w, name, relu=True, padding='SAME', group=1, biased=True): 134 | # Verify that the padding is acceptable 135 | self.validate_padding(padding) 136 | # Get the number of channels in the input 137 | c_i = int(inp.get_shape()[-1]) 138 | # Verify that the grouping parameter is valid 139 | assert c_i % group == 0 140 | assert c_o % group == 0 141 | # Convolution for a given input and kernel 142 | convolve = lambda i, k: tf.nn.conv2d(i, k, [1, s_h, s_w, 1], padding=padding) 143 | with tf.variable_scope(name) as scope: 144 | kernel = self.make_var('weights', shape=[k_h, k_w, c_i // group, c_o]) 145 | # This is the common-case. Convolve the input without any further complications. 146 | output = convolve(inp, kernel) 147 | # Add the biases 148 | if biased: 149 | biases = self.make_var('biases', [c_o]) 150 | output = tf.nn.bias_add(output, biases) 151 | if relu: 152 | # ReLU non-linearity 153 | output = tf.nn.relu(output, name=scope.name) 154 | return output 155 | 156 | @layer 157 | def prelu(self, inp, name): 158 | with tf.variable_scope(name): 159 | i = int(inp.get_shape()[-1]) 160 | alpha = self.make_var('alpha', shape=(i,)) 161 | output = tf.nn.relu(inp) + tf.multiply(alpha, -tf.nn.relu(-inp)) 162 | return output 163 | 164 | @layer 165 | def max_pool(self, inp, k_h, k_w, s_h, s_w, name, padding='SAME'): 166 | self.validate_padding(padding) 167 | return tf.nn.max_pool(inp, 168 | ksize=[1, k_h, k_w, 1], 169 | strides=[1, s_h, s_w, 1], 170 | padding=padding, 171 | name=name) 172 | 173 | @layer 174 | def fc(self, inp, num_out, name, relu=True): 175 | with tf.variable_scope(name): 176 | input_shape = inp.get_shape() 177 | if input_shape.ndims == 4: 178 | # The input is spatial. Vectorize it first. 179 | dim = 1 180 | for d in input_shape[1:].as_list(): 181 | dim *= int(d) 182 | feed_in = tf.reshape(inp, [-1, dim]) 183 | else: 184 | feed_in, dim = (inp, input_shape[-1].value) 185 | weights = self.make_var('weights', shape=[dim, num_out]) 186 | biases = self.make_var('biases', [num_out]) 187 | op = tf.nn.relu_layer if relu else tf.nn.xw_plus_b 188 | fc = op(feed_in, weights, biases, name=name) 189 | return fc 190 | 191 | """ 192 | Multi dimensional softmax, 193 | refer to https://github.com/tensorflow/tensorflow/issues/210 194 | compute softmax along the dimension of target 195 | the native softmax only supports batch_size x dimension 196 | """ 197 | 198 | @layer 199 | def softmax(self, target, axis, name=None): 200 | max_axis = tf.reduce_max(target, axis, keepdims=True) 201 | target_exp = tf.exp(target - max_axis) 202 | normalize = tf.reduce_sum(target_exp, axis, keepdims=True) 203 | softmax = tf.div(target_exp, normalize, name) 204 | return softmax 205 | 206 | 207 | class PNet(Network): 208 | def setup(self): 209 | (self.feed('data') # pylint: disable=no-value-for-parameter, no-member 210 | .conv(3, 3, 10, 1, 1, padding='VALID', relu=False, name='conv1') 211 | .prelu(name='PReLU1') 212 | .max_pool(2, 2, 2, 2, name='pool1') 213 | .conv(3, 3, 16, 1, 1, padding='VALID', relu=False, name='conv2') 214 | .prelu(name='PReLU2') 215 | .conv(3, 3, 32, 1, 1, padding='VALID', relu=False, name='conv3') 216 | .prelu(name='PReLU3') 217 | .conv(1, 1, 2, 1, 1, relu=False, name='conv4-1') 218 | .softmax(3, name='prob1')) 219 | 220 | (self.feed('PReLU3') # pylint: disable=no-value-for-parameter 221 | .conv(1, 1, 4, 1, 1, relu=False, name='conv4-2')) 222 | 223 | 224 | class RNet(Network): 225 | def setup(self): 226 | (self.feed('data') # pylint: disable=no-value-for-parameter, no-member 227 | .conv(3, 3, 28, 1, 1, padding='VALID', relu=False, name='conv1') 228 | .prelu(name='prelu1') 229 | .max_pool(3, 3, 2, 2, name='pool1') 230 | .conv(3, 3, 48, 1, 1, padding='VALID', relu=False, name='conv2') 231 | .prelu(name='prelu2') 232 | .max_pool(3, 3, 2, 2, padding='VALID', name='pool2') 233 | .conv(2, 2, 64, 1, 1, padding='VALID', relu=False, name='conv3') 234 | .prelu(name='prelu3') 235 | .fc(128, relu=False, name='conv4') 236 | .prelu(name='prelu4') 237 | .fc(2, relu=False, name='conv5-1') 238 | .softmax(1, name='prob1')) 239 | 240 | (self.feed('prelu4') # pylint: disable=no-value-for-parameter 241 | .fc(4, relu=False, name='conv5-2')) 242 | 243 | 244 | class ONet(Network): 245 | def setup(self): 246 | (self.feed('data') # pylint: disable=no-value-for-parameter, no-member 247 | .conv(3, 3, 32, 1, 1, padding='VALID', relu=False, name='conv1') 248 | .prelu(name='prelu1') 249 | .max_pool(3, 3, 2, 2, name='pool1') 250 | .conv(3, 3, 64, 1, 1, padding='VALID', relu=False, name='conv2') 251 | .prelu(name='prelu2') 252 | .max_pool(3, 3, 2, 2, padding='VALID', name='pool2') 253 | .conv(3, 3, 64, 1, 1, padding='VALID', relu=False, name='conv3') 254 | .prelu(name='prelu3') 255 | .max_pool(2, 2, 2, 2, name='pool3') 256 | .conv(2, 2, 128, 1, 1, padding='VALID', relu=False, name='conv4') 257 | .prelu(name='prelu4') 258 | .fc(256, relu=False, name='conv5') 259 | .prelu(name='prelu5') 260 | .fc(2, relu=False, name='conv6-1') 261 | .softmax(1, name='prob1')) 262 | 263 | (self.feed('prelu5') # pylint: disable=no-value-for-parameter 264 | .fc(4, relu=False, name='conv6-2')) 265 | 266 | (self.feed('prelu5') # pylint: disable=no-value-for-parameter 267 | .fc(10, relu=False, name='conv6-3')) 268 | 269 | 270 | def create_mtcnn(sess, model_path): 271 | if not model_path: 272 | model_path, _ = os.path.split(os.path.realpath(__file__)) 273 | 274 | with tf.variable_scope('pnet'): 275 | data = tf.placeholder(tf.float32, (None, None, None, 3), 'input') 276 | pnet = PNet({'data': data}) 277 | pnet.load(os.path.join(model_path, 'det1.npy'), sess) 278 | with tf.variable_scope('rnet'): 279 | data = tf.placeholder(tf.float32, (None, 24, 24, 3), 'input') 280 | rnet = RNet({'data': data}) 281 | rnet.load(os.path.join(model_path, 'det2.npy'), sess) 282 | with tf.variable_scope('onet'): 283 | data = tf.placeholder(tf.float32, (None, 48, 48, 3), 'input') 284 | onet = ONet({'data': data}) 285 | onet.load(os.path.join(model_path, 'det3.npy'), sess) 286 | 287 | pnet_fun = lambda img: sess.run(('pnet/conv4-2/BiasAdd:0', 'pnet/prob1:0'), feed_dict={'pnet/input:0': img}) 288 | rnet_fun = lambda img: sess.run(('rnet/conv5-2/conv5-2:0', 'rnet/prob1:0'), feed_dict={'rnet/input:0': img}) 289 | onet_fun = lambda img: sess.run(('onet/conv6-2/conv6-2:0', 'onet/conv6-3/conv6-3:0', 'onet/prob1:0'), 290 | feed_dict={'onet/input:0': img}) 291 | return pnet_fun, rnet_fun, onet_fun 292 | 293 | 294 | def detect_face(img, minsize, pnet, rnet, onet, threshold, factor): 295 | """Detects faces in an image, and returns bounding boxes and points for them. 296 | img: input image 297 | minsize: minimum faces' size 298 | pnet, rnet, onet: caffemodel 299 | threshold: threshold=[th1, th2, th3], th1-3 are three steps's threshold 300 | factor: the factor used to create a scaling pyramid of face sizes to detect in the image. 301 | """ 302 | factor_count = 0 303 | total_boxes = np.empty((0, 9)) 304 | points = np.empty(0) 305 | h = img.shape[0] 306 | w = img.shape[1] 307 | minl = np.amin([h, w]) 308 | m = 12.0 / minsize 309 | minl = minl * m 310 | # create scale pyramid 311 | scales = [] 312 | while minl >= 12: 313 | scales += [m * np.power(factor, factor_count)] 314 | minl = minl * factor 315 | factor_count += 1 316 | 317 | # first stage 318 | for scale in scales: 319 | hs = int(np.ceil(h * scale)) 320 | ws = int(np.ceil(w * scale)) 321 | im_data = imresample(img, (hs, ws)) 322 | im_data = (im_data - 127.5) * 0.0078125 323 | img_x = np.expand_dims(im_data, 0) 324 | img_y = np.transpose(img_x, (0, 2, 1, 3)) 325 | out = pnet(img_y) 326 | out0 = np.transpose(out[0], (0, 2, 1, 3)) 327 | out1 = np.transpose(out[1], (0, 2, 1, 3)) 328 | 329 | boxes, _ = generateBoundingBox(out1[0, :, :, 1].copy(), out0[0, :, :, :].copy(), scale, threshold[0]) 330 | 331 | # inter-scale nms 332 | pick = nms(boxes.copy(), 0.5, 'Union') 333 | if boxes.size > 0 and pick.size > 0: 334 | boxes = boxes[pick, :] 335 | total_boxes = np.append(total_boxes, boxes, axis=0) 336 | 337 | numbox = total_boxes.shape[0] 338 | if numbox > 0: 339 | pick = nms(total_boxes.copy(), 0.7, 'Union') 340 | total_boxes = total_boxes[pick, :] 341 | regw = total_boxes[:, 2] - total_boxes[:, 0] 342 | regh = total_boxes[:, 3] - total_boxes[:, 1] 343 | qq1 = total_boxes[:, 0] + total_boxes[:, 5] * regw 344 | qq2 = total_boxes[:, 1] + total_boxes[:, 6] * regh 345 | qq3 = total_boxes[:, 2] + total_boxes[:, 7] * regw 346 | qq4 = total_boxes[:, 3] + total_boxes[:, 8] * regh 347 | total_boxes = np.transpose(np.vstack([qq1, qq2, qq3, qq4, total_boxes[:, 4]])) 348 | total_boxes = rerec(total_boxes.copy()) 349 | total_boxes[:, 0:4] = np.fix(total_boxes[:, 0:4]).astype(np.int32) 350 | dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph = pad(total_boxes.copy(), w, h) 351 | 352 | numbox = total_boxes.shape[0] 353 | if numbox > 0: 354 | # second stage 355 | tempimg = np.zeros((24, 24, 3, numbox)) 356 | for k in range(0, numbox): 357 | tmp = np.zeros((int(tmph[k]), int(tmpw[k]), 3)) 358 | tmp[dy[k] - 1:edy[k], dx[k] - 1:edx[k], :] = img[y[k] - 1:ey[k], x[k] - 1:ex[k], :] 359 | if tmp.shape[0] > 0 and tmp.shape[1] > 0 or tmp.shape[0] == 0 and tmp.shape[1] == 0: 360 | tempimg[:, :, :, k] = imresample(tmp, (24, 24)) 361 | else: 362 | return np.empty() 363 | tempimg = (tempimg - 127.5) * 0.0078125 364 | tempimg1 = np.transpose(tempimg, (3, 1, 0, 2)) 365 | out = rnet(tempimg1) 366 | out0 = np.transpose(out[0]) 367 | out1 = np.transpose(out[1]) 368 | score = out1[1, :] 369 | ipass = np.where(score > threshold[1]) 370 | total_boxes = np.hstack([total_boxes[ipass[0], 0:4].copy(), np.expand_dims(score[ipass].copy(), 1)]) 371 | mv = out0[:, ipass[0]] 372 | if total_boxes.shape[0] > 0: 373 | pick = nms(total_boxes, 0.7, 'Union') 374 | total_boxes = total_boxes[pick, :] 375 | total_boxes = bbreg(total_boxes.copy(), np.transpose(mv[:, pick])) 376 | total_boxes = rerec(total_boxes.copy()) 377 | 378 | numbox = total_boxes.shape[0] 379 | if numbox > 0: 380 | # third stage 381 | total_boxes = np.fix(total_boxes).astype(np.int32) 382 | dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph = pad(total_boxes.copy(), w, h) 383 | tempimg = np.zeros((48, 48, 3, numbox)) 384 | for k in range(0, numbox): 385 | tmp = np.zeros((int(tmph[k]), int(tmpw[k]), 3)) 386 | tmp[dy[k] - 1:edy[k], dx[k] - 1:edx[k], :] = img[y[k] - 1:ey[k], x[k] - 1:ex[k], :] 387 | if tmp.shape[0] > 0 and tmp.shape[1] > 0 or tmp.shape[0] == 0 and tmp.shape[1] == 0: 388 | tempimg[:, :, :, k] = imresample(tmp, (48, 48)) 389 | else: 390 | return np.empty() 391 | tempimg = (tempimg - 127.5) * 0.0078125 392 | tempimg1 = np.transpose(tempimg, (3, 1, 0, 2)) 393 | out = onet(tempimg1) 394 | out0 = np.transpose(out[0]) 395 | out1 = np.transpose(out[1]) 396 | out2 = np.transpose(out[2]) 397 | score = out2[1, :] 398 | points = out1 399 | ipass = np.where(score > threshold[2]) 400 | points = points[:, ipass[0]] 401 | total_boxes = np.hstack([total_boxes[ipass[0], 0:4].copy(), np.expand_dims(score[ipass].copy(), 1)]) 402 | mv = out0[:, ipass[0]] 403 | 404 | w = total_boxes[:, 2] - total_boxes[:, 0] + 1 405 | h = total_boxes[:, 3] - total_boxes[:, 1] + 1 406 | points[0:5, :] = np.tile(w, (5, 1)) * points[0:5, :] + np.tile(total_boxes[:, 0], (5, 1)) - 1 407 | points[5:10, :] = np.tile(h, (5, 1)) * points[5:10, :] + np.tile(total_boxes[:, 1], (5, 1)) - 1 408 | if total_boxes.shape[0] > 0: 409 | total_boxes = bbreg(total_boxes.copy(), np.transpose(mv)) 410 | pick = nms(total_boxes.copy(), 0.7, 'Min') 411 | total_boxes = total_boxes[pick, :] 412 | points = points[:, pick] 413 | 414 | return total_boxes, points 415 | 416 | 417 | # function [boundingbox] = bbreg(boundingbox,reg) 418 | def bbreg(boundingbox, reg): 419 | """Calibrate bounding boxes""" 420 | if reg.shape[1] == 1: 421 | reg = np.reshape(reg, (reg.shape[2], reg.shape[3])) 422 | 423 | w = boundingbox[:, 2] - boundingbox[:, 0] + 1 424 | h = boundingbox[:, 3] - boundingbox[:, 1] + 1 425 | b1 = boundingbox[:, 0] + reg[:, 0] * w 426 | b2 = boundingbox[:, 1] + reg[:, 1] * h 427 | b3 = boundingbox[:, 2] + reg[:, 2] * w 428 | b4 = boundingbox[:, 3] + reg[:, 3] * h 429 | boundingbox[:, 0:4] = np.transpose(np.vstack([b1, b2, b3, b4])) 430 | return boundingbox 431 | 432 | 433 | def generateBoundingBox(imap, reg, scale, t): 434 | """Use heatmap to generate bounding boxes""" 435 | stride = 2 436 | cellsize = 12 437 | 438 | imap = np.transpose(imap) 439 | dx1 = np.transpose(reg[:, :, 0]) 440 | dy1 = np.transpose(reg[:, :, 1]) 441 | dx2 = np.transpose(reg[:, :, 2]) 442 | dy2 = np.transpose(reg[:, :, 3]) 443 | y, x = np.where(imap >= t) 444 | if y.shape[0] == 1: 445 | dx1 = np.flipud(dx1) 446 | dy1 = np.flipud(dy1) 447 | dx2 = np.flipud(dx2) 448 | dy2 = np.flipud(dy2) 449 | score = imap[(y, x)] 450 | reg = np.transpose(np.vstack([dx1[(y, x)], dy1[(y, x)], dx2[(y, x)], dy2[(y, x)]])) 451 | if reg.size == 0: 452 | reg = np.empty((0, 3)) 453 | bb = np.transpose(np.vstack([y, x])) 454 | q1 = np.fix((stride * bb + 1) / scale) 455 | q2 = np.fix((stride * bb + cellsize - 1 + 1) / scale) 456 | boundingbox = np.hstack([q1, q2, np.expand_dims(score, 1), reg]) 457 | return boundingbox, reg 458 | 459 | 460 | # function pick = nms(boxes,threshold,type) 461 | def nms(boxes, threshold, method): 462 | if boxes.size == 0: 463 | return np.empty((0, 3)) 464 | x1 = boxes[:, 0] 465 | y1 = boxes[:, 1] 466 | x2 = boxes[:, 2] 467 | y2 = boxes[:, 3] 468 | s = boxes[:, 4] 469 | area = (x2 - x1 + 1) * (y2 - y1 + 1) 470 | I = np.argsort(s) 471 | pick = np.zeros_like(s, dtype=np.int16) 472 | counter = 0 473 | while I.size > 0: 474 | i = I[-1] 475 | pick[counter] = i 476 | counter += 1 477 | idx = I[0:-1] 478 | xx1 = np.maximum(x1[i], x1[idx]) 479 | yy1 = np.maximum(y1[i], y1[idx]) 480 | xx2 = np.minimum(x2[i], x2[idx]) 481 | yy2 = np.minimum(y2[i], y2[idx]) 482 | w = np.maximum(0.0, xx2 - xx1 + 1) 483 | h = np.maximum(0.0, yy2 - yy1 + 1) 484 | inter = w * h 485 | if method is 'Min': 486 | o = inter / np.minimum(area[i], area[idx]) 487 | else: 488 | o = inter / (area[i] + area[idx] - inter) 489 | I = I[np.where(o <= threshold)] 490 | pick = pick[0:counter] 491 | return pick 492 | 493 | 494 | # function [dy edy dx edx y ey x ex tmpw tmph] = pad(total_boxes,w,h) 495 | def pad(total_boxes, w, h): 496 | """Compute the padding coordinates (pad the bounding boxes to square)""" 497 | tmpw = (total_boxes[:, 2] - total_boxes[:, 0] + 1).astype(np.int32) 498 | tmph = (total_boxes[:, 3] - total_boxes[:, 1] + 1).astype(np.int32) 499 | numbox = total_boxes.shape[0] 500 | 501 | dx = np.ones((numbox), dtype=np.int32) 502 | dy = np.ones((numbox), dtype=np.int32) 503 | edx = tmpw.copy().astype(np.int32) 504 | edy = tmph.copy().astype(np.int32) 505 | 506 | x = total_boxes[:, 0].copy().astype(np.int32) 507 | y = total_boxes[:, 1].copy().astype(np.int32) 508 | ex = total_boxes[:, 2].copy().astype(np.int32) 509 | ey = total_boxes[:, 3].copy().astype(np.int32) 510 | 511 | tmp = np.where(ex > w) 512 | edx.flat[tmp] = np.expand_dims(-ex[tmp] + w + tmpw[tmp], 1) 513 | ex[tmp] = w 514 | 515 | tmp = np.where(ey > h) 516 | edy.flat[tmp] = np.expand_dims(-ey[tmp] + h + tmph[tmp], 1) 517 | ey[tmp] = h 518 | 519 | tmp = np.where(x < 1) 520 | dx.flat[tmp] = np.expand_dims(2 - x[tmp], 1) 521 | x[tmp] = 1 522 | 523 | tmp = np.where(y < 1) 524 | dy.flat[tmp] = np.expand_dims(2 - y[tmp], 1) 525 | y[tmp] = 1 526 | 527 | return dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph 528 | 529 | 530 | def rerec(bboxA): 531 | """Convert bboxA to square.""" 532 | h = bboxA[:, 3] - bboxA[:, 1] 533 | w = bboxA[:, 2] - bboxA[:, 0] 534 | l = np.maximum(w, h) 535 | bboxA[:, 0] = bboxA[:, 0] + w * 0.5 - l * 0.5 536 | bboxA[:, 1] = bboxA[:, 1] + h * 0.5 - l * 0.5 537 | bboxA[:, 2:4] = bboxA[:, 0:2] + np.transpose(np.tile(l, (2, 1))) 538 | return bboxA 539 | 540 | 541 | def imresample(img, sz): 542 | im_data = cv2.resize(img, (sz[1], sz[0]), interpolation=cv2.INTER_AREA) # @UndefinedVariable 543 | return im_data 544 | -------------------------------------------------------------------------------- /tensorface/recognition.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import numpy as np 4 | from PIL import Image 5 | 6 | from tensorface import classifier 7 | from tensorface.const import FACE_PIC_SIZE, EMBEDDING_SIZE 8 | from tensorface.detection import img_to_np 9 | from tensorface.embedding import embedding 10 | from tensorface.model import Face 11 | 12 | 13 | def recognize(faces) -> List[Face]: 14 | X = np.zeros((len(faces), EMBEDDING_SIZE), np.float32) 15 | for i, f in enumerate(faces): 16 | img = f.img.resize((FACE_PIC_SIZE, FACE_PIC_SIZE), Image.BICUBIC) if f.img.size != (FACE_PIC_SIZE, 17 | FACE_PIC_SIZE) else f.img 18 | 19 | X[i, :] = embedding(img_to_np(img)) 20 | 21 | result = classifier.predict(X) 22 | for f, r in zip(faces, result): 23 | n, prob, c_list, c_prob = r 24 | f.name = n 25 | f.predict_proba = prob 26 | f.predict_candidates = c_list 27 | f.predict_candidates_proba = c_prob 28 | 29 | return faces 30 | 31 | 32 | def learn_from_examples(name, image_sprite, num, size): 33 | 34 | print("Adding new training data for: ", name, "...") 35 | 36 | # update classifier 37 | faces = [] 38 | for i in range(int(num)): 39 | faces.append(image_sprite.crop(( 40 | size * i, 41 | 0, 42 | size * (i + 1), 43 | size 44 | ))) 45 | 46 | # do embedding for all faces 47 | X = np.zeros((num, EMBEDDING_SIZE), np.float32) 48 | for i, f in enumerate(faces): 49 | X[i, :] = embedding(img_to_np(f)) 50 | 51 | # all example cames from single person 52 | y = [name] * num 53 | 54 | # do the actual update 55 | classifier.add(X, y) 56 | 57 | return classifier.training_data_info() 58 | -------------------------------------------------------------------------------- /tensorface/recognition_sklearn.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from PIL import Image 4 | from sklearn.externals import joblib 5 | 6 | from tensorface.const import FACE_PIC_SIZE 7 | from tensorface.detection import img_to_np 8 | from tensorface.embedding import embedding 9 | from tensorface.model import Face 10 | 11 | UNKNOW_CLASS = "unknown" 12 | 13 | model = joblib.load('/Users/b.twardowski/Development/tf-face-recognition/notebooks/knn_test.model') 14 | 15 | 16 | def recognize(faces) -> List[Face]: 17 | for f in faces: 18 | img = f.img.resize((FACE_PIC_SIZE, FACE_PIC_SIZE), Image.BICUBIC) if f.img.size != (FACE_PIC_SIZE, 19 | FACE_PIC_SIZE) else f.img 20 | 21 | e = embedding(img_to_np(img)) 22 | x = e.reshape([1, -1]) 23 | n = model.predict(x)[0] 24 | f.name = n 25 | f.predict_proba = model.predict_proba(x).tolist() 26 | return faces 27 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/__init__.py -------------------------------------------------------------------------------- /test/test_embedding.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PIL import Image 3 | 4 | from tensorface import embedding 5 | from tensorface.const import FACE_PIC_SIZE 6 | from tensorface.detection import img_to_np 7 | 8 | 9 | def test_embedding_rand(): 10 | embedding.embedding(np.random.randint(0, 254, (FACE_PIC_SIZE, FACE_PIC_SIZE, 3))) 11 | 12 | 13 | def test_embedding(): 14 | pic = Image.open('./train_StudBoy_160_10.png') 15 | f_pic = pic.crop((0, 0, FACE_PIC_SIZE, FACE_PIC_SIZE)) 16 | f_np = img_to_np(f_pic) 17 | # crop = cv2.resize(crop, (96, 96), interpolation=cv2.INTER_CUBIC) 18 | e = embedding.embedding(f_np) 19 | e.shape == embedding.embedding_size() 20 | 21 | 22 | def test_embedding_size(): 23 | # [batch_size, height, width, 3] 24 | assert embedding.embedding_size() == 512 25 | 26 | 27 | def test_input_shape(): 28 | assert embedding.input_shape() == (1, 2) 29 | -------------------------------------------------------------------------------- /test/test_examples/faces/Bartek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/test_examples/faces/Bartek.png -------------------------------------------------------------------------------- /test/test_examples/faces/CoverGirl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/test_examples/faces/CoverGirl.png -------------------------------------------------------------------------------- /test/test_examples/faces/StudBoy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/test_examples/faces/StudBoy.png -------------------------------------------------------------------------------- /test/test_examples/faces/StudGirl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/test_examples/faces/StudGirl.png -------------------------------------------------------------------------------- /test/test_examples/faces/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/test_examples/faces/unknown.png -------------------------------------------------------------------------------- /test/test_examples/faces/unknown_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/test_examples/faces/unknown_1.png -------------------------------------------------------------------------------- /test/test_examples/test_1523794239.922389.json: -------------------------------------------------------------------------------- 1 | [{"x": 105.22474923729897, "y": 201.68234062194824, "w": 134.6091696768999, "h": 159.71059942245483, "confidence": 0.9999376535415649, "predict_proba": [[1.0, 0.0]], "name": "cover-girl"}, {"x": 449.5062884092331, "y": 168.0526333898306, "w": 62.023711040616035, "h": 70.31412920355797, "confidence": 0.9999207258224487, "predict_proba": [[0.0, 1.0]], "name": "stud1"}, {"x": 399.11673390865326, "y": 91.69564455747604, "w": 49.928053483366966, "h": 55.5232461206615, "confidence": 0.9998167157173157, "predict_proba": [[0.0, 1.0]], "name": "stud1"}, {"x": 509.7535718232393, "y": 128.31520192325115, "w": 46.7483921200037, "h": 56.94013901986182, "confidence": 0.9973353743553162, "predict_proba": [[0.4, 0.6]], "name": "stud1"}, {"x": 389.70470543950796, "y": 180.8956884369254, "w": 48.89784427732229, "h": 55.27318821847439, "confidence": 0.997032642364502, "predict_proba": [[1.0, 0.0]], "name": "cover-girl"}, {"x": 342.1486351788044, "y": 228.05428725481033, "w": 54.841732293367386, "h": 79.69219416938722, "confidence": 0.9900246858596802, "predict_proba": [[1.0, 0.0]], "name": "cover-girl"}, {"x": 361.4081776738167, "y": 332.8894887715578, "w": 33.042817048728466, "h": 42.92115281522274, "confidence": 0.7816532850265503, "predict_proba": [[0.0, 1.0]], "name": "stud1"}] -------------------------------------------------------------------------------- /test/test_examples/test_1523794239.922389.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/test_examples/test_1523794239.922389.png -------------------------------------------------------------------------------- /test/train_examples/train_Bartek_160_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/train_examples/train_Bartek_160_10.png -------------------------------------------------------------------------------- /test/train_examples/train_CoverGirl_160_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/train_examples/train_CoverGirl_160_10.png -------------------------------------------------------------------------------- /test/train_examples/train_StudBoy_160_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/train_examples/train_StudBoy_160_10.png -------------------------------------------------------------------------------- /test/train_examples/train_StudGirl_160_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btwardow/tf-face-recognition/12c56c8a59cb9445508ad24448bc8e11a8cbc406/test/train_examples/train_StudGirl_160_10.png --------------------------------------------------------------------------------