├── LICENSE.txt
├── README.md
├── samples
├── demo
│ ├── cv.js
│ ├── demo.html
│ ├── handtracking.js
│ └── libs
│ │ └── polyfill.js
└── fast_demo
│ ├── cv.js
│ ├── fast_demo.html
│ ├── handtracking.js
│ └── libs
│ └── polyfill.js
└── src
├── cv.js
└── handtracking.js
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Juan Mellado
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
21 |
22 | OpenCV
23 | ======
24 | IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
25 |
26 | By downloading, copying, installing or using the software you agree to this license.
27 | If you do not agree to this license, do not download, install,
28 | copy or use the software.
29 |
30 |
31 | License Agreement
32 | For Open Source Computer Vision Library
33 |
34 | Copyright (C) 2000-2008, Intel Corporation, all rights reserved.
35 | Copyright (C) 2009, Willow Garage Inc., all rights reserved.
36 | Third party copyrights are property of their respective owners.
37 |
38 | Redistribution and use in source and binary forms, with or without modification,
39 | are permitted provided that the following conditions are met:
40 |
41 | * Redistribution's of source code must retain the above copyright notice,
42 | this list of conditions and the following disclaimer.
43 |
44 | * Redistribution's in binary form must reproduce the above copyright notice,
45 | this list of conditions and the following disclaimer in the documentation
46 | and/or other materials provided with the distribution.
47 |
48 | * The name of the copyright holders may not be used to endorse or promote products
49 | derived from this software without specific prior written permission.
50 |
51 | This software is provided by the copyright holders and contributors "as is" and
52 | any express or implied warranties, including, but not limited to, the implied
53 | warranties of merchantability and fitness for a particular purpose are disclaimed.
54 | In no event shall the Intel Corporation or contributors be liable for any direct,
55 | indirect, incidental, special, exemplary, or consequential damages
56 | (including, but not limited to, procurement of substitute goods or services;
57 | loss of use, data, or profits; or business interruption) however caused
58 | and on any theory of liability, whether in contract, strict liability,
59 | or tort (including negligence or otherwise) arising in any way out of
60 | the use of this software, even if advised of the possibility of such damage.
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | JavaScript library for hand tracking applications featuring:
2 |
3 | * Skin detection
4 | * Erode / Dilate operations
5 | * Contour extraction
6 | * Contour optimization
7 | * Convex Hull calculation
8 | * Convexity Defects calculation
9 |
10 | ### Demo ###
11 | [Basic Demo](https://jcmellado.github.io/showcase/js/js-handtracking/demo/index.html), webcam basic demo
12 |
13 | [Fast Demo](https://jcmellado.github.io/showcase/js/js-handtracking/fast_demo/index.html), erode and dilate operations are disabled
14 |
15 | ### Video ###
16 |
17 | [js-handtracking](https://jcmellado.github.io/showcase/videos/js-handtracking_%20JavaScript%20Hand%20Tracking.mp4)
18 |
19 | ### Usage ###
20 | Create one `HT.Tracker` object:
21 |
22 | ```
23 | var tracker = new HT.Tracker();
24 | ```
25 |
26 | Call `detect` function:
27 |
28 | ```
29 | var candidate = tracker.detect(imageData);
30 | ```
31 |
32 | `imageData` argument must be a valid `ImageData` canvas object.
33 |
34 | `candidate` result (if any) will be a `HT.Candidate` object with the following properties:
35 |
36 | * `contour`: Optimized contour as a plain array of two dimensional vectors
37 | * `hull`: Convex hull as a plain array of two dimensional vectors
38 | * `defects`: Convexity defects as a plain array of objects
39 |
40 | `defects` objects have the following properties:
41 |
42 | * `start`: Start point of hull segment as a two dimensional vector
43 | * `end`: End point of hull segment as a two dimensional vector
44 | * `depthPoint`: Deeper defect point as a two dimensional vector
45 | * `depth`: Minimum distance from hull segment to deeper defect point
46 |
47 | ### Skin detection ###
48 | The library converts RGB images to HSV one. V and H channels are used to characterize the colors range for skin detection:
49 |
50 | ```
51 | v >= 15 and v <= 250
52 |
53 | h >= 3 and h <= 33
54 | ```
55 |
56 | Note that source alpha channel is ignored.
57 |
--------------------------------------------------------------------------------
/samples/demo/cv.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2012 Juan Mellado
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 | */
22 |
23 | /*
24 | References:
25 | - "OpenCV: Open Computer Vision Library"
26 | http://sourceforge.net/projects/opencvlibrary/
27 | */
28 |
29 | var CV = CV || {};
30 |
31 | CV.Image = function(width, height, data){
32 | this.width = width || 0;
33 | this.height = height || 0;
34 | this.data = data || [];
35 | };
36 |
37 | CV.findContours = function(imageSrc){
38 | var contours = [], src = imageSrc.data,
39 | width = imageSrc.width - 2, height = imageSrc.height - 2,
40 | pos = width + 3, nbd = 1,
41 | deltas, pix, outer, hole, i, j;
42 |
43 | deltas = CV.neighborhoodDeltas(width + 2);
44 |
45 | for (i = 0; i < height; ++ i, pos += 2){
46 |
47 | for (j = 0; j < width; ++ j, ++ pos){
48 | pix = src[pos];
49 |
50 | if (0 !== pix){
51 | outer = hole = false;
52 |
53 | if (1 === pix && 0 === src[pos - 1]){
54 | outer = true;
55 | }
56 | else if (pix >= 1 && 0 === src[pos + 1]){
57 | hole = true;
58 | }
59 |
60 | if (outer || hole){
61 | ++ nbd;
62 |
63 | contours.push( CV.borderFollowing(src, pos, nbd,
64 | {x: j, y: i}, hole, deltas) );
65 | }
66 | }
67 | }
68 | }
69 |
70 | return contours;
71 | };
72 |
73 | CV.borderFollowing = function(src, pos, nbd, point, hole, deltas){
74 | var contour = [], pos1, pos3, pos4, s, s_end, s_prev;
75 |
76 | contour.hole = hole;
77 |
78 | s = s_end = hole? 0: 4;
79 | do{
80 | s = (s - 1) & 7;
81 | pos1 = pos + deltas[s];
82 | if (src[pos1] !== 0){
83 | break;
84 | }
85 | }while(s !== s_end);
86 |
87 | if (s === s_end){
88 | src[pos] = -nbd;
89 | contour.push( {x: point.x, y: point.y} );
90 |
91 | }else{
92 | pos3 = pos;
93 | s_prev = s ^ 4;
94 |
95 | while(true){
96 | s_end = s;
97 |
98 | do{
99 | pos4 = pos3 + deltas[++ s];
100 | }while(src[pos4] === 0);
101 |
102 | s &= 7;
103 |
104 | if ( ( (s - 1) >>> 0) < (s_end >>> 0) ){
105 | src[pos3] = -nbd;
106 | }
107 | else if (src[pos3] === 1){
108 | src[pos3] = nbd;
109 | }
110 |
111 | contour.push( {x: point.x, y: point.y} );
112 |
113 | s_prev = s;
114 |
115 | point.x += CV.neighborhood[s][0];
116 | point.y += CV.neighborhood[s][1];
117 |
118 | if ( (pos4 === pos) && (pos3 === pos1) ){
119 | break;
120 | }
121 |
122 | pos3 = pos4;
123 | s = (s + 4) & 7;
124 | }
125 | }
126 |
127 | return contour;
128 | };
129 |
130 | CV.neighborhood =
131 | [ [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1] ];
132 |
133 | CV.neighborhoodDeltas = function(width){
134 | var deltas = [], len = CV.neighborhood.length, i = 0;
135 |
136 | for (; i < len; ++ i){
137 | deltas[i] = CV.neighborhood[i][0] + (CV.neighborhood[i][1] * width);
138 | }
139 |
140 | return deltas.concat(deltas);
141 | };
142 |
143 | CV.approxPolyDP = function(contour, epsilon){
144 | var slice = {start_index: 0, end_index: 0},
145 | right_slice = {start_index: 0, end_index: 0},
146 | poly = [], stack = [], len = contour.length,
147 | pt, start_pt, end_pt, dist, max_dist, le_eps,
148 | dx, dy, i, j, k;
149 |
150 | epsilon *= epsilon;
151 |
152 | k = 0;
153 |
154 | for (i = 0; i < 3; ++ i){
155 | max_dist = 0;
156 |
157 | k = (k + right_slice.start_index) % len;
158 | start_pt = contour[k];
159 | if (++ k === len) {k = 0;}
160 |
161 | for (j = 1; j < len; ++ j){
162 | pt = contour[k];
163 | if (++ k === len) {k = 0;}
164 |
165 | dx = pt.x - start_pt.x;
166 | dy = pt.y - start_pt.y;
167 | dist = dx * dx + dy * dy;
168 |
169 | if (dist > max_dist){
170 | max_dist = dist;
171 | right_slice.start_index = j;
172 | }
173 | }
174 | }
175 |
176 | if (max_dist <= epsilon){
177 | poly.push( {x: start_pt.x, y: start_pt.y} );
178 |
179 | }else{
180 | slice.start_index = k;
181 | slice.end_index = (right_slice.start_index += slice.start_index);
182 |
183 | right_slice.start_index -= right_slice.start_index >= len? len: 0;
184 | right_slice.end_index = slice.start_index;
185 | if (right_slice.end_index < right_slice.start_index){
186 | right_slice.end_index += len;
187 | }
188 |
189 | stack.push( {start_index: right_slice.start_index, end_index: right_slice.end_index} );
190 | stack.push( {start_index: slice.start_index, end_index: slice.end_index} );
191 | }
192 |
193 | while(stack.length !== 0){
194 | slice = stack.pop();
195 |
196 | end_pt = contour[slice.end_index % len];
197 | start_pt = contour[k = slice.start_index % len];
198 | if (++ k === len) {k = 0;}
199 |
200 | if (slice.end_index <= slice.start_index + 1){
201 | le_eps = true;
202 |
203 | }else{
204 | max_dist = 0;
205 |
206 | dx = end_pt.x - start_pt.x;
207 | dy = end_pt.y - start_pt.y;
208 |
209 | for (i = slice.start_index + 1; i < slice.end_index; ++ i){
210 | pt = contour[k];
211 | if (++ k === len) {k = 0;}
212 |
213 | dist = Math.abs( (pt.y - start_pt.y) * dx - (pt.x - start_pt.x) * dy);
214 |
215 | if (dist > max_dist){
216 | max_dist = dist;
217 | right_slice.start_index = i;
218 | }
219 | }
220 |
221 | le_eps = max_dist * max_dist <= epsilon * (dx * dx + dy * dy);
222 | }
223 |
224 | if (le_eps){
225 | poly.push( {x: start_pt.x, y: start_pt.y} );
226 |
227 | }else{
228 | right_slice.end_index = slice.end_index;
229 | slice.end_index = right_slice.start_index;
230 |
231 | stack.push( {start_index: right_slice.start_index, end_index: right_slice.end_index} );
232 | stack.push( {start_index: slice.start_index, end_index: slice.end_index} );
233 | }
234 | }
235 |
236 | return poly;
237 | };
238 |
239 | CV.erode = function(imageSrc, imageDst){
240 | return CV.applyKernel(imageSrc, imageDst, Math.min);
241 | };
242 |
243 | CV.dilate = function(imageSrc, imageDst){
244 | return CV.applyKernel(imageSrc, imageDst, Math.max);
245 | };
246 |
247 | CV.applyKernel = function(imageSrc, imageDst, fn){
248 | var src = imageSrc.data, dst = imageDst.data,
249 | width = imageSrc.width, height = imageSrc.height,
250 | offsets = [-width - 1, -width, -width + 1, -1, 1, width - 1, width, width + 1],
251 | klen = offsets.length,
252 | pos = 0, value, i, j, k;
253 |
254 | for (i = 0; i < width; ++ i){
255 | dst[pos ++] = 0;
256 | }
257 |
258 | for (i = 2; i < height; ++ i){
259 | dst[pos ++] = 0;
260 |
261 | for (j = 2; j < width; ++ j){
262 | value = src[pos];
263 |
264 | for (k = 0; k < klen; ++ k){
265 | value = fn(value, src[ pos + offsets[k] ] );
266 | }
267 |
268 | dst[pos ++] = value;
269 | }
270 |
271 | dst[pos ++] = 0;
272 | }
273 |
274 | for (i = 0; i < width; ++ i){
275 | dst[pos ++] = 0;
276 | }
277 |
278 | imageDst.width = imageSrc.width;
279 | imageDst.height = imageSrc.height;
280 |
281 | return imageDst;
282 | };
283 |
284 | CV.convexHull = function(points){
285 | var deque = [], i = 3, point;
286 |
287 | if (points.length >= 3){
288 |
289 | if ( CV.position(points[0], points[1], points[2]) > 0){
290 | deque.push(points[0]);
291 | deque.push(points[1]);
292 | }else{
293 | deque.push(points[1]);
294 | deque.push(points[0]);
295 | }
296 | deque.push(points[2]);
297 | deque.unshift(points[2]);
298 |
299 | for (; i < points.length; ++ i){
300 | point = points[i];
301 |
302 | if ( CV.position(point, deque[0], deque[1]) < 0 ||
303 | CV.position(deque[deque.length - 2], deque[deque.length - 1], point) < 0 ){
304 |
305 | while( CV.position(deque[deque.length - 2], deque[deque.length - 1], point) <= 0 ){
306 | deque.pop();
307 | }
308 | deque.push(point);
309 |
310 | while( CV.position(point, deque[0], deque[1]) <= 0 ){
311 | deque.shift();
312 | }
313 | deque.unshift(point);
314 | }
315 | }
316 |
317 | }
318 |
319 | return deque;
320 | };
321 |
322 | CV.position = function(p1, p2, p3){
323 | return ( (p2.x - p1.x) * (p3.y - p1.y) ) - ( (p3.x - p1.x) * (p2.y - p1.y) );
324 | };
325 |
326 | CV.convexityDefects = function(points, hull){
327 | var defects = [], len = hull.length,
328 | curr, next, point, dx0, dy0, scale, defect, isDefect,
329 | idx1, idx2, idx3, sign, inc, depth, dx, dy, dist, i, j;
330 |
331 | if (len >= 3){
332 | idx1 = CV.indexPoint(points, hull[0]);
333 | idx2 = CV.indexPoint(points, hull[1]);
334 | idx3 = CV.indexPoint(points, hull[2]);
335 |
336 | sign = 0;
337 | sign += idx2 > idx1? 1: 0;
338 | sign += idx3 > idx2? 1: 0;
339 | sign += idx1 > idx3? 1: 0;
340 |
341 | inc = (sign === 2)? 1: -1;
342 |
343 | j = idx1;
344 | curr = hull[0];
345 |
346 | for (i = 1; i !== len; ++ i){
347 | next = hull[i];
348 | isDefect = false;
349 | depth = 0;
350 |
351 | dx0 = next.x - curr.x;
352 | dy0 = next.y - curr.y;
353 | scale = 1 / Math.sqrt(dx0 * dx0 + dy0 * dy0);
354 |
355 | defect = {start: curr, end: next};
356 |
357 | while(true){
358 | j += inc;
359 | j = (j < 0)? points.length - 1: j % points.length;
360 |
361 | point = points[j];
362 |
363 | if (point.x === next.x && point.y === next.y){
364 | break;
365 | }
366 |
367 | dx = point.x - curr.x;
368 | dy = point.y - curr.y;
369 | dist = Math.abs(-dy0 * dx + dx0 * dy) * scale;
370 |
371 | if (dist > depth){
372 | isDefect = true;
373 |
374 | defect.depth = depth = dist;
375 | defect.depthPoint = point;
376 | }
377 | }
378 |
379 | if (isDefect){
380 | defects.push(defect);
381 | }
382 |
383 | curr = next;
384 | }
385 | }
386 |
387 | return defects;
388 | };
389 |
390 | CV.indexPoint = function(points, point){
391 | var len = points.length, i = 0;
392 | for (; i < len; ++ i){
393 | if (points[i].x === point.x && points[i].y === point.y){
394 | break;
395 | }
396 | }
397 | return i;
398 | };
399 |
400 | CV.area = function(poly){
401 | var area = 0, len = poly.length, i = 1,
402 | x, y, xmin, xmax, ymin, ymax;
403 |
404 | if (len > 0){
405 | xmin = xmax = poly[0].x;
406 | ymin = ymax = poly[0].y;
407 |
408 | for (; i < len; ++ i){
409 | x = poly[i].x;
410 | if (x < xmin){
411 | xmin = x;
412 | }
413 | if (x > xmax){
414 | xmax = x;
415 | }
416 |
417 | y = poly[i].y;
418 | if (y < ymin){
419 | ymin = y;
420 | }
421 | if (y > ymax){
422 | ymax = y;
423 | }
424 | }
425 |
426 | area = (xmax - xmin + 1) * (ymax - ymin + 1);
427 | }
428 |
429 | return area;
430 | };
431 |
--------------------------------------------------------------------------------
/samples/demo/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hand Tracking
5 |
6 |
7 |
8 |
9 |
10 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | -= Hand Tracking =-
177 |
178 |
179 |
180 | Convex Hull
181 | Convexity Defects
182 | Skin Detection
183 |
184 |
185 |
186 |
187 |
188 |
189 |
--------------------------------------------------------------------------------
/samples/demo/handtracking.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2012 Juan Mellado
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 | */
22 |
23 | var HT = HT || {};
24 |
25 | HT.Tracker = function(params){
26 | this.params = params || {};
27 |
28 | this.mask = new CV.Image();
29 | this.eroded = new CV.Image();
30 | this.contours = [];
31 |
32 | this.skinner = new HT.Skinner();
33 | };
34 |
35 | HT.Tracker.prototype.detect = function(image){
36 | this.skinner.mask(image, this.mask);
37 |
38 | if (this.params.fast){
39 | this.blackBorder(this.mask);
40 | }else{
41 | CV.erode(this.mask, this.eroded);
42 | CV.dilate(this.eroded, this.mask);
43 | }
44 |
45 | this.contours = CV.findContours(this.mask);
46 |
47 | return this.findCandidate(this.contours, image.width * image.height * 0.05, 0.005);
48 | };
49 |
50 | HT.Tracker.prototype.findCandidate = function(contours, minSize, epsilon){
51 | var contour, candidate;
52 |
53 | contour = this.findMaxArea(contours, minSize);
54 | if (contour){
55 | contour = CV.approxPolyDP(contour, contour.length * epsilon);
56 |
57 | candidate = new HT.Candidate(contour);
58 | }
59 |
60 | return candidate;
61 | };
62 |
63 | HT.Tracker.prototype.findMaxArea = function(contours, minSize){
64 | var len = contours.length, i = 0,
65 | maxArea = -Infinity, area, contour;
66 |
67 | for (; i < len; ++ i){
68 | area = CV.area(contours[i]);
69 | if (area >= minSize){
70 |
71 | if (area > maxArea) {
72 | maxArea = area;
73 |
74 | contour = contours[i];
75 | }
76 | }
77 | }
78 |
79 | return contour;
80 | };
81 |
82 | HT.Tracker.prototype.blackBorder = function(image){
83 | var img = image.data, width = image.width, height = image.height,
84 | pos = 0, i;
85 |
86 | for (i = 0; i < width; ++ i){
87 | img[pos ++] = 0;
88 | }
89 |
90 | for (i = 2; i < height; ++ i){
91 | img[pos] = img[pos + width - 1] = 0;
92 |
93 | pos += width;
94 | }
95 |
96 | for (i = 0; i < width; ++ i){
97 | img[pos ++] = 0;
98 | }
99 |
100 | return image;
101 | };
102 |
103 | HT.Candidate = function(contour){
104 | this.contour = contour;
105 | this.hull = CV.convexHull(contour);
106 | this.defects = CV.convexityDefects(contour, this.hull);
107 | };
108 |
109 | HT.Skinner = function(){
110 | };
111 |
112 | HT.Skinner.prototype.mask = function(imageSrc, imageDst){
113 | var src = imageSrc.data, dst = imageDst.data, len = src.length,
114 | i = 0, j = 0,
115 | r, g, b, h, s, v, value;
116 |
117 | for(; i < len; i += 4){
118 | r = src[i];
119 | g = src[i + 1];
120 | b = src[i + 2];
121 |
122 | v = Math.max(r, g, b);
123 | s = v === 0? 0: 255 * ( v - Math.min(r, g, b) ) / v;
124 | h = 0;
125 |
126 | if (0 !== s){
127 | if (v === r){
128 | h = 30 * (g - b) / s;
129 | }else if (v === g){
130 | h = 60 + ( (b - r) / s);
131 | }else{
132 | h = 120 + ( (r - g) / s);
133 | }
134 | if (h < 0){
135 | h += 360;
136 | }
137 | }
138 |
139 | value = 0;
140 |
141 | if (v >= 15 && v <= 250){
142 | if (h >= 3 && h <= 33){
143 | value = 255;
144 | }
145 | }
146 |
147 | dst[j ++] = value;
148 | }
149 |
150 | imageDst.width = imageSrc.width;
151 | imageDst.height = imageSrc.height;
152 |
153 | return imageDst;
154 | };
155 |
--------------------------------------------------------------------------------
/samples/demo/libs/polyfill.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jcmellado/js-handtracking/0e6f06f64aba04020b71cfae79c16cdf697a81f9/samples/demo/libs/polyfill.js
--------------------------------------------------------------------------------
/samples/fast_demo/cv.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2012 Juan Mellado
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 | */
22 |
23 | /*
24 | References:
25 | - "OpenCV: Open Computer Vision Library"
26 | http://sourceforge.net/projects/opencvlibrary/
27 | */
28 |
29 | var CV = CV || {};
30 |
31 | CV.Image = function(width, height, data){
32 | this.width = width || 0;
33 | this.height = height || 0;
34 | this.data = data || [];
35 | };
36 |
37 | CV.findContours = function(imageSrc){
38 | var contours = [], src = imageSrc.data,
39 | width = imageSrc.width - 2, height = imageSrc.height - 2,
40 | pos = width + 3, nbd = 1,
41 | deltas, pix, outer, hole, i, j;
42 |
43 | deltas = CV.neighborhoodDeltas(width + 2);
44 |
45 | for (i = 0; i < height; ++ i, pos += 2){
46 |
47 | for (j = 0; j < width; ++ j, ++ pos){
48 | pix = src[pos];
49 |
50 | if (0 !== pix){
51 | outer = hole = false;
52 |
53 | if (1 === pix && 0 === src[pos - 1]){
54 | outer = true;
55 | }
56 | else if (pix >= 1 && 0 === src[pos + 1]){
57 | hole = true;
58 | }
59 |
60 | if (outer || hole){
61 | ++ nbd;
62 |
63 | contours.push( CV.borderFollowing(src, pos, nbd,
64 | {x: j, y: i}, hole, deltas) );
65 | }
66 | }
67 | }
68 | }
69 |
70 | return contours;
71 | };
72 |
73 | CV.borderFollowing = function(src, pos, nbd, point, hole, deltas){
74 | var contour = [], pos1, pos3, pos4, s, s_end, s_prev;
75 |
76 | contour.hole = hole;
77 |
78 | s = s_end = hole? 0: 4;
79 | do{
80 | s = (s - 1) & 7;
81 | pos1 = pos + deltas[s];
82 | if (src[pos1] !== 0){
83 | break;
84 | }
85 | }while(s !== s_end);
86 |
87 | if (s === s_end){
88 | src[pos] = -nbd;
89 | contour.push( {x: point.x, y: point.y} );
90 |
91 | }else{
92 | pos3 = pos;
93 | s_prev = s ^ 4;
94 |
95 | while(true){
96 | s_end = s;
97 |
98 | do{
99 | pos4 = pos3 + deltas[++ s];
100 | }while(src[pos4] === 0);
101 |
102 | s &= 7;
103 |
104 | if ( ( (s - 1) >>> 0) < (s_end >>> 0) ){
105 | src[pos3] = -nbd;
106 | }
107 | else if (src[pos3] === 1){
108 | src[pos3] = nbd;
109 | }
110 |
111 | contour.push( {x: point.x, y: point.y} );
112 |
113 | s_prev = s;
114 |
115 | point.x += CV.neighborhood[s][0];
116 | point.y += CV.neighborhood[s][1];
117 |
118 | if ( (pos4 === pos) && (pos3 === pos1) ){
119 | break;
120 | }
121 |
122 | pos3 = pos4;
123 | s = (s + 4) & 7;
124 | }
125 | }
126 |
127 | return contour;
128 | };
129 |
130 | CV.neighborhood =
131 | [ [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1] ];
132 |
133 | CV.neighborhoodDeltas = function(width){
134 | var deltas = [], len = CV.neighborhood.length, i = 0;
135 |
136 | for (; i < len; ++ i){
137 | deltas[i] = CV.neighborhood[i][0] + (CV.neighborhood[i][1] * width);
138 | }
139 |
140 | return deltas.concat(deltas);
141 | };
142 |
143 | CV.approxPolyDP = function(contour, epsilon){
144 | var slice = {start_index: 0, end_index: 0},
145 | right_slice = {start_index: 0, end_index: 0},
146 | poly = [], stack = [], len = contour.length,
147 | pt, start_pt, end_pt, dist, max_dist, le_eps,
148 | dx, dy, i, j, k;
149 |
150 | epsilon *= epsilon;
151 |
152 | k = 0;
153 |
154 | for (i = 0; i < 3; ++ i){
155 | max_dist = 0;
156 |
157 | k = (k + right_slice.start_index) % len;
158 | start_pt = contour[k];
159 | if (++ k === len) {k = 0;}
160 |
161 | for (j = 1; j < len; ++ j){
162 | pt = contour[k];
163 | if (++ k === len) {k = 0;}
164 |
165 | dx = pt.x - start_pt.x;
166 | dy = pt.y - start_pt.y;
167 | dist = dx * dx + dy * dy;
168 |
169 | if (dist > max_dist){
170 | max_dist = dist;
171 | right_slice.start_index = j;
172 | }
173 | }
174 | }
175 |
176 | if (max_dist <= epsilon){
177 | poly.push( {x: start_pt.x, y: start_pt.y} );
178 |
179 | }else{
180 | slice.start_index = k;
181 | slice.end_index = (right_slice.start_index += slice.start_index);
182 |
183 | right_slice.start_index -= right_slice.start_index >= len? len: 0;
184 | right_slice.end_index = slice.start_index;
185 | if (right_slice.end_index < right_slice.start_index){
186 | right_slice.end_index += len;
187 | }
188 |
189 | stack.push( {start_index: right_slice.start_index, end_index: right_slice.end_index} );
190 | stack.push( {start_index: slice.start_index, end_index: slice.end_index} );
191 | }
192 |
193 | while(stack.length !== 0){
194 | slice = stack.pop();
195 |
196 | end_pt = contour[slice.end_index % len];
197 | start_pt = contour[k = slice.start_index % len];
198 | if (++ k === len) {k = 0;}
199 |
200 | if (slice.end_index <= slice.start_index + 1){
201 | le_eps = true;
202 |
203 | }else{
204 | max_dist = 0;
205 |
206 | dx = end_pt.x - start_pt.x;
207 | dy = end_pt.y - start_pt.y;
208 |
209 | for (i = slice.start_index + 1; i < slice.end_index; ++ i){
210 | pt = contour[k];
211 | if (++ k === len) {k = 0;}
212 |
213 | dist = Math.abs( (pt.y - start_pt.y) * dx - (pt.x - start_pt.x) * dy);
214 |
215 | if (dist > max_dist){
216 | max_dist = dist;
217 | right_slice.start_index = i;
218 | }
219 | }
220 |
221 | le_eps = max_dist * max_dist <= epsilon * (dx * dx + dy * dy);
222 | }
223 |
224 | if (le_eps){
225 | poly.push( {x: start_pt.x, y: start_pt.y} );
226 |
227 | }else{
228 | right_slice.end_index = slice.end_index;
229 | slice.end_index = right_slice.start_index;
230 |
231 | stack.push( {start_index: right_slice.start_index, end_index: right_slice.end_index} );
232 | stack.push( {start_index: slice.start_index, end_index: slice.end_index} );
233 | }
234 | }
235 |
236 | return poly;
237 | };
238 |
239 | CV.erode = function(imageSrc, imageDst){
240 | return CV.applyKernel(imageSrc, imageDst, Math.min);
241 | };
242 |
243 | CV.dilate = function(imageSrc, imageDst){
244 | return CV.applyKernel(imageSrc, imageDst, Math.max);
245 | };
246 |
247 | CV.applyKernel = function(imageSrc, imageDst, fn){
248 | var src = imageSrc.data, dst = imageDst.data,
249 | width = imageSrc.width, height = imageSrc.height,
250 | offsets = [-width - 1, -width, -width + 1, -1, 1, width - 1, width, width + 1],
251 | klen = offsets.length,
252 | pos = 0, value, i, j, k;
253 |
254 | for (i = 0; i < width; ++ i){
255 | dst[pos ++] = 0;
256 | }
257 |
258 | for (i = 2; i < height; ++ i){
259 | dst[pos ++] = 0;
260 |
261 | for (j = 2; j < width; ++ j){
262 | value = src[pos];
263 |
264 | for (k = 0; k < klen; ++ k){
265 | value = fn(value, src[ pos + offsets[k] ] );
266 | }
267 |
268 | dst[pos ++] = value;
269 | }
270 |
271 | dst[pos ++] = 0;
272 | }
273 |
274 | for (i = 0; i < width; ++ i){
275 | dst[pos ++] = 0;
276 | }
277 |
278 | imageDst.width = imageSrc.width;
279 | imageDst.height = imageSrc.height;
280 |
281 | return imageDst;
282 | };
283 |
284 | CV.convexHull = function(points){
285 | var deque = [], i = 3, point;
286 |
287 | if (points.length >= 3){
288 |
289 | if ( CV.position(points[0], points[1], points[2]) > 0){
290 | deque.push(points[0]);
291 | deque.push(points[1]);
292 | }else{
293 | deque.push(points[1]);
294 | deque.push(points[0]);
295 | }
296 | deque.push(points[2]);
297 | deque.unshift(points[2]);
298 |
299 | for (; i < points.length; ++ i){
300 | point = points[i];
301 |
302 | if ( CV.position(point, deque[0], deque[1]) < 0 ||
303 | CV.position(deque[deque.length - 2], deque[deque.length - 1], point) < 0 ){
304 |
305 | while( CV.position(deque[deque.length - 2], deque[deque.length - 1], point) <= 0 ){
306 | deque.pop();
307 | }
308 | deque.push(point);
309 |
310 | while( CV.position(point, deque[0], deque[1]) <= 0 ){
311 | deque.shift();
312 | }
313 | deque.unshift(point);
314 | }
315 | }
316 |
317 | }
318 |
319 | return deque;
320 | };
321 |
322 | CV.position = function(p1, p2, p3){
323 | return ( (p2.x - p1.x) * (p3.y - p1.y) ) - ( (p3.x - p1.x) * (p2.y - p1.y) );
324 | };
325 |
326 | CV.convexityDefects = function(points, hull){
327 | var defects = [], len = hull.length,
328 | curr, next, point, dx0, dy0, scale, defect, isDefect,
329 | idx1, idx2, idx3, sign, inc, depth, dx, dy, dist, i, j;
330 |
331 | if (len >= 3){
332 | idx1 = CV.indexPoint(points, hull[0]);
333 | idx2 = CV.indexPoint(points, hull[1]);
334 | idx3 = CV.indexPoint(points, hull[2]);
335 |
336 | sign = 0;
337 | sign += idx2 > idx1? 1: 0;
338 | sign += idx3 > idx2? 1: 0;
339 | sign += idx1 > idx3? 1: 0;
340 |
341 | inc = (sign === 2)? 1: -1;
342 |
343 | j = idx1;
344 | curr = hull[0];
345 |
346 | for (i = 1; i !== len; ++ i){
347 | next = hull[i];
348 | isDefect = false;
349 | depth = 0;
350 |
351 | dx0 = next.x - curr.x;
352 | dy0 = next.y - curr.y;
353 | scale = 1 / Math.sqrt(dx0 * dx0 + dy0 * dy0);
354 |
355 | defect = {start: curr, end: next};
356 |
357 | while(true){
358 | j += inc;
359 | j = (j < 0)? points.length - 1: j % points.length;
360 |
361 | point = points[j];
362 |
363 | if (point.x === next.x && point.y === next.y){
364 | break;
365 | }
366 |
367 | dx = point.x - curr.x;
368 | dy = point.y - curr.y;
369 | dist = Math.abs(-dy0 * dx + dx0 * dy) * scale;
370 |
371 | if (dist > depth){
372 | isDefect = true;
373 |
374 | defect.depth = depth = dist;
375 | defect.depthPoint = point;
376 | }
377 | }
378 |
379 | if (isDefect){
380 | defects.push(defect);
381 | }
382 |
383 | curr = next;
384 | }
385 | }
386 |
387 | return defects;
388 | };
389 |
390 | CV.indexPoint = function(points, point){
391 | var len = points.length, i = 0;
392 | for (; i < len; ++ i){
393 | if (points[i].x === point.x && points[i].y === point.y){
394 | break;
395 | }
396 | }
397 | return i;
398 | };
399 |
400 | CV.area = function(poly){
401 | var area = 0, len = poly.length, i = 1,
402 | x, y, xmin, xmax, ymin, ymax;
403 |
404 | if (len > 0){
405 | xmin = xmax = poly[0].x;
406 | ymin = ymax = poly[0].y;
407 |
408 | for (; i < len; ++ i){
409 | x = poly[i].x;
410 | if (x < xmin){
411 | xmin = x;
412 | }
413 | if (x > xmax){
414 | xmax = x;
415 | }
416 |
417 | y = poly[i].y;
418 | if (y < ymin){
419 | ymin = y;
420 | }
421 | if (y > ymax){
422 | ymax = y;
423 | }
424 | }
425 |
426 | area = (xmax - xmin + 1) * (ymax - ymin + 1);
427 | }
428 |
429 | return area;
430 | };
431 |
--------------------------------------------------------------------------------
/samples/fast_demo/fast_demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hand Tracking
5 |
6 |
7 |
8 |
9 |
10 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | -= Hand Tracking =-
175 |
176 |
177 |
178 | Convex Hull
179 | Convexity Defects
180 | Skin Detection
181 |
182 |
183 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/samples/fast_demo/handtracking.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2012 Juan Mellado
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 | */
22 |
23 | var HT = HT || {};
24 |
25 | HT.Tracker = function(params){
26 | this.params = params || {};
27 |
28 | this.mask = new CV.Image();
29 | this.eroded = new CV.Image();
30 | this.contours = [];
31 |
32 | this.skinner = new HT.Skinner();
33 | };
34 |
35 | HT.Tracker.prototype.detect = function(image){
36 | this.skinner.mask(image, this.mask);
37 |
38 | if (this.params.fast){
39 | this.blackBorder(this.mask);
40 | }else{
41 | CV.erode(this.mask, this.eroded);
42 | CV.dilate(this.eroded, this.mask);
43 | }
44 |
45 | this.contours = CV.findContours(this.mask);
46 |
47 | return this.findCandidate(this.contours, image.width * image.height * 0.05, 0.005);
48 | };
49 |
50 | HT.Tracker.prototype.findCandidate = function(contours, minSize, epsilon){
51 | var contour, candidate;
52 |
53 | contour = this.findMaxArea(contours, minSize);
54 | if (contour){
55 | contour = CV.approxPolyDP(contour, contour.length * epsilon);
56 |
57 | candidate = new HT.Candidate(contour);
58 | }
59 |
60 | return candidate;
61 | };
62 |
63 | HT.Tracker.prototype.findMaxArea = function(contours, minSize){
64 | var len = contours.length, i = 0,
65 | maxArea = -Infinity, area, contour;
66 |
67 | for (; i < len; ++ i){
68 | area = CV.area(contours[i]);
69 | if (area >= minSize){
70 |
71 | if (area > maxArea) {
72 | maxArea = area;
73 |
74 | contour = contours[i];
75 | }
76 | }
77 | }
78 |
79 | return contour;
80 | };
81 |
82 | HT.Tracker.prototype.blackBorder = function(image){
83 | var img = image.data, width = image.width, height = image.height,
84 | pos = 0, i;
85 |
86 | for (i = 0; i < width; ++ i){
87 | img[pos ++] = 0;
88 | }
89 |
90 | for (i = 2; i < height; ++ i){
91 | img[pos] = img[pos + width - 1] = 0;
92 |
93 | pos += width;
94 | }
95 |
96 | for (i = 0; i < width; ++ i){
97 | img[pos ++] = 0;
98 | }
99 |
100 | return image;
101 | };
102 |
103 | HT.Candidate = function(contour){
104 | this.contour = contour;
105 | this.hull = CV.convexHull(contour);
106 | this.defects = CV.convexityDefects(contour, this.hull);
107 | };
108 |
109 | HT.Skinner = function(){
110 | };
111 |
112 | HT.Skinner.prototype.mask = function(imageSrc, imageDst){
113 | var src = imageSrc.data, dst = imageDst.data, len = src.length,
114 | i = 0, j = 0,
115 | r, g, b, h, s, v, value;
116 |
117 | for(; i < len; i += 4){
118 | r = src[i];
119 | g = src[i + 1];
120 | b = src[i + 2];
121 |
122 | v = Math.max(r, g, b);
123 | s = v === 0? 0: 255 * ( v - Math.min(r, g, b) ) / v;
124 | h = 0;
125 |
126 | if (0 !== s){
127 | if (v === r){
128 | h = 30 * (g - b) / s;
129 | }else if (v === g){
130 | h = 60 + ( (b - r) / s);
131 | }else{
132 | h = 120 + ( (r - g) / s);
133 | }
134 | if (h < 0){
135 | h += 360;
136 | }
137 | }
138 |
139 | value = 0;
140 |
141 | if (v >= 15 && v <= 250){
142 | if (h >= 3 && h <= 33){
143 | value = 255;
144 | }
145 | }
146 |
147 | dst[j ++] = value;
148 | }
149 |
150 | imageDst.width = imageSrc.width;
151 | imageDst.height = imageSrc.height;
152 |
153 | return imageDst;
154 | };
155 |
--------------------------------------------------------------------------------
/samples/fast_demo/libs/polyfill.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jcmellado/js-handtracking/0e6f06f64aba04020b71cfae79c16cdf697a81f9/samples/fast_demo/libs/polyfill.js
--------------------------------------------------------------------------------
/src/cv.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2012 Juan Mellado
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 | */
22 |
23 | /*
24 | References:
25 | - "OpenCV: Open Computer Vision Library"
26 | http://sourceforge.net/projects/opencvlibrary/
27 | */
28 |
29 | var CV = CV || {};
30 |
31 | CV.Image = function(width, height, data){
32 | this.width = width || 0;
33 | this.height = height || 0;
34 | this.data = data || [];
35 | };
36 |
37 | CV.findContours = function(imageSrc){
38 | var contours = [], src = imageSrc.data,
39 | width = imageSrc.width - 2, height = imageSrc.height - 2,
40 | pos = width + 3, nbd = 1,
41 | deltas, pix, outer, hole, i, j;
42 |
43 | deltas = CV.neighborhoodDeltas(width + 2);
44 |
45 | for (i = 0; i < height; ++ i, pos += 2){
46 |
47 | for (j = 0; j < width; ++ j, ++ pos){
48 | pix = src[pos];
49 |
50 | if (0 !== pix){
51 | outer = hole = false;
52 |
53 | if (1 === pix && 0 === src[pos - 1]){
54 | outer = true;
55 | }
56 | else if (pix >= 1 && 0 === src[pos + 1]){
57 | hole = true;
58 | }
59 |
60 | if (outer || hole){
61 | ++ nbd;
62 |
63 | contours.push( CV.borderFollowing(src, pos, nbd,
64 | {x: j, y: i}, hole, deltas) );
65 | }
66 | }
67 | }
68 | }
69 |
70 | return contours;
71 | };
72 |
73 | CV.borderFollowing = function(src, pos, nbd, point, hole, deltas){
74 | var contour = [], pos1, pos3, pos4, s, s_end, s_prev;
75 |
76 | contour.hole = hole;
77 |
78 | s = s_end = hole? 0: 4;
79 | do{
80 | s = (s - 1) & 7;
81 | pos1 = pos + deltas[s];
82 | if (src[pos1] !== 0){
83 | break;
84 | }
85 | }while(s !== s_end);
86 |
87 | if (s === s_end){
88 | src[pos] = -nbd;
89 | contour.push( {x: point.x, y: point.y} );
90 |
91 | }else{
92 | pos3 = pos;
93 | s_prev = s ^ 4;
94 |
95 | while(true){
96 | s_end = s;
97 |
98 | do{
99 | pos4 = pos3 + deltas[++ s];
100 | }while(src[pos4] === 0);
101 |
102 | s &= 7;
103 |
104 | if ( ( (s - 1) >>> 0) < (s_end >>> 0) ){
105 | src[pos3] = -nbd;
106 | }
107 | else if (src[pos3] === 1){
108 | src[pos3] = nbd;
109 | }
110 |
111 | contour.push( {x: point.x, y: point.y} );
112 |
113 | s_prev = s;
114 |
115 | point.x += CV.neighborhood[s][0];
116 | point.y += CV.neighborhood[s][1];
117 |
118 | if ( (pos4 === pos) && (pos3 === pos1) ){
119 | break;
120 | }
121 |
122 | pos3 = pos4;
123 | s = (s + 4) & 7;
124 | }
125 | }
126 |
127 | return contour;
128 | };
129 |
130 | CV.neighborhood =
131 | [ [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1] ];
132 |
133 | CV.neighborhoodDeltas = function(width){
134 | var deltas = [], len = CV.neighborhood.length, i = 0;
135 |
136 | for (; i < len; ++ i){
137 | deltas[i] = CV.neighborhood[i][0] + (CV.neighborhood[i][1] * width);
138 | }
139 |
140 | return deltas.concat(deltas);
141 | };
142 |
143 | CV.approxPolyDP = function(contour, epsilon){
144 | var slice = {start_index: 0, end_index: 0},
145 | right_slice = {start_index: 0, end_index: 0},
146 | poly = [], stack = [], len = contour.length,
147 | pt, start_pt, end_pt, dist, max_dist, le_eps,
148 | dx, dy, i, j, k;
149 |
150 | epsilon *= epsilon;
151 |
152 | k = 0;
153 |
154 | for (i = 0; i < 3; ++ i){
155 | max_dist = 0;
156 |
157 | k = (k + right_slice.start_index) % len;
158 | start_pt = contour[k];
159 | if (++ k === len) {k = 0;}
160 |
161 | for (j = 1; j < len; ++ j){
162 | pt = contour[k];
163 | if (++ k === len) {k = 0;}
164 |
165 | dx = pt.x - start_pt.x;
166 | dy = pt.y - start_pt.y;
167 | dist = dx * dx + dy * dy;
168 |
169 | if (dist > max_dist){
170 | max_dist = dist;
171 | right_slice.start_index = j;
172 | }
173 | }
174 | }
175 |
176 | if (max_dist <= epsilon){
177 | poly.push( {x: start_pt.x, y: start_pt.y} );
178 |
179 | }else{
180 | slice.start_index = k;
181 | slice.end_index = (right_slice.start_index += slice.start_index);
182 |
183 | right_slice.start_index -= right_slice.start_index >= len? len: 0;
184 | right_slice.end_index = slice.start_index;
185 | if (right_slice.end_index < right_slice.start_index){
186 | right_slice.end_index += len;
187 | }
188 |
189 | stack.push( {start_index: right_slice.start_index, end_index: right_slice.end_index} );
190 | stack.push( {start_index: slice.start_index, end_index: slice.end_index} );
191 | }
192 |
193 | while(stack.length !== 0){
194 | slice = stack.pop();
195 |
196 | end_pt = contour[slice.end_index % len];
197 | start_pt = contour[k = slice.start_index % len];
198 | if (++ k === len) {k = 0;}
199 |
200 | if (slice.end_index <= slice.start_index + 1){
201 | le_eps = true;
202 |
203 | }else{
204 | max_dist = 0;
205 |
206 | dx = end_pt.x - start_pt.x;
207 | dy = end_pt.y - start_pt.y;
208 |
209 | for (i = slice.start_index + 1; i < slice.end_index; ++ i){
210 | pt = contour[k];
211 | if (++ k === len) {k = 0;}
212 |
213 | dist = Math.abs( (pt.y - start_pt.y) * dx - (pt.x - start_pt.x) * dy);
214 |
215 | if (dist > max_dist){
216 | max_dist = dist;
217 | right_slice.start_index = i;
218 | }
219 | }
220 |
221 | le_eps = max_dist * max_dist <= epsilon * (dx * dx + dy * dy);
222 | }
223 |
224 | if (le_eps){
225 | poly.push( {x: start_pt.x, y: start_pt.y} );
226 |
227 | }else{
228 | right_slice.end_index = slice.end_index;
229 | slice.end_index = right_slice.start_index;
230 |
231 | stack.push( {start_index: right_slice.start_index, end_index: right_slice.end_index} );
232 | stack.push( {start_index: slice.start_index, end_index: slice.end_index} );
233 | }
234 | }
235 |
236 | return poly;
237 | };
238 |
239 | CV.erode = function(imageSrc, imageDst){
240 | return CV.applyKernel(imageSrc, imageDst, Math.min);
241 | };
242 |
243 | CV.dilate = function(imageSrc, imageDst){
244 | return CV.applyKernel(imageSrc, imageDst, Math.max);
245 | };
246 |
247 | CV.applyKernel = function(imageSrc, imageDst, fn){
248 | var src = imageSrc.data, dst = imageDst.data,
249 | width = imageSrc.width, height = imageSrc.height,
250 | offsets = [-width - 1, -width, -width + 1, -1, 1, width - 1, width, width + 1],
251 | klen = offsets.length,
252 | pos = 0, value, i, j, k;
253 |
254 | for (i = 0; i < width; ++ i){
255 | dst[pos ++] = 0;
256 | }
257 |
258 | for (i = 2; i < height; ++ i){
259 | dst[pos ++] = 0;
260 |
261 | for (j = 2; j < width; ++ j){
262 | value = src[pos];
263 |
264 | for (k = 0; k < klen; ++ k){
265 | value = fn(value, src[ pos + offsets[k] ] );
266 | }
267 |
268 | dst[pos ++] = value;
269 | }
270 |
271 | dst[pos ++] = 0;
272 | }
273 |
274 | for (i = 0; i < width; ++ i){
275 | dst[pos ++] = 0;
276 | }
277 |
278 | imageDst.width = imageSrc.width;
279 | imageDst.height = imageSrc.height;
280 |
281 | return imageDst;
282 | };
283 |
284 | CV.convexHull = function(points){
285 | var deque = [], i = 3, point;
286 |
287 | if (points.length >= 3){
288 |
289 | if ( CV.position(points[0], points[1], points[2]) > 0){
290 | deque.push(points[0]);
291 | deque.push(points[1]);
292 | }else{
293 | deque.push(points[1]);
294 | deque.push(points[0]);
295 | }
296 | deque.push(points[2]);
297 | deque.unshift(points[2]);
298 |
299 | for (; i < points.length; ++ i){
300 | point = points[i];
301 |
302 | if ( CV.position(point, deque[0], deque[1]) < 0 ||
303 | CV.position(deque[deque.length - 2], deque[deque.length - 1], point) < 0 ){
304 |
305 | while( CV.position(deque[deque.length - 2], deque[deque.length - 1], point) <= 0 ){
306 | deque.pop();
307 | }
308 | deque.push(point);
309 |
310 | while( CV.position(point, deque[0], deque[1]) <= 0 ){
311 | deque.shift();
312 | }
313 | deque.unshift(point);
314 | }
315 | }
316 |
317 | }
318 |
319 | return deque;
320 | };
321 |
322 | CV.position = function(p1, p2, p3){
323 | return ( (p2.x - p1.x) * (p3.y - p1.y) ) - ( (p3.x - p1.x) * (p2.y - p1.y) );
324 | };
325 |
326 | CV.convexityDefects = function(points, hull){
327 | var defects = [], len = hull.length,
328 | curr, next, point, dx0, dy0, scale, defect, isDefect,
329 | idx1, idx2, idx3, sign, inc, depth, dx, dy, dist, i, j;
330 |
331 | if (len >= 3){
332 | idx1 = CV.indexPoint(points, hull[0]);
333 | idx2 = CV.indexPoint(points, hull[1]);
334 | idx3 = CV.indexPoint(points, hull[2]);
335 |
336 | sign = 0;
337 | sign += idx2 > idx1? 1: 0;
338 | sign += idx3 > idx2? 1: 0;
339 | sign += idx1 > idx3? 1: 0;
340 |
341 | inc = (sign === 2)? 1: -1;
342 |
343 | j = idx1;
344 | curr = hull[0];
345 |
346 | for (i = 1; i !== len; ++ i){
347 | next = hull[i];
348 | isDefect = false;
349 | depth = 0;
350 |
351 | dx0 = next.x - curr.x;
352 | dy0 = next.y - curr.y;
353 | scale = 1 / Math.sqrt(dx0 * dx0 + dy0 * dy0);
354 |
355 | defect = {start: curr, end: next};
356 |
357 | while(true){
358 | j += inc;
359 | j = (j < 0)? points.length - 1: j % points.length;
360 |
361 | point = points[j];
362 |
363 | if (point.x === next.x && point.y === next.y){
364 | break;
365 | }
366 |
367 | dx = point.x - curr.x;
368 | dy = point.y - curr.y;
369 | dist = Math.abs(-dy0 * dx + dx0 * dy) * scale;
370 |
371 | if (dist > depth){
372 | isDefect = true;
373 |
374 | defect.depth = depth = dist;
375 | defect.depthPoint = point;
376 | }
377 | }
378 |
379 | if (isDefect){
380 | defects.push(defect);
381 | }
382 |
383 | curr = next;
384 | }
385 | }
386 |
387 | return defects;
388 | };
389 |
390 | CV.indexPoint = function(points, point){
391 | var len = points.length, i = 0;
392 | for (; i < len; ++ i){
393 | if (points[i].x === point.x && points[i].y === point.y){
394 | break;
395 | }
396 | }
397 | return i;
398 | };
399 |
400 | CV.area = function(poly){
401 | var area = 0, len = poly.length, i = 1,
402 | x, y, xmin, xmax, ymin, ymax;
403 |
404 | if (len > 0){
405 | xmin = xmax = poly[0].x;
406 | ymin = ymax = poly[0].y;
407 |
408 | for (; i < len; ++ i){
409 | x = poly[i].x;
410 | if (x < xmin){
411 | xmin = x;
412 | }
413 | if (x > xmax){
414 | xmax = x;
415 | }
416 |
417 | y = poly[i].y;
418 | if (y < ymin){
419 | ymin = y;
420 | }
421 | if (y > ymax){
422 | ymax = y;
423 | }
424 | }
425 |
426 | area = (xmax - xmin + 1) * (ymax - ymin + 1);
427 | }
428 |
429 | return area;
430 | };
431 |
--------------------------------------------------------------------------------
/src/handtracking.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2012 Juan Mellado
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 | */
22 |
23 | var HT = HT || {};
24 |
25 | HT.Tracker = function(params){
26 | this.params = params || {};
27 |
28 | this.mask = new CV.Image();
29 | this.eroded = new CV.Image();
30 | this.contours = [];
31 |
32 | this.skinner = new HT.Skinner();
33 | };
34 |
35 | HT.Tracker.prototype.detect = function(image){
36 | this.skinner.mask(image, this.mask);
37 |
38 | if (this.params.fast){
39 | this.blackBorder(this.mask);
40 | }else{
41 | CV.erode(this.mask, this.eroded);
42 | CV.dilate(this.eroded, this.mask);
43 | }
44 |
45 | this.contours = CV.findContours(this.mask);
46 |
47 | return this.findCandidate(this.contours, image.width * image.height * 0.05, 0.005);
48 | };
49 |
50 | HT.Tracker.prototype.findCandidate = function(contours, minSize, epsilon){
51 | var contour, candidate;
52 |
53 | contour = this.findMaxArea(contours, minSize);
54 | if (contour){
55 | contour = CV.approxPolyDP(contour, contour.length * epsilon);
56 |
57 | candidate = new HT.Candidate(contour);
58 | }
59 |
60 | return candidate;
61 | };
62 |
63 | HT.Tracker.prototype.findMaxArea = function(contours, minSize){
64 | var len = contours.length, i = 0,
65 | maxArea = -Infinity, area, contour;
66 |
67 | for (; i < len; ++ i){
68 | area = CV.area(contours[i]);
69 | if (area >= minSize){
70 |
71 | if (area > maxArea) {
72 | maxArea = area;
73 |
74 | contour = contours[i];
75 | }
76 | }
77 | }
78 |
79 | return contour;
80 | };
81 |
82 | HT.Tracker.prototype.blackBorder = function(image){
83 | var img = image.data, width = image.width, height = image.height,
84 | pos = 0, i;
85 |
86 | for (i = 0; i < width; ++ i){
87 | img[pos ++] = 0;
88 | }
89 |
90 | for (i = 2; i < height; ++ i){
91 | img[pos] = img[pos + width - 1] = 0;
92 |
93 | pos += width;
94 | }
95 |
96 | for (i = 0; i < width; ++ i){
97 | img[pos ++] = 0;
98 | }
99 |
100 | return image;
101 | };
102 |
103 | HT.Candidate = function(contour){
104 | this.contour = contour;
105 | this.hull = CV.convexHull(contour);
106 | this.defects = CV.convexityDefects(contour, this.hull);
107 | };
108 |
109 | HT.Skinner = function(){
110 | };
111 |
112 | HT.Skinner.prototype.mask = function(imageSrc, imageDst){
113 | var src = imageSrc.data, dst = imageDst.data, len = src.length,
114 | i = 0, j = 0,
115 | r, g, b, h, s, v, value;
116 |
117 | for(; i < len; i += 4){
118 | r = src[i];
119 | g = src[i + 1];
120 | b = src[i + 2];
121 |
122 | v = Math.max(r, g, b);
123 | s = v === 0? 0: 255 * ( v - Math.min(r, g, b) ) / v;
124 | h = 0;
125 |
126 | if (0 !== s){
127 | if (v === r){
128 | h = 30 * (g - b) / s;
129 | }else if (v === g){
130 | h = 60 + ( (b - r) / s);
131 | }else{
132 | h = 120 + ( (r - g) / s);
133 | }
134 | if (h < 0){
135 | h += 360;
136 | }
137 | }
138 |
139 | value = 0;
140 |
141 | if (v >= 15 && v <= 250){
142 | if (h >= 3 && h <= 33){
143 | value = 255;
144 | }
145 | }
146 |
147 | dst[j ++] = value;
148 | }
149 |
150 | imageDst.width = imageSrc.width;
151 | imageDst.height = imageSrc.height;
152 |
153 | return imageDst;
154 | };
155 |
--------------------------------------------------------------------------------