├── lib ├── __init__.py └── detector.py ├── Dockerfile ├── Procfile ├── app.json ├── static ├── js │ └── jquery.min.js └── css │ └── bootstrap.min.css ├── .dockerignore ├── requirements.txt ├── docker-compose.yml ├── gulpfile.js ├── .eslintrc ├── README.md ├── package.json ├── main.py ├── LICENSE ├── templates └── index.html ├── .gitignore └── src └── js └── main.js /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM sugyan/heroku-python-opencv 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn main:app --log-file=- 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "sugyan/heroku-python-opencv" 3 | } 4 | -------------------------------------------------------------------------------- /static/js/jquery.min.js: -------------------------------------------------------------------------------- 1 | ../../node_modules/jquery/dist/jquery.min.js -------------------------------------------------------------------------------- /static/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | ../../node_modules/bootstrap/dist/css/bootstrap.min.css -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !requirements.txt 3 | !main.py 4 | !lib 5 | !node_modules 6 | !static 7 | !templates 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cv2==1.0 2 | Flask==0.10.1 3 | gunicorn==19.4.1 4 | itsdangerous==0.24 5 | Jinja2==2.8 6 | MarkupSafe==0.23 7 | numpy==1.10.1 8 | Werkzeug==0.11.2 9 | wheel==0.24.0 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: . 3 | command: gunicorn main:app --log-file=- 4 | working_dir: /app/user 5 | environment: 6 | PORT: 8080 7 | ports: 8 | - '8080:8080' 9 | shell: 10 | build: . 11 | command: bash 12 | working_dir: /app/user 13 | environment: 14 | PORT: 8080 15 | ports: 16 | - '8080:8080' 17 | volumes: 18 | - '.:/app/user' 19 | 20 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | 5 | gulp.task('build', function() { 6 | return gulp.src('src/js/*.js') 7 | .pipe(babel({ presets: ['es2015'] })) 8 | .pipe(gulp.dest('static/js')); 9 | }); 10 | 11 | gulp.task('watch', function() { 12 | gulp.watch('src/js/*.js', ['build']); 13 | }); 14 | 15 | gulp.task('default', ['build']); 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 4 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [ 16 | 2, 17 | "always" 18 | ] 19 | }, 20 | "env": { 21 | "es6": true, 22 | "browser": true 23 | }, 24 | "extends": "eslint:recommended" 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # face-detector 2 | 3 | face detection by OpenCV. 4 | 5 | ![](https://cloud.githubusercontent.com/assets/80381/11557325/77d7da88-99ef-11e5-8551-d3e0f1f5124c.png) 6 | 7 | ## Requirements ## 8 | 9 | - Python 2.7 10 | - OpenCV 3.0 11 | - Node 5.1 12 | 13 | ## How to use ## 14 | 15 | $ git@github.com:sugyan/face-detector.git 16 | $ cd face-detector 17 | $ pip install -r requirements.txt 18 | $ npm install 19 | $ gunicorn main:app --log-file=- 20 | 21 | ## Deploy to heroku ## 22 | 23 | $ git@github.com:sugyan/face-detector.git 24 | $ cd face-detector 25 | $ npm install 26 | $ heroku docker:release 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "face-detector", 3 | "version": "1.0.0", 4 | "description": "face detection by OpenCV", 5 | "main": "", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "postinstall": "gulp" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sugyan/face-detector.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/sugyan/face-detector/issues" 18 | }, 19 | "homepage": "https://github.com/sugyan/face-detector#readme", 20 | "devDependencies": { 21 | "babel-preset-es2015": "^6.1.18", 22 | "bootstrap": "^3.3.6", 23 | "gulp": "^3.9.0", 24 | "gulp-babel": "^6.1.1", 25 | "jquery": "^3.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, render_template 2 | 3 | import cv2 4 | import numpy as np 5 | import urllib 6 | from lib import detector 7 | 8 | app = Flask(__name__) 9 | app.debug = True 10 | 11 | @app.route('/api') 12 | def api(): 13 | url = request.args.get('url') 14 | if url is None: 15 | return jsonify(error='"url" is required.') 16 | try: 17 | data = urllib.urlopen(url).read() 18 | except Exception: 19 | return jsonify(error='urlopen failed.') 20 | buf = np.fromstring(data, dtype=np.uint8) 21 | img = cv2.imdecode(buf, cv2.IMREAD_COLOR) 22 | if img is None: 23 | return jsonify(error='read image failed.') 24 | 25 | faces, image = detector.detect(img) 26 | return jsonify(faces=faces, image=image, url=url) 27 | 28 | @app.route('/') 29 | def main(): 30 | return render_template('index.html') 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yoshihiro Sugi 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 | 23 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | face detector 6 | 7 | 8 | 9 | 10 | 11 | 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 | 28 |
29 | 30 |
31 |
32 |
33 |

response:

34 |

35 |         
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | # lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | 62 | # Logs 63 | logs 64 | *.log 65 | npm-debug.log* 66 | 67 | # Runtime data 68 | pids 69 | *.pid 70 | *.seed 71 | 72 | # Directory for instrumented libs generated by jscoverage/JSCover 73 | lib-cov 74 | 75 | # Coverage directory used by tools like istanbul 76 | coverage 77 | 78 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (http://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directory 88 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 89 | node_modules 90 | 91 | # Optional npm cache directory 92 | .npm 93 | 94 | # Optional REPL history 95 | .node_repl_history 96 | 97 | 98 | # Others 99 | static/js/main.js 100 | -------------------------------------------------------------------------------- /lib/detector.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import math 4 | from math import sin, cos 5 | from os import path 6 | 7 | cascades_dir = path.normpath(path.join(cv2.__file__, '..', '..', '..', '..', 'share', 'OpenCV', 'haarcascades')) 8 | max_size = 720 9 | 10 | def detect(img): 11 | cascade_f = cv2.CascadeClassifier(path.join(cascades_dir, 'haarcascade_frontalface_alt2.xml')) 12 | cascade_e = cv2.CascadeClassifier(path.join(cascades_dir, 'haarcascade_eye.xml')) 13 | # resize if learch image 14 | shape = img.shape 15 | if max(shape[0], shape[1]) > max_size: 16 | l = max(shape[0], shape[1]) 17 | img = cv2.resize(img, (shape[1] * max_size / l, shape[0] * max_size / l)) 18 | rows, cols, _ = img.shape 19 | # create gray image for rotate 20 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 21 | hypot = int(math.ceil(math.hypot(rows, cols))) 22 | frame = np.zeros((hypot, hypot), np.uint8) 23 | frame[(hypot - rows) * 0.5:(hypot + rows) * 0.5, (hypot - cols) * 0.5:(hypot + cols) * 0.5] = gray 24 | 25 | def translate(coord, deg): 26 | x, y = coord 27 | rad = math.radians(deg) 28 | return { 29 | 'x': ( cos(rad) * x + sin(rad) * y - hypot * 0.5 * cos(rad) - hypot * 0.5 * sin(rad) + hypot * 0.5 - (hypot - cols) * 0.5) / float(cols) * 100.0, 30 | 'y': (- sin(rad) * x + cos(rad) * y + hypot * 0.5 * sin(rad) - hypot * 0.5 * cos(rad) + hypot * 0.5 - (hypot - rows) * 0.5) / float(rows) * 100.0, 31 | } 32 | # rotate and detect faces 33 | results = [] 34 | for deg in range(-48, 49, 6): 35 | M = cv2.getRotationMatrix2D((hypot * 0.5, hypot * 0.5), deg, 1.0) 36 | rotated = cv2.warpAffine(frame, M, (hypot, hypot)) 37 | faces = cascade_f.detectMultiScale(rotated, 1.08, 2) 38 | print deg, len(faces) 39 | for face in faces: 40 | x, y, w, h = face 41 | # eyes in face? 42 | y_offset = int(h * 0.1) 43 | roi = rotated[y + y_offset: y + h, x: x + w] 44 | eyes = cascade_e.detectMultiScale(roi, 1.05) 45 | eyes = filter(lambda e: (e[0] > w / 2 or e[0] + e[2] < w / 2) and e[1] + e[3] < h / 2, eyes) 46 | if len(eyes) == 2 and abs(eyes[0][0] - eyes[1][0]) > w / 4: 47 | score = math.atan2(abs(eyes[1][1] - eyes[0][1]), abs(eyes[1][0] - eyes[0][0])) 48 | if eyes[0][1] == eyes[1][1]: 49 | score = 0.0 50 | results.append({ 51 | 'center': translate([x + w * 0.5, y + h * 0.5], -deg), 52 | 'w': float(w) / float(cols) * 100.0, 53 | 'h': float(h) / float(rows) * 100.0, 54 | 'eyes': [translate([x + e[0] + e[2] * 0.5, y + y_offset + e[1] + e[3] * 0.5], -deg) for e in eyes], 55 | 'score': score, 56 | }) 57 | # unify duplicate faces 58 | faces = [] 59 | for result in results: 60 | x, y = result['center']['x'], result['center']['y'] 61 | exists = False 62 | for i in range(len(faces)): 63 | face = faces[i] 64 | if (face['center']['x'] - face['w'] * 0.5 < x < face['center']['x'] + face['w'] * 0.5 and 65 | face['center']['y'] - face['h'] * 0.5 < y < face['center']['y'] + face['h'] * 0.5): 66 | exists = True 67 | if result['score'] < face['score']: 68 | faces[i] = result 69 | break 70 | if not exists: 71 | faces.append(result) 72 | for face in faces: 73 | del face['score'] 74 | return faces, { 'height': shape[0], 'width': shape[1] } 75 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | class Main { 3 | constructor(canvas, output, message) { 4 | this.output = $(output); 5 | this.message = $(message); 6 | this.width = canvas.width; 7 | this.height = canvas.height; 8 | this.ctx = canvas.getContext('2d'); 9 | } 10 | load(url) { 11 | window.clearInterval(this.interval); 12 | const img = new Image(); 13 | img.onload = () => { 14 | const scale = Math.max(img.width / this.width, img.height / this.height); 15 | const w = img.width / scale; 16 | const h = img.height / scale; 17 | const offset_x = (this.width - w) / 2.0; 18 | const offset_y = (this.width - h) / 2.0; 19 | this.fillStyle = 'rgb(0, 0, 0)'; 20 | this.ctx.fillRect(0, 0, this.width, this.height); 21 | this.ctx.drawImage(img, offset_x, offset_y, w, h); 22 | this.output.text(''); 23 | this.message.text('.'); 24 | this.interval = window.setInterval(() => { 25 | this.message.text(this.message.text() + '.'); 26 | }, 500); 27 | $.ajax({ 28 | url: '/api', 29 | data: { 30 | url: url 31 | }, 32 | success: (result) => { 33 | window.clearInterval(this.interval); 34 | if (result.url != this.url) { 35 | return; 36 | } 37 | this.message.text(result.faces.length + ' faces detected'); 38 | this.output.text(JSON.stringify(result, null, ' ')); 39 | result.faces.forEach((face) => { 40 | const center = { 41 | x: offset_x + face.center.x * w / 100.0, 42 | y: offset_y + face.center.y * h / 100.0 43 | }; 44 | const rad = Math.atan2((face.eyes[0].y - face.eyes[1].y) * h, (face.eyes[0].x - face.eyes[1].x) * w); 45 | const tl = this.rotate({ x: offset_x + (face.center.x - face.w * 0.5) * w / 100.0, y: offset_y + (face.center.y - face.h * 0.5) * h / 100.0 }, center, -rad); 46 | const tr = this.rotate({ x: offset_x + (face.center.x + face.w * 0.5) * w / 100.0, y: offset_y + (face.center.y - face.h * 0.5) * h / 100.0 }, center, -rad); 47 | const bl = this.rotate({ x: offset_x + (face.center.x - face.w * 0.5) * w / 100.0, y: offset_y + (face.center.y + face.h * 0.5) * h / 100.0 }, center, -rad); 48 | const br = this.rotate({ x: offset_x + (face.center.x + face.w * 0.5) * w / 100.0, y: offset_y + (face.center.y + face.h * 0.5) * h / 100.0 }, center, -rad); 49 | // draw face rectangle 50 | this.ctx.beginPath(); 51 | this.ctx.moveTo(tl.x, tl.y); 52 | this.ctx.lineTo(tr.x, tr.y); 53 | this.ctx.lineTo(br.x, br.y); 54 | this.ctx.lineTo(bl.x, bl.y); 55 | this.ctx.closePath(); 56 | this.ctx.lineWidth = 2; 57 | this.ctx.strokeStyle = 'rgb(0, 255, 0)'; 58 | this.ctx.stroke(); 59 | // draw eye 60 | face.eyes.forEach((eye) => { 61 | this.ctx.beginPath(); 62 | this.ctx.arc( 63 | offset_x + eye.x * w / 100.0, 64 | offset_y + eye.y * h / 100.0, 65 | (w * face.w + h * face.h) / 2000.0, 66 | 0.0, Math.PI * 2.0 67 | ); 68 | this.ctx.lineWidth = 2; 69 | this.ctx.strokeStyle = 'rgb(255, 0, 0)'; 70 | this.ctx.stroke(); 71 | }); 72 | }); 73 | } 74 | }); 75 | }; 76 | img.onerror = () => { 77 | this.message.text('error!'); 78 | }; 79 | img.src = this.url = url; 80 | } 81 | rotate(target, center, rad) { 82 | return { 83 | x: Math.cos(rad) * target.x + Math.sin(rad) * target.y - center.x * Math.cos(rad) - center.y * Math.sin(rad) + center.x, 84 | y: - Math.sin(rad) * target.x + Math.cos(rad) * target.y + center.x * Math.sin(rad) - center.y * Math.cos(rad) + center.y 85 | }; 86 | } 87 | } 88 | 89 | $(() => { 90 | const main = new Main( 91 | document.getElementById('canvas'), 92 | document.getElementById('response'), 93 | document.getElementById('message') 94 | ); 95 | $('#url').submit(() => { 96 | const url = $('input[name="image_url"]').val(); 97 | if (url) { 98 | main.load(url); 99 | } 100 | return false; 101 | }); 102 | }); 103 | --------------------------------------------------------------------------------