├── 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 |
Powered by js-handtracking
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 |
Powered by js-handtracking
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 | --------------------------------------------------------------------------------