├── .gitignore ├── tests ├── assets │ └── faces.jpg ├── test-runner-dist.html ├── test-runner.html └── tests.js ├── examples ├── assets │ ├── video.mp4 │ ├── video.ogv │ ├── video.webm │ └── picture.jpg ├── img │ ├── background.jpg │ ├── tryit.svg │ └── logo.svg ├── css │ └── styles.css ├── picture.html └── video.html ├── .travis.yml ├── bower.json ├── package.json ├── LICENSE.md ├── README.md ├── src ├── jquery.facedetection.js └── ccv.js └── gulpfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dev/* 3 | tmp/* -------------------------------------------------------------------------------- /tests/assets/faces.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysalvat/jquery.facedetection/HEAD/tests/assets/faces.jpg -------------------------------------------------------------------------------- /examples/assets/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysalvat/jquery.facedetection/HEAD/examples/assets/video.mp4 -------------------------------------------------------------------------------- /examples/assets/video.ogv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysalvat/jquery.facedetection/HEAD/examples/assets/video.ogv -------------------------------------------------------------------------------- /examples/assets/video.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysalvat/jquery.facedetection/HEAD/examples/assets/video.webm -------------------------------------------------------------------------------- /examples/assets/picture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysalvat/jquery.facedetection/HEAD/examples/assets/picture.jpg -------------------------------------------------------------------------------- /examples/img/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysalvat/jquery.facedetection/HEAD/examples/img/background.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 5.11.1 4 | 5 | before_script: 6 | - npm install -g gulp 7 | 8 | script: "gulp test" -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.facedetection", 3 | "version": "2.0.3", 4 | "homepage": "https://facedetection.jaysalvat.com", 5 | "main": "./dist/jquery.facedetection.min.js", 6 | "dependencies": { 7 | "jquery": ">=1.10" 8 | } 9 | } -------------------------------------------------------------------------------- /tests/test-runner-dist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
12 |
13 |
14 |
12 |
13 |
14 |
36 |
41 |
42 | Apply the plugin to this image and get the face coordinates.
43 |
44 |
51 |
52 | Results
53 | -------
54 |
55 | Returns an array of found faces object:
56 |
57 | - **x** — X coord of the face in the picture
58 | - **y** — Y coord of the face in the picture
59 | - **width** — Width of the face
60 | - **height** — Height of the face
61 | - **positionX** — X position relative to the document
62 | - **positionY** — Y position relative to the document
63 | - **offsetX** — X position relative to the offset parent
64 | - **offsetY** — Y position relative to the offset parent
65 | - **scaleX** — Ratio between original image width and displayed width
66 | - **scaleY** — Ratio between original image height and displayed height
67 | - **confidence** — Level of confidence
68 |
69 | Settings
70 | --------
71 | - **interval** — Interval (default 4)
72 | - **minNeighbors** — Minimum neighbors threshold which sets the cutoff level for discarding rectangle groups as face (default 1)
73 | - **confidence** — Minimum confidence (default null)
74 | - **async** — Async mode if Worker available (default false). The async mode uses Workers and needs the script to be on the same domain.
75 | - **grayscale** — Convert to grayscale before processing (default true)
76 | - **complete** — Callback function trigged after the detection is completed
77 |
78 | complete: function (faces) {
79 | // ...
80 | }
81 |
82 | - **error** — Callback function trigged on errors
83 |
84 | error: function (code, message) {
85 | // ...
86 | }
87 |
--------------------------------------------------------------------------------
/src/jquery.facedetection.js:
--------------------------------------------------------------------------------
1 | /*
2 | FaceDetection jQuery Plugin
3 | Copyright (c) 2016 Jay Salvat
4 | */
5 |
6 | /* global $, ccv, cascade */
7 |
8 | $.fn.faceDetection = function (settingsOrCallback) {
9 | "use strict";
10 |
11 | var time;
12 |
13 | var options = {
14 | interval: 4,
15 | minNeighbors: 1,
16 | grayscale: true,
17 | confidence: null,
18 | async: false,
19 | complete: function () {}, // (faces)
20 | error: function () {} // (code, message)
21 | };
22 |
23 | if ($.isFunction(settingsOrCallback)) {
24 | options.complete = settingsOrCallback;
25 | } else {
26 | $.extend(options, settingsOrCallback);
27 | }
28 |
29 | return this.each(function() {
30 | var $$ = $(this),
31 | offset = $$.offset(),
32 | position = $$.position(),
33 | scaleX = ($$.width() / (this.naturalWidth || this.videoWidth )) || 1,
34 | scaleY = ($$.height() / (this.naturalHeight || this.videoHeight)) || 1;
35 |
36 | if (!$$.is('img, video, canvas')) {
37 | options.error.apply($$, [ 1, 'Face detection is possible on images, videos and canvas only.' ]);
38 | options.complete.apply($$, [ [] ]);
39 |
40 | return;
41 | }
42 |
43 | function detect() {
44 | var source, canvas;
45 |
46 | time = new Date().getTime();
47 |
48 | if ($$.is('img')) {
49 | source = new Image();
50 | source.src = $$.attr('src');
51 | source.crossOrigin = $$.attr('crossorigin');
52 |
53 | canvas = ccv.pre(source);
54 | } else if ($$.is('video') || $$.is('canvas')) {
55 | var copy, context;
56 |
57 | source = $$[0];
58 |
59 | copy = document.createElement('canvas');
60 | copy.setAttribute('width', source.videoWidth || source.width);
61 | copy.setAttribute('height', source.videoHeight || source.height);
62 |
63 | context = copy.getContext("2d");
64 | context.drawImage(source, 0, 0);
65 |
66 | canvas = ccv.pre(copy);
67 | }
68 |
69 | if (options.grayscale) {
70 | canvas = ccv.grayscale(canvas);
71 | }
72 |
73 | try {
74 | if (options.async && window.Worker) {
75 | ccv.detect_objects({
76 | "canvas": canvas,
77 | "cascade": cascade,
78 | "interval": options.interval,
79 | "min_neighbors": options.minNeighbors,
80 | "worker": 1,
81 | "async": true
82 | })(done);
83 | } else {
84 | done(ccv.detect_objects({
85 | "canvas": canvas,
86 | "cascade": cascade,
87 | "interval": options.interval,
88 | "min_neighbors": options.minNeighbors
89 | }));
90 | }
91 | } catch (e) {
92 | options.error.apply($$, [ 2, e.message ]);
93 | options.complete.apply($$, [ false ]);
94 | }
95 | }
96 |
97 | function done(faces) {
98 | var n = faces.length,
99 | data = [];
100 |
101 | for (var i = 0; i < n; ++i) {
102 | if (options.confidence !== null && faces[i].confidence <= options.confidence) {
103 | continue;
104 | }
105 |
106 | faces[i].positionX = position.left + faces[i].x;
107 | faces[i].positionY = position.top + faces[i].y;
108 | faces[i].offsetX = offset.left + faces[i].x;
109 | faces[i].offsetY = offset.top + faces[i].y;
110 | faces[i].scaleX = scaleX;
111 | faces[i].scaleY = scaleY;
112 |
113 | data.push(faces[i]);
114 | }
115 |
116 | data.time = new Date().getTime() - time;
117 |
118 | options.complete.apply($$, [ data ]);
119 | }
120 |
121 | return detect();
122 | });
123 | };
124 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /* Utlimate Jay Mega Gulpfile */
2 | /* jshint node: true */
3 |
4 | (function () {
5 | "use strict";
6 |
7 | var pkg = require("./package.json"),
8 | del = require("del"),
9 | yargs = require("yargs"),
10 | exec = require("exec"),
11 | fs = require("fs"),
12 | gulp = require("gulp"),
13 | bump = require("gulp-bump"),
14 | header = require("gulp-header"),
15 | qunit = require("gulp-qunit"),
16 | uglify = require("gulp-uglifyjs"),
17 | jshint = require('gulp-jshint'),
18 | gutil = require("gulp-util"),
19 | zip = require("gulp-zip"),
20 | replace = require("gulp-replace"),
21 | gsync = require("gulp-sync"),
22 | sync = gsync(gulp).sync;
23 | // TODO:
24 | // Use gulp-load-plugins
25 |
26 | var version = yargs.argv.type || "patch";
27 |
28 | var settings = {
29 | banner: {
30 | content: [
31 | '/*! ----------------------------------------------------------------------------',
32 | ' * <%= pkg.description %>',
33 | ' * v<%= pkg.version %> released <%= datetime %>',
34 | ' * <%= pkg.homepage %>',
35 | ' * Copyright (c) 2010-<%= year %>, Jay Salvat',
36 | ' * http://jaysalvat.com/',
37 | ' * ----------------------------------------------------------------------------',
38 | ' * ccv.js and cascade.js',
39 | ' * Copyright (c) 2010-<%= year %>, Liu Liu',
40 | ' * http://liuliu.me/',
41 | ' * ----------------------------------------------------------------------------',
42 | ' */',
43 | '',
44 | ].join("\n"),
45 | vars: {
46 | pkg: pkg,
47 | datetime: gutil.date("yyyy-mm-dd HH:MM"),
48 | year: gutil.date("yyyy")
49 | }
50 | },
51 | files: {
52 | in: [
53 | './src/cascade.js',
54 | './src/ccv.js',
55 | './src/jquery.facedetection.js'
56 | ],
57 | out: 'jquery.facedetection'
58 | },
59 | enclose: {
60 | // jQuery / Zepto / Worker mode
61 | '("function" === typeof jQuery) ? jQuery : ("function" === typeof Zepto) ? Zepto : { fn: {} }': '$'
62 | }
63 | };
64 |
65 | var getPackageJson = function () {
66 | return JSON.parse(fs.readFileSync('./package.json'));
67 | };
68 |
69 | gulp.task("clean", function (cb) {
70 | return del([ "./dist" ], cb);
71 | });
72 |
73 | gulp.task("tmp-clean", function (cb) {
74 | return del([ "./tmp" ], cb);
75 | });
76 |
77 | gulp.task("tmp-create", function (cb) {
78 | return exec("mkdir -p ./tmp", cb);
79 | });
80 |
81 | gulp.task("tmp-copy", [ "tmp-create" ], function () {
82 | return gulp.src("./dist/*")
83 | .pipe(gulp.dest("./tmp"));
84 | });
85 |
86 | gulp.task("zip", [ "tmp-create" ], function () {
87 | var filename = settings.files.out + ".zip";
88 |
89 | return gulp.src("./dist/*")
90 | .pipe(zip(filename))
91 | .pipe(gulp.dest("./tmp"));
92 | });
93 |
94 | gulp.task("fail-if-dirty", function (cb) {
95 | return exec('git diff-index HEAD --', function (err, output) { // err, output, code
96 | if (err) {
97 | return cb(err);
98 | }
99 | if (output) {
100 | return cb("Repository is dirty");
101 | }
102 | return cb();
103 | });
104 | });
105 |
106 | gulp.task("fail-if-not-master", function (cb) {
107 | exec('git symbolic-ref -q HEAD', function (err, output) { // err, output, code
108 | if (err) {
109 | return cb(err);
110 | }
111 | if (!/refs\/heads\/master/.test(output)) {
112 | return cb("Branch is not Master");
113 | }
114 | return cb();
115 | });
116 | });
117 |
118 | gulp.task("git-tag", function (cb) {
119 | var message = "v" + getPackageJson().version;
120 |
121 | return exec('git tag ' + message, cb);
122 | });
123 |
124 | gulp.task("git-add", function (cb) {
125 | return exec('git add -A', cb);
126 | });
127 |
128 | gulp.task("git-commit", [ "git-add" ], function (cb) {
129 | var message = "Build v" + getPackageJson().version;
130 |
131 | return exec('git commit -m "' + message + '"', cb);
132 | });
133 |
134 | gulp.task("git-pull", function (cb) {
135 | return exec('git pull origin master', function (err, output, code) {
136 | if (code !== 0) {
137 | return cb(err + output);
138 | }
139 | return cb();
140 | });
141 | });
142 |
143 | gulp.task("git-push", [ "git-commit" ], function (cb) {
144 | return exec('git push origin master --tags', function (err, output, code) {
145 | if (code !== 0) {
146 | return cb(err + output);
147 | }
148 | return cb();
149 | });
150 | });
151 |
152 | gulp.task("meta", [ "tmp-create" ], function (cb) {
153 | var metadata = {
154 | date: gutil.date("yyyy-mm-dd HH:MM"),
155 | version: "v" + getPackageJson().version
156 | },
157 | json = JSON.stringify(metadata, null, 4);
158 |
159 | fs.writeFileSync("tmp/metadata.json", json);
160 | fs.writeFileSync("tmp/metadata.js", "__metadata(" + json + ");");
161 |
162 | return cb();
163 | });
164 |
165 | gulp.task("bump", function () {
166 | return gulp.src([ "package.json", "bower.json", "facedetection.jquery.json" ])
167 | .pipe(bump({
168 | type: version
169 | }))
170 | .pipe(gulp.dest("."));
171 | });
172 |
173 | gulp.task("license", function () {
174 | return gulp.src([ "./LICENSE.md", "./README.md" ])
175 | .pipe(replace(/( 2010-)(\d{4})/g, "$1" + gutil.date("yyyy")))
176 | .pipe(gulp.dest("."));
177 | });
178 |
179 | gulp.task('lint', function() {
180 | return gulp.src('./src/jquery.facedetection.js')
181 | .pipe(jshint())
182 | .pipe(jshint.reporter('default'));
183 | });
184 |
185 | gulp.task("test-dev", function () {
186 | return gulp.src("./tests/test-runner.html")
187 | .pipe(qunit());
188 | });
189 |
190 | gulp.task("test-dist", function () {
191 | return gulp.src("./tests/test-runner-dist.html")
192 | .pipe(qunit());
193 | });
194 |
195 | gulp.task("uglify", function () {
196 | settings.banner.vars.pkg = getPackageJson();
197 |
198 | return gulp.src(settings.files.in)
199 | .pipe(header(settings.banner.content, settings.banner.vars ))
200 | .pipe(uglify(settings.files.out + '.min.js', {
201 | enclose: settings.enclose,
202 | compress: {
203 | warnings: false
204 | },
205 | mangle: true,
206 | outSourceMap: true
207 | }))
208 | .pipe(gulp.dest('./dist/'));
209 | });
210 |
211 | gulp.task("beautify", function () {
212 | settings.banner.vars.pkg = getPackageJson();
213 |
214 | return gulp.src(settings.files.in)
215 | .pipe(header(settings.banner.content, settings.banner.vars ))
216 | .pipe(uglify(settings.files.out + '.js', {
217 | enclose: settings.enclose,
218 | compress: {
219 | warnings: false
220 | },
221 | output: {
222 | beautify: true
223 | },
224 | mangle: false
225 | }))
226 | .pipe(gulp.dest('./dist/'));
227 | });
228 |
229 | // gulp.task("header", function () {
230 | // settings.banner.vars.pkg = getPackageJson();
231 |
232 | // return gulp.src('./dist/*.js')
233 | // .pipe(header(settings.banner.content, settings.banner.vars ))
234 | // .pipe(gulp.dest('./dist/'));
235 | // });
236 |
237 | gulp.task("gh-pages", function (cb) {
238 | version = getPackageJson().version;
239 |
240 | exec([ 'git checkout gh-pages',
241 | 'rm -rf releases/' + version,
242 | 'mkdir -p releases/' + version,
243 | 'cp -r tmp/* releases/' + version,
244 | 'git add -A releases/' + version,
245 | 'rm -rf releases/latest',
246 | 'mkdir -p releases/latest',
247 | 'cp -r tmp/* releases/latest',
248 | 'git add -A releases/latest',
249 | 'git commit -m "Publish release v' + version + '."',
250 | 'git push origin gh-pages',
251 | 'git checkout -',
252 | ].join(" && "),
253 | function (err, output, code) {
254 | if (code !== 0) {
255 | return cb(err + output);
256 | }
257 | return cb();
258 | }
259 | );
260 | });
261 |
262 | gulp.task("npm-publish", function (cb) {
263 | exec('npm publish', function (err, output, code) {
264 | if (code !== 0) {
265 | return cb(err + output);
266 | }
267 | return cb();
268 | }
269 | );
270 | });
271 |
272 | gulp.task("test", sync([
273 | "lint",
274 | "test-dev"
275 | ],
276 | "building"));
277 |
278 | gulp.task("build", sync([
279 | "lint",
280 | "test-dev",
281 | "clean",
282 | "beautify",
283 | "uglify",
284 | "test-dist"
285 | ],
286 | "building"));
287 |
288 | gulp.task("release", sync([
289 | [ "fail-if-not-master", "fail-if-dirty" ],
290 | "git-pull",
291 | "lint",
292 | "test-dev",
293 | "bump",
294 | "license",
295 | "clean",
296 | "beautify",
297 | "uglify",
298 | "test-dist",
299 | "git-add",
300 | "git-commit",
301 | "git-tag",
302 | "git-push",
303 | "publish",
304 | "npm-publish"
305 | ],
306 | "releasing"));
307 |
308 | gulp.task("publish", sync([
309 | [ "fail-if-not-master", "fail-if-dirty" ],
310 | "tmp-create",
311 | "tmp-copy",
312 | "meta",
313 | "zip",
314 | "gh-pages",
315 | "tmp-clean"
316 | ],
317 | "publising"));
318 | })();
319 |
320 | /*
321 |
322 | NOTES
323 | =====
324 |
325 | Gh-pages creation
326 | -----------------
327 |
328 | git checkout --orphan gh-pages
329 | git rm -rf .
330 | rm -fr
331 | echo "Welcome" > index.html
332 | git add index.html
333 | git commit -a -m "First commit"
334 | git push origin gh-pages
335 | git checkout -
336 |
337 | */
338 |
--------------------------------------------------------------------------------
/examples/img/tryit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ccv.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2010, Liu Liu
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 | * Neither the name of the authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10 | */
11 |
12 | /* Added by Jay Salvat: Get Script Path */
13 | try {
14 | var getScriptPath = function () {
15 | "use strict";
16 |
17 | var scripts = document.getElementsByTagName('script');
18 |
19 | for (var i = 0; i < scripts.length; i++) {
20 | if (scripts[i].src.match(/(jquery\.facedetection(\.min)?\.js)|\/ccv\.js/)) {
21 | return scripts[i].src;
22 | }
23 | }
24 | };
25 |
26 | var scriptPath = getScriptPath();
27 | } catch (e) {}
28 | /* End Jay Salvat */
29 |
30 | if (parallable === undefined) {
31 | var parallable = function (file, funct) {
32 | "use strict";
33 |
34 | parallable.core[funct.toString()] = funct().core;
35 |
36 | return function () {
37 | var i;
38 | var async, worker_num, params;
39 | if (arguments.length > 1) {
40 | async = arguments[arguments.length - 2];
41 | worker_num = arguments[arguments.length - 1];
42 | params = new Array(arguments.length - 2);
43 | for (i = 0; i < arguments.length - 2; i++)
44 | params[i] = arguments[i];
45 | } else {
46 | async = arguments[0].async;
47 | worker_num = arguments[0].worker;
48 | params = arguments[0];
49 | delete params["async"];
50 | delete params["worker"];
51 | params = [params];
52 | }
53 | var scope = { "shared" : {} };
54 | var ctrl = funct.apply(scope, params);
55 | if (async) {
56 | return function (complete, error) {
57 | var executed = 0;
58 | var outputs = new Array(worker_num);
59 | var inputs = ctrl.pre.apply(scope, [worker_num]);
60 | /* sanitize scope shared because for Chrome/WebKit, worker only support JSONable data */
61 | for (i in scope.shared)
62 | /* delete function, if any */
63 | if (typeof scope.shared[i] == "function")
64 | delete scope.shared[i];
65 | /* delete DOM object, if any */
66 | else if (scope.shared[i].tagName !== undefined)
67 | delete scope.shared[i];
68 | for (i = 0; i < worker_num; i++) {
69 | var worker = new Worker(file);
70 | worker.onmessage = (function (i) {
71 | return function (event) {
72 | outputs[i] = (typeof event.data == "string") ? JSON.parse(event.data) : event.data;
73 | executed++;
74 | if (executed == worker_num)
75 | complete(ctrl.post.apply(scope, [outputs]));
76 | }
77 | })(i);
78 | var msg = { "input" : inputs[i],
79 | "name" : funct.toString(),
80 | "shared" : scope.shared,
81 | "id" : i,
82 | "worker" : params.worker_num,
83 | "from" : "jquery.facedetection" };
84 | try {
85 | worker.postMessage(msg);
86 | } catch (e) {
87 | worker.postMessage(JSON.stringify(msg));
88 | }
89 | }
90 | }
91 | } else {
92 | return ctrl.post.apply(scope, [[ctrl.core.apply(scope, [ctrl.pre.apply(scope, [1])[0], 0, 1])]]);
93 | }
94 | }
95 | };
96 | parallable.core = {};
97 | }
98 |
99 | function get_named_arguments(params, names) {
100 | if (params.length > 1) {
101 | var new_params = {};
102 | for (var i = 0; i < names.length; i++)
103 | new_params[names[i]] = params[i];
104 | return new_params;
105 | } else if (params.length == 1) {
106 | return params[0];
107 | } else {
108 | return {};
109 | }
110 | }
111 |
112 | var ccv = {
113 | pre : function (image) {
114 | if (image.tagName.toLowerCase() == "img") {
115 | var canvas = document.createElement("canvas");
116 | document.body.appendChild(image);
117 | canvas.width = image.offsetWidth;
118 | canvas.style.width = image.offsetWidth.toString() + "px";
119 | canvas.height = image.offsetHeight;
120 | canvas.style.height = image.offsetHeight.toString() + "px";
121 | document.body.removeChild(image);
122 | var ctx = canvas.getContext("2d");
123 | ctx.drawImage(image, 0, 0);
124 | return canvas;
125 | }
126 | return image;
127 | },
128 |
129 | grayscale : function (canvas) {
130 | var ctx = canvas.getContext("2d");
131 | var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
132 | var data = imageData.data;
133 | var pix1, pix2, pix = canvas.width * canvas.height * 4;
134 | while (pix > 0)
135 | data[pix -= 4] = data[pix1 = pix + 1] = data[pix2 = pix + 2] = (data[pix] * 0.3 + data[pix1] * 0.59 + data[pix2] * 0.11);
136 | ctx.putImageData(imageData, 0, 0);
137 | return canvas;
138 | },
139 |
140 | array_group : function (seq, gfunc) {
141 | var i, j;
142 | var node = new Array(seq.length);
143 | for (i = 0; i < seq.length; i++)
144 | node[i] = {"parent" : -1,
145 | "element" : seq[i],
146 | "rank" : 0};
147 | for (i = 0; i < seq.length; i++) {
148 | if (!node[i].element)
149 | continue;
150 | var root = i;
151 | while (node[root].parent != -1)
152 | root = node[root].parent;
153 | for (j = 0; j < seq.length; j++) {
154 | if( i != j && node[j].element && gfunc(node[i].element, node[j].element)) {
155 | var root2 = j;
156 |
157 | while (node[root2].parent != -1)
158 | root2 = node[root2].parent;
159 |
160 | if(root2 != root) {
161 | if(node[root].rank > node[root2].rank)
162 | node[root2].parent = root;
163 | else {
164 | node[root].parent = root2;
165 | if (node[root].rank == node[root2].rank)
166 | node[root2].rank++;
167 | root = root2;
168 | }
169 |
170 | /* compress path from node2 to the root: */
171 | var temp, node2 = j;
172 | while (node[node2].parent != -1) {
173 | temp = node2;
174 | node2 = node[node2].parent;
175 | node[temp].parent = root;
176 | }
177 |
178 | /* compress path from node to the root: */
179 | node2 = i;
180 | while (node[node2].parent != -1) {
181 | temp = node2;
182 | node2 = node[node2].parent;
183 | node[temp].parent = root;
184 | }
185 | }
186 | }
187 | }
188 | }
189 | var idx = new Array(seq.length);
190 | var class_idx = 0;
191 | for(i = 0; i < seq.length; i++) {
192 | j = -1;
193 | var node1 = i;
194 | if(node[node1].element) {
195 | while (node[node1].parent != -1)
196 | node1 = node[node1].parent;
197 | if(node[node1].rank >= 0)
198 | node[node1].rank = ~class_idx++;
199 | j = ~node[node1].rank;
200 | }
201 | idx[i] = j;
202 | }
203 | return {"index" : idx, "cat" : class_idx};
204 | },
205 |
206 | detect_objects : parallable(scriptPath, function (canvas, cascade, interval, min_neighbors) {
207 | if (this.shared !== undefined) {
208 | var params = get_named_arguments(arguments, ["canvas", "cascade", "interval", "min_neighbors"]);
209 | this.shared.canvas = params.canvas;
210 | this.shared.interval = params.interval;
211 | this.shared.min_neighbors = params.min_neighbors;
212 | this.shared.cascade = params.cascade;
213 | this.shared.scale = Math.pow(2, 1 / (params.interval + 1));
214 | this.shared.next = params.interval + 1;
215 | this.shared.scale_upto = Math.floor(Math.log(Math.min(params.canvas.width / params.cascade.width, params.canvas.height / params.cascade.height)) / Math.log(this.shared.scale));
216 | var i;
217 | for (i = 0; i < this.shared.cascade.stage_classifier.length; i++)
218 | this.shared.cascade.stage_classifier[i].orig_feature = this.shared.cascade.stage_classifier[i].feature;
219 | }
220 |
221 | function pre(worker_num) {
222 | var canvas = this.shared.canvas;
223 | var interval = this.shared.interval;
224 | var scale = this.shared.scale;
225 | var next = this.shared.next;
226 | var scale_upto = this.shared.scale_upto;
227 | var pyr = new Array((scale_upto + next * 2) * 4);
228 | var ret = new Array((scale_upto + next * 2) * 4);
229 | pyr[0] = canvas;
230 | ret[0] = { "width" : pyr[0].width,
231 | "height" : pyr[0].height,
232 | "data" : pyr[0].getContext("2d").getImageData(0, 0, pyr[0].width, pyr[0].height).data };
233 | var i;
234 | for (i = 1; i <= interval; i++) {
235 | pyr[i * 4] = document.createElement("canvas");
236 | pyr[i * 4].width = Math.floor(pyr[0].width / Math.pow(scale, i));
237 | pyr[i * 4].height = Math.floor(pyr[0].height / Math.pow(scale, i));
238 | pyr[i * 4].getContext("2d").drawImage(pyr[0], 0, 0, pyr[0].width, pyr[0].height, 0, 0, pyr[i * 4].width, pyr[i * 4].height);
239 | ret[i * 4] = { "width" : pyr[i * 4].width,
240 | "height" : pyr[i * 4].height,
241 | "data" : pyr[i * 4].getContext("2d").getImageData(0, 0, pyr[i * 4].width, pyr[i * 4].height).data };
242 | }
243 | for (i = next; i < scale_upto + next * 2; i++) {
244 | pyr[i * 4] = document.createElement("canvas");
245 | pyr[i * 4].width = Math.floor(pyr[i * 4 - next * 4].width / 2);
246 | pyr[i * 4].height = Math.floor(pyr[i * 4 - next * 4].height / 2);
247 | pyr[i * 4].getContext("2d").drawImage(pyr[i * 4 - next * 4], 0, 0, pyr[i * 4 - next * 4].width, pyr[i * 4 - next * 4].height, 0, 0, pyr[i * 4].width, pyr[i * 4].height);
248 | ret[i * 4] = { "width" : pyr[i * 4].width,
249 | "height" : pyr[i * 4].height,
250 | "data" : pyr[i * 4].getContext("2d").getImageData(0, 0, pyr[i * 4].width, pyr[i * 4].height).data };
251 | }
252 | for (i = next * 2; i < scale_upto + next * 2; i++) {
253 | pyr[i * 4 + 1] = document.createElement("canvas");
254 | pyr[i * 4 + 1].width = Math.floor(pyr[i * 4 - next * 4].width / 2);
255 | pyr[i * 4 + 1].height = Math.floor(pyr[i * 4 - next * 4].height / 2);
256 | pyr[i * 4 + 1].getContext("2d").drawImage(pyr[i * 4 - next * 4], 1, 0, pyr[i * 4 - next * 4].width - 1, pyr[i * 4 - next * 4].height, 0, 0, pyr[i * 4 + 1].width - 2, pyr[i * 4 + 1].height);
257 | ret[i * 4 + 1] = { "width" : pyr[i * 4 + 1].width,
258 | "height" : pyr[i * 4 + 1].height,
259 | "data" : pyr[i * 4 + 1].getContext("2d").getImageData(0, 0, pyr[i * 4 + 1].width, pyr[i * 4 + 1].height).data };
260 | pyr[i * 4 + 2] = document.createElement("canvas");
261 | pyr[i * 4 + 2].width = Math.floor(pyr[i * 4 - next * 4].width / 2);
262 | pyr[i * 4 + 2].height = Math.floor(pyr[i * 4 - next * 4].height / 2);
263 | pyr[i * 4 + 2].getContext("2d").drawImage(pyr[i * 4 - next * 4], 0, 1, pyr[i * 4 - next * 4].width, pyr[i * 4 - next * 4].height - 1, 0, 0, pyr[i * 4 + 2].width, pyr[i * 4 + 2].height - 2);
264 | ret[i * 4 + 2] = { "width" : pyr[i * 4 + 2].width,
265 | "height" : pyr[i * 4 + 2].height,
266 | "data" : pyr[i * 4 + 2].getContext("2d").getImageData(0, 0, pyr[i * 4 + 2].width, pyr[i * 4 + 2].height).data };
267 | pyr[i * 4 + 3] = document.createElement("canvas");
268 | pyr[i * 4 + 3].width = Math.floor(pyr[i * 4 - next * 4].width / 2);
269 | pyr[i * 4 + 3].height = Math.floor(pyr[i * 4 - next * 4].height / 2);
270 | pyr[i * 4 + 3].getContext("2d").drawImage(pyr[i * 4 - next * 4], 1, 1, pyr[i * 4 - next * 4].width - 1, pyr[i * 4 - next * 4].height - 1, 0, 0, pyr[i * 4 + 3].width - 2, pyr[i * 4 + 3].height - 2);
271 | ret[i * 4 + 3] = { "width" : pyr[i * 4 + 3].width,
272 | "height" : pyr[i * 4 + 3].height,
273 | "data" : pyr[i * 4 + 3].getContext("2d").getImageData(0, 0, pyr[i * 4 + 3].width, pyr[i * 4 + 3].height).data };
274 | }
275 | return [ret];
276 | };
277 |
278 | function core(pyr, id, worker_num) {
279 | var cascade = this.shared.cascade;
280 | var interval = this.shared.interval;
281 | var scale = this.shared.scale;
282 | var next = this.shared.next;
283 | var scale_upto = this.shared.scale_upto;
284 | var i, j, k, x, y, q;
285 | var scale_x = 1, scale_y = 1;
286 | var dx = [0, 1, 0, 1];
287 | var dy = [0, 0, 1, 1];
288 | var seq = [];
289 | for (i = 0; i < scale_upto; i++) {
290 | var qw = pyr[i * 4 + next * 8].width - Math.floor(cascade.width / 4);
291 | var qh = pyr[i * 4 + next * 8].height - Math.floor(cascade.height / 4);
292 | var step = [pyr[i * 4].width * 4, pyr[i * 4 + next * 4].width * 4, pyr[i * 4 + next * 8].width * 4];
293 | var paddings = [pyr[i * 4].width * 16 - qw * 16,
294 | pyr[i * 4 + next * 4].width * 8 - qw * 8,
295 | pyr[i * 4 + next * 8].width * 4 - qw * 4];
296 | for (j = 0; j < cascade.stage_classifier.length; j++) {
297 | var orig_feature = cascade.stage_classifier[j].orig_feature;
298 | var feature = cascade.stage_classifier[j].feature = new Array(cascade.stage_classifier[j].count);
299 | for (k = 0; k < cascade.stage_classifier[j].count; k++) {
300 | feature[k] = {"size" : orig_feature[k].size,
301 | "px" : new Array(orig_feature[k].size),
302 | "pz" : new Array(orig_feature[k].size),
303 | "nx" : new Array(orig_feature[k].size),
304 | "nz" : new Array(orig_feature[k].size)};
305 | for (q = 0; q < orig_feature[k].size; q++) {
306 | feature[k].px[q] = orig_feature[k].px[q] * 4 + orig_feature[k].py[q] * step[orig_feature[k].pz[q]];
307 | feature[k].pz[q] = orig_feature[k].pz[q];
308 | feature[k].nx[q] = orig_feature[k].nx[q] * 4 + orig_feature[k].ny[q] * step[orig_feature[k].nz[q]];
309 | feature[k].nz[q] = orig_feature[k].nz[q];
310 | }
311 | }
312 | }
313 | for (q = 0; q < 4; q++) {
314 | var u8 = [pyr[i * 4].data, pyr[i * 4 + next * 4].data, pyr[i * 4 + next * 8 + q].data];
315 | var u8o = [dx[q] * 8 + dy[q] * pyr[i * 4].width * 8, dx[q] * 4 + dy[q] * pyr[i * 4 + next * 4].width * 4, 0];
316 | for (y = 0; y < qh; y++) {
317 | for (x = 0; x < qw; x++) {
318 | var sum = 0;
319 | var flag = true;
320 | for (j = 0; j < cascade.stage_classifier.length; j++) {
321 | sum = 0;
322 | var alpha = cascade.stage_classifier[j].alpha;
323 | var feature = cascade.stage_classifier[j].feature;
324 | for (k = 0; k < cascade.stage_classifier[j].count; k++) {
325 | var feature_k = feature[k];
326 | var p, pmin = u8[feature_k.pz[0]][u8o[feature_k.pz[0]] + feature_k.px[0]];
327 | var n, nmax = u8[feature_k.nz[0]][u8o[feature_k.nz[0]] + feature_k.nx[0]];
328 | if (pmin <= nmax) {
329 | sum += alpha[k * 2];
330 | } else {
331 | var f, shortcut = true;
332 | for (f = 0; f < feature_k.size; f++) {
333 | if (feature_k.pz[f] >= 0) {
334 | p = u8[feature_k.pz[f]][u8o[feature_k.pz[f]] + feature_k.px[f]];
335 | if (p < pmin) {
336 | if (p <= nmax) {
337 | shortcut = false;
338 | break;
339 | }
340 | pmin = p;
341 | }
342 | }
343 | if (feature_k.nz[f] >= 0) {
344 | n = u8[feature_k.nz[f]][u8o[feature_k.nz[f]] + feature_k.nx[f]];
345 | if (n > nmax) {
346 | if (pmin <= n) {
347 | shortcut = false;
348 | break;
349 | }
350 | nmax = n;
351 | }
352 | }
353 | }
354 | sum += (shortcut) ? alpha[k * 2 + 1] : alpha[k * 2];
355 | }
356 | }
357 | if (sum < cascade.stage_classifier[j].threshold) {
358 | flag = false;
359 | break;
360 | }
361 | }
362 | if (flag) {
363 | seq.push({"x" : (x * 4 + dx[q] * 2) * scale_x,
364 | "y" : (y * 4 + dy[q] * 2) * scale_y,
365 | "width" : cascade.width * scale_x,
366 | "height" : cascade.height * scale_y,
367 | "neighbor" : 1,
368 | "confidence" : sum});
369 | }
370 | u8o[0] += 16;
371 | u8o[1] += 8;
372 | u8o[2] += 4;
373 | }
374 | u8o[0] += paddings[0];
375 | u8o[1] += paddings[1];
376 | u8o[2] += paddings[2];
377 | }
378 | }
379 | scale_x *= scale;
380 | scale_y *= scale;
381 | }
382 | return seq;
383 | };
384 |
385 | function post(seq) {
386 | var min_neighbors = this.shared.min_neighbors;
387 | var cascade = this.shared.cascade;
388 | var interval = this.shared.interval;
389 | var scale = this.shared.scale;
390 | var next = this.shared.next;
391 | var scale_upto = this.shared.scale_upto;
392 | var i, j;
393 | for (i = 0; i < cascade.stage_classifier.length; i++)
394 | cascade.stage_classifier[i].feature = cascade.stage_classifier[i].orig_feature;
395 | seq = seq[0];
396 | if (!(min_neighbors > 0))
397 | return seq;
398 | else {
399 | var result = ccv.array_group(seq, function (r1, r2) {
400 | var distance = Math.floor(r1.width * 0.25 + 0.5);
401 |
402 | return r2.x <= r1.x + distance &&
403 | r2.x >= r1.x - distance &&
404 | r2.y <= r1.y + distance &&
405 | r2.y >= r1.y - distance &&
406 | r2.width <= Math.floor(r1.width * 1.5 + 0.5) &&
407 | Math.floor(r2.width * 1.5 + 0.5) >= r1.width;
408 | });
409 | var ncomp = result.cat;
410 | var idx_seq = result.index;
411 | var comps = new Array(ncomp + 1);
412 | for (i = 0; i < comps.length; i++)
413 | comps[i] = {"neighbors" : 0,
414 | "x" : 0,
415 | "y" : 0,
416 | "width" : 0,
417 | "height" : 0,
418 | "confidence" : 0};
419 |
420 | // count number of neighbors
421 | for(i = 0; i < seq.length; i++)
422 | {
423 | var r1 = seq[i];
424 | var idx = idx_seq[i];
425 |
426 | if (comps[idx].neighbors == 0)
427 | comps[idx].confidence = r1.confidence;
428 |
429 | ++comps[idx].neighbors;
430 |
431 | comps[idx].x += r1.x;
432 | comps[idx].y += r1.y;
433 | comps[idx].width += r1.width;
434 | comps[idx].height += r1.height;
435 | comps[idx].confidence = Math.max(comps[idx].confidence, r1.confidence);
436 | }
437 |
438 | var seq2 = [];
439 | // calculate average bounding box
440 | for(i = 0; i < ncomp; i++)
441 | {
442 | var n = comps[i].neighbors;
443 | if (n >= min_neighbors)
444 | seq2.push({"x" : (comps[i].x * 2 + n) / (2 * n),
445 | "y" : (comps[i].y * 2 + n) / (2 * n),
446 | "width" : (comps[i].width * 2 + n) / (2 * n),
447 | "height" : (comps[i].height * 2 + n) / (2 * n),
448 | "neighbors" : comps[i].neighbors,
449 | "confidence" : comps[i].confidence});
450 | }
451 |
452 | var result_seq = [];
453 | // filter out small face rectangles inside large face rectangles
454 | for(i = 0; i < seq2.length; i++)
455 | {
456 | var r1 = seq2[i];
457 | var flag = true;
458 | for(j = 0; j < seq2.length; j++)
459 | {
460 | var r2 = seq2[j];
461 | var distance = Math.floor(r2.width * 0.25 + 0.5);
462 |
463 | if(i != j &&
464 | r1.x >= r2.x - distance &&
465 | r1.y >= r2.y - distance &&
466 | r1.x + r1.width <= r2.x + r2.width + distance &&
467 | r1.y + r1.height <= r2.y + r2.height + distance &&
468 | (r2.neighbors > Math.max(3, r1.neighbors) || r1.neighbors < 3))
469 | {
470 | flag = false;
471 | break;
472 | }
473 | }
474 |
475 | if(flag)
476 | result_seq.push(r1);
477 | }
478 | return result_seq;
479 | }
480 | };
481 | return { "pre" : pre, "core" : core, "post" : post };
482 | })
483 | }
484 |
485 | // Monkey patch to avoid overriding window's onmessage handler.
486 | var originalOnMessage = window.onmessage || function () {};
487 | onmessage = function (event) {
488 | var data;
489 | try {
490 | data = (typeof event.data == "string") ? JSON.parse(event.data) : event.data;
491 | if (data.type === "jquery.facedetection") {
492 | // This is the event that is intended for jquery.facedetection
493 | var scope = { "shared" : data.shared };
494 | var result = parallable.core[data.name].apply(scope, [data.input, data.id, data.worker]);
495 | try {
496 | postMessage(result);
497 | } catch (e) {
498 | postMessage(JSON.stringify(result));
499 | }
500 | } else {
501 | // Nope. This is not the event that should be handled by jquery.facedetection
502 | var args = Array.prototype.slice.call(arguments);
503 | originalOnMessage.apply(window, args);
504 | }
505 | } catch (e) {
506 | // `event.data` is string, but too bad it is not in JSON format.
507 | // so just pass it to window's original onmessage handler
508 | var args = Array.prototype.slice.call(arguments);
509 | originalOnMessage.apply(window, args);
510 | }
511 | }
512 |
--------------------------------------------------------------------------------
/examples/img/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------