├── 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 | 
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 |
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 |
--------------------------------------------------------------------------------