├── .gitignore ├── LICENSE.txt ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js └── src └── MagicWand.js /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /.vs 6 | /node_modules 7 | /dist 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014, Ryasnoy Paul (ryasnoypaul@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![view on npm](http://img.shields.io/npm/v/magic-wand-tool.svg)](https://www.npmjs.org/package/magic-wand-tool) 3 | [![License: MIT](https://img.shields.io/github/license/tamersoul/magic-wand-js.svg)](https://github.com/Tamersoul/magic-wand-js/blob/master/LICENSE.txt) 4 | 5 | # Magic wand tool (fuzzy selection) by color for Javascript 6 | 7 | Creates binary mask and contours (vector data) from raster image data by color differences 8 | 9 | ## Installation 10 | 11 | Install it thought NPM: 12 | 13 | ```shell 14 | npm install magic-wand-tool 15 | ``` 16 | 17 | Or add from CDN: 18 | 19 | ```html 20 | 21 | ``` 22 | 23 | ### Example usage: 24 | 25 | [Live example on the jsFiddle](http://jsfiddle.net/Tamersoul/dr7Dw/) 26 | 27 | ## License 28 | 29 | [MIT](https://opensource.org/licenses/MIT) (c) 2014, Ryasnoy Paul 30 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-wand-tool", 3 | "version": "1.1.7", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.10.4", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", 10 | "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.10.4" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.10.4", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", 19 | "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.10.4", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", 25 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.10.4", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | } 32 | }, 33 | "@types/estree": { 34 | "version": "0.0.45", 35 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz", 36 | "integrity": "sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==", 37 | "dev": true 38 | }, 39 | "@types/node": { 40 | "version": "14.11.8", 41 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz", 42 | "integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==", 43 | "dev": true 44 | }, 45 | "acorn": { 46 | "version": "7.4.1", 47 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", 48 | "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", 49 | "dev": true 50 | }, 51 | "ansi-styles": { 52 | "version": "3.2.1", 53 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 54 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 55 | "dev": true, 56 | "requires": { 57 | "color-convert": "^1.9.0" 58 | } 59 | }, 60 | "buffer-from": { 61 | "version": "1.1.1", 62 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 63 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 64 | "dev": true 65 | }, 66 | "chalk": { 67 | "version": "2.4.2", 68 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 69 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 70 | "dev": true, 71 | "requires": { 72 | "ansi-styles": "^3.2.1", 73 | "escape-string-regexp": "^1.0.5", 74 | "supports-color": "^5.3.0" 75 | } 76 | }, 77 | "color-convert": { 78 | "version": "1.9.3", 79 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 80 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 81 | "dev": true, 82 | "requires": { 83 | "color-name": "1.1.3" 84 | } 85 | }, 86 | "color-name": { 87 | "version": "1.1.3", 88 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 89 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 90 | "dev": true 91 | }, 92 | "commander": { 93 | "version": "2.20.3", 94 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 95 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 96 | "dev": true 97 | }, 98 | "escape-string-regexp": { 99 | "version": "1.0.5", 100 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 101 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 102 | "dev": true 103 | }, 104 | "estree-walker": { 105 | "version": "0.6.1", 106 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 107 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 108 | "dev": true 109 | }, 110 | "has-flag": { 111 | "version": "3.0.0", 112 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 113 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 114 | "dev": true 115 | }, 116 | "jest-worker": { 117 | "version": "24.9.0", 118 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", 119 | "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", 120 | "dev": true, 121 | "requires": { 122 | "merge-stream": "^2.0.0", 123 | "supports-color": "^6.1.0" 124 | }, 125 | "dependencies": { 126 | "supports-color": { 127 | "version": "6.1.0", 128 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", 129 | "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", 130 | "dev": true, 131 | "requires": { 132 | "has-flag": "^3.0.0" 133 | } 134 | } 135 | } 136 | }, 137 | "js-tokens": { 138 | "version": "4.0.0", 139 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 140 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 141 | "dev": true 142 | }, 143 | "merge-stream": { 144 | "version": "2.0.0", 145 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 146 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 147 | "dev": true 148 | }, 149 | "randombytes": { 150 | "version": "2.1.0", 151 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 152 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 153 | "dev": true, 154 | "requires": { 155 | "safe-buffer": "^5.1.0" 156 | } 157 | }, 158 | "rollup": { 159 | "version": "1.32.1", 160 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", 161 | "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", 162 | "dev": true, 163 | "requires": { 164 | "@types/estree": "*", 165 | "@types/node": "*", 166 | "acorn": "^7.1.0" 167 | } 168 | }, 169 | "rollup-plugin-terser": { 170 | "version": "5.3.1", 171 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.1.tgz", 172 | "integrity": "sha512-1pkwkervMJQGFYvM9nscrUoncPwiKR/K+bHdjv6PFgRo3cgPHoRT83y2Aa3GvINj4539S15t/tpFPb775TDs6w==", 173 | "dev": true, 174 | "requires": { 175 | "@babel/code-frame": "^7.5.5", 176 | "jest-worker": "^24.9.0", 177 | "rollup-pluginutils": "^2.8.2", 178 | "serialize-javascript": "^4.0.0", 179 | "terser": "^4.6.2" 180 | } 181 | }, 182 | "rollup-pluginutils": { 183 | "version": "2.8.2", 184 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 185 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 186 | "dev": true, 187 | "requires": { 188 | "estree-walker": "^0.6.1" 189 | } 190 | }, 191 | "safe-buffer": { 192 | "version": "5.2.1", 193 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 194 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 195 | "dev": true 196 | }, 197 | "serialize-javascript": { 198 | "version": "4.0.0", 199 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", 200 | "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", 201 | "dev": true, 202 | "requires": { 203 | "randombytes": "^2.1.0" 204 | } 205 | }, 206 | "source-map": { 207 | "version": "0.6.1", 208 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 209 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 210 | "dev": true 211 | }, 212 | "source-map-support": { 213 | "version": "0.5.19", 214 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 215 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 216 | "dev": true, 217 | "requires": { 218 | "buffer-from": "^1.0.0", 219 | "source-map": "^0.6.0" 220 | } 221 | }, 222 | "supports-color": { 223 | "version": "5.5.0", 224 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 225 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 226 | "dev": true, 227 | "requires": { 228 | "has-flag": "^3.0.0" 229 | } 230 | }, 231 | "terser": { 232 | "version": "4.8.0", 233 | "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", 234 | "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", 235 | "dev": true, 236 | "requires": { 237 | "commander": "^2.20.0", 238 | "source-map": "~0.6.1", 239 | "source-map-support": "~0.5.12" 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-wand-tool", 3 | "version": "1.1.7", 4 | "description": "Magic wand tool (fuzzy selection) by color", 5 | "main": "dist/magic-wand.js", 6 | "scripts": { 7 | "rollup": "rollup -c", 8 | "production": "npm run rollup --silent" 9 | }, 10 | "homepage": "https://github.com/Tamersoul/magic-wand-js", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Tamersoul/magic-wand-js.git" 14 | }, 15 | "keywords": [ 16 | "image", 17 | "magic-wand", 18 | "selection", 19 | "floodfill", 20 | "contour" 21 | ], 22 | "author": "Ryasnoy Paul ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/Tamersoul/magic-wand-js/issues" 26 | }, 27 | "devDependencies": { 28 | "rollup": "^1.32.1", 29 | "rollup-plugin-terser": "^5.3.1" 30 | }, 31 | "files": [ 32 | "LICENSE", 33 | "README.md", 34 | "dist", 35 | "src" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import packageJson from './package.json'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | 5 | const banner = `/* 6 | ${packageJson.description} 7 | 8 | @package ${packageJson.name} 9 | @author ${packageJson.author} 10 | @version ${packageJson.version} 11 | @license ${packageJson.license} 12 | @copyright (c) 2014-${new Date().getFullYear()}, ${packageJson.author} 13 | 14 | */ 15 | `; 16 | 17 | export default { 18 | input: path.resolve(__dirname, './src/MagicWand.js'), 19 | output: [{ 20 | format: 'esm', 21 | file: path.join(__dirname, `./dist/magic-wand.js`), 22 | sourcemap: true, 23 | banner: banner 24 | }, { 25 | format: 'esm', 26 | file: path.join(__dirname, `./dist/magic-wand.min.js`), 27 | sourcemap: true, 28 | banner: banner, 29 | plugins: [terser()] 30 | }] 31 | }; -------------------------------------------------------------------------------- /src/MagicWand.js: -------------------------------------------------------------------------------- 1 |  2 | var MagicWand = (function () { 3 | var lib = {}; 4 | 5 | /** Create a binary mask on the image by color threshold 6 | * Algorithm: Scanline flood fill (http://en.wikipedia.org/wiki/Flood_fill) 7 | * @param {Object} image: {Uint8Array} data, {int} width, {int} height, {int} bytes 8 | * @param {int} x of start pixel 9 | * @param {int} y of start pixel 10 | * @param {int} color threshold 11 | * @param {Uint8Array} mask of visited points (optional) 12 | * @param {boolean} [includeBorders=false] indicate whether to include borders pixels 13 | * @return {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 14 | */ 15 | lib.floodFill = function(image, px, py, colorThreshold, mask, includeBorders) { 16 | return includeBorders 17 | ? floodFillWithBorders(image, px, py, colorThreshold, mask) 18 | : floodFillWithoutBorders(image, px, py, colorThreshold, mask); 19 | }; 20 | 21 | function floodFillWithoutBorders(image, px, py, colorThreshold, mask) { 22 | 23 | var c, x, newY, el, xr, xl, dy, dyl, dyr, checkY, 24 | data = image.data, 25 | w = image.width, 26 | h = image.height, 27 | bytes = image.bytes, // number of bytes in the color 28 | maxX = -1, minX = w + 1, maxY = -1, minY = h + 1, 29 | i = py * w + px, // start point index in the mask data 30 | result = new Uint8Array(w * h), // result mask 31 | visited = new Uint8Array(mask ? mask : w * h); // mask of visited points 32 | 33 | if (visited[i] === 1) return null; 34 | 35 | i = i * bytes; // start point index in the image data 36 | var sampleColor = [data[i], data[i + 1], data[i + 2], data[i + 3]]; // start point color (sample) 37 | 38 | var stack = [{ y: py, left: px - 1, right: px + 1, dir: 1 }]; // first scanning line 39 | do { 40 | el = stack.shift(); // get line for scanning 41 | 42 | checkY = false; 43 | for (x = el.left + 1; x < el.right; x++) { 44 | dy = el.y * w; 45 | i = (dy + x) * bytes; // point index in the image data 46 | 47 | if (visited[dy + x] === 1) continue; // check whether the point has been visited 48 | // compare the color of the sample 49 | c = data[i] - sampleColor[0]; // check by red 50 | if (c > colorThreshold || c < -colorThreshold) continue; 51 | c = data[i + 1] - sampleColor[1]; // check by green 52 | if (c > colorThreshold || c < -colorThreshold) continue; 53 | c = data[i + 2] - sampleColor[2]; // check by blue 54 | if (c > colorThreshold || c < -colorThreshold) continue; 55 | 56 | checkY = true; // if the color of the new point(x,y) is similar to the sample color need to check minmax for Y 57 | 58 | result[dy + x] = 1; // mark a new point in mask 59 | visited[dy + x] = 1; // mark a new point as visited 60 | 61 | xl = x - 1; 62 | // walk to left side starting with the left neighbor 63 | while (xl > -1) { 64 | dyl = dy + xl; 65 | i = dyl * bytes; // point index in the image data 66 | if (visited[dyl] === 1) break; // check whether the point has been visited 67 | // compare the color of the sample 68 | c = data[i] - sampleColor[0]; // check by red 69 | if (c > colorThreshold || c < -colorThreshold) break; 70 | c = data[i + 1] - sampleColor[1]; // check by green 71 | if (c > colorThreshold || c < -colorThreshold) break; 72 | c = data[i + 2] - sampleColor[2]; // check by blue 73 | if (c > colorThreshold || c < -colorThreshold) break; 74 | 75 | result[dyl] = 1; 76 | visited[dyl] = 1; 77 | 78 | xl--; 79 | } 80 | xr = x + 1; 81 | // walk to right side starting with the right neighbor 82 | while (xr < w) { 83 | dyr = dy + xr; 84 | i = dyr * bytes; // index point in the image data 85 | if (visited[dyr] === 1) break; // check whether the point has been visited 86 | // compare the color of the sample 87 | c = data[i] - sampleColor[0]; // check by red 88 | if (c > colorThreshold || c < -colorThreshold) break; 89 | c = data[i + 1] - sampleColor[1]; // check by green 90 | if (c > colorThreshold || c < -colorThreshold) break; 91 | c = data[i + 2] - sampleColor[2]; // check by blue 92 | if (c > colorThreshold || c < -colorThreshold) break; 93 | 94 | result[dyr] = 1; 95 | visited[dyr] = 1; 96 | 97 | xr++; 98 | } 99 | 100 | // check minmax for X 101 | if (xl < minX) minX = xl + 1; 102 | if (xr > maxX) maxX = xr - 1; 103 | 104 | newY = el.y - el.dir; 105 | if (newY >= 0 && newY < h) { // add two scanning lines in the opposite direction (y - dir) if necessary 106 | if (xl < el.left) stack.push({ y: newY, left: xl, right: el.left, dir: -el.dir }); // from "new left" to "current left" 107 | if (el.right < xr) stack.push({ y: newY, left: el.right, right: xr, dir: -el.dir }); // from "current right" to "new right" 108 | } 109 | newY = el.y + el.dir; 110 | if (newY >= 0 && newY < h) { // add the scanning line in the direction (y + dir) if necessary 111 | if (xl < xr) stack.push({ y: newY, left: xl, right: xr, dir: el.dir }); // from "new left" to "new right" 112 | } 113 | } 114 | // check minmax for Y if necessary 115 | if (checkY) { 116 | if (el.y < minY) minY = el.y; 117 | if (el.y > maxY) maxY = el.y; 118 | } 119 | } while (stack.length > 0); 120 | 121 | return { 122 | data: result, 123 | width: image.width, 124 | height: image.height, 125 | bounds: { 126 | minX: minX, 127 | minY: minY, 128 | maxX: maxX, 129 | maxY: maxY 130 | } 131 | }; 132 | }; 133 | 134 | function floodFillWithBorders(image, px, py, colorThreshold, mask) { 135 | 136 | var c, x, newY, el, xr, xl, dy, dyl, dyr, checkY, 137 | data = image.data, 138 | w = image.width, 139 | h = image.height, 140 | bytes = image.bytes, // number of bytes in the color 141 | maxX = -1, minX = w + 1, maxY = -1, minY = h + 1, 142 | i = py * w + px, // start point index in the mask data 143 | result = new Uint8Array(w * h), // result mask 144 | visited = new Uint8Array(mask ? mask : w * h); // mask of visited points 145 | 146 | if (visited[i] === 1) return null; 147 | 148 | i = i * bytes; // start point index in the image data 149 | var sampleColor = [data[i], data[i + 1], data[i + 2], data[i + 3]]; // start point color (sample) 150 | 151 | var stack = [{ y: py, left: px - 1, right: px + 1, dir: 1 }]; // first scanning line 152 | do { 153 | el = stack.shift(); // get line for scanning 154 | 155 | checkY = false; 156 | for (x = el.left + 1; x < el.right; x++) { 157 | dy = el.y * w; 158 | i = (dy + x) * bytes; // point index in the image data 159 | 160 | if (visited[dy + x] === 1) continue; // check whether the point has been visited 161 | 162 | checkY = true; // if the color of the new point(x,y) is similar to the sample color need to check minmax for Y 163 | 164 | result[dy + x] = 1; // mark a new point in mask 165 | visited[dy + x] = 1; // mark a new point as visited 166 | 167 | // compare the color of the sample 168 | c = data[i] - sampleColor[0]; // check by red 169 | if (c > colorThreshold || c < -colorThreshold) continue; 170 | c = data[i + 1] - sampleColor[1]; // check by green 171 | if (c > colorThreshold || c < -colorThreshold) continue; 172 | c = data[i + 2] - sampleColor[2]; // check by blue 173 | if (c > colorThreshold || c < -colorThreshold) continue; 174 | 175 | xl = x - 1; 176 | // walk to left side starting with the left neighbor 177 | while (xl > -1) { 178 | dyl = dy + xl; 179 | i = dyl * bytes; // point index in the image data 180 | if (visited[dyl] === 1) break; // check whether the point has been visited 181 | 182 | result[dyl] = 1; 183 | visited[dyl] = 1; 184 | xl--; 185 | 186 | // compare the color of the sample 187 | c = data[i] - sampleColor[0]; // check by red 188 | if (c > colorThreshold || c < -colorThreshold) break; 189 | c = data[i + 1] - sampleColor[1]; // check by green 190 | if (c > colorThreshold || c < -colorThreshold) break; 191 | c = data[i + 2] - sampleColor[2]; // check by blue 192 | if (c > colorThreshold || c < -colorThreshold) break; 193 | } 194 | xr = x + 1; 195 | // walk to right side starting with the right neighbor 196 | while (xr < w) { 197 | dyr = dy + xr; 198 | i = dyr * bytes; // index point in the image data 199 | if (visited[dyr] === 1) break; // check whether the point has been visited 200 | 201 | result[dyr] = 1; 202 | visited[dyr] = 1; 203 | xr++; 204 | 205 | // compare the color of the sample 206 | c = data[i] - sampleColor[0]; // check by red 207 | if (c > colorThreshold || c < -colorThreshold) break; 208 | c = data[i + 1] - sampleColor[1]; // check by green 209 | if (c > colorThreshold || c < -colorThreshold) break; 210 | c = data[i + 2] - sampleColor[2]; // check by blue 211 | if (c > colorThreshold || c < -colorThreshold) break; 212 | } 213 | 214 | // check minmax for X 215 | if (xl < minX) minX = xl + 1; 216 | if (xr > maxX) maxX = xr - 1; 217 | 218 | newY = el.y - el.dir; 219 | if (newY >= 0 && newY < h) { // add two scanning lines in the opposite direction (y - dir) if necessary 220 | if (xl < el.left) stack.push({ y: newY, left: xl, right: el.left, dir: -el.dir }); // from "new left" to "current left" 221 | if (el.right < xr) stack.push({ y: newY, left: el.right, right: xr, dir: -el.dir }); // from "current right" to "new right" 222 | } 223 | newY = el.y + el.dir; 224 | if (newY >= 0 && newY < h) { // add the scanning line in the direction (y + dir) if necessary 225 | if (xl < xr) stack.push({ y: newY, left: xl, right: xr, dir: el.dir }); // from "new left" to "new right" 226 | } 227 | } 228 | // check minmax for Y if necessary 229 | if (checkY) { 230 | if (el.y < minY) minY = el.y; 231 | if (el.y > maxY) maxY = el.y; 232 | } 233 | } while (stack.length > 0); 234 | 235 | return { 236 | data: result, 237 | width: image.width, 238 | height: image.height, 239 | bounds: { 240 | minX: minX, 241 | minY: minY, 242 | maxX: maxX, 243 | maxY: maxY 244 | } 245 | }; 246 | }; 247 | 248 | /** Apply the gauss-blur filter to binary mask 249 | * Algorithms: http://blog.ivank.net/fastest-gaussian-blur.html 250 | * http://www.librow.com/articles/article-9 251 | * http://elynxsdk.free.fr/ext-docs/Blur/Fast_box_blur.pdf 252 | * @param {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 253 | * @param {int} blur radius 254 | * @return {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 255 | */ 256 | lib.gaussBlur = function(mask, radius) { 257 | 258 | var i, k, k1, x, y, val, start, end, 259 | n = radius * 2 + 1, // size of the pattern for radius-neighbors (from -r to +r with the center point) 260 | s2 = radius * radius, 261 | wg = new Float32Array(n), // weights 262 | total = 0, // sum of weights(used for normalization) 263 | w = mask.width, 264 | h = mask.height, 265 | data = mask.data, 266 | minX = mask.bounds.minX, 267 | maxX = mask.bounds.maxX, 268 | minY = mask.bounds.minY, 269 | maxY = mask.bounds.maxY; 270 | 271 | // calc gauss weights 272 | for (i = 0; i < radius; i++) { 273 | var dsq = (radius - i) * (radius - i); 274 | var ww = Math.exp(-dsq / (2.0 * s2)) / (2 * Math.PI * s2); 275 | wg[radius + i] = wg[radius - i] = ww; 276 | total += 2 * ww; 277 | } 278 | // normalization weights 279 | for (i = 0; i < n; i++) { 280 | wg[i] /= total; 281 | } 282 | 283 | var result = new Uint8Array(w * h), // result mask 284 | endX = radius + w, 285 | endY = radius + h; 286 | 287 | //walk through all source points for blur 288 | for (y = minY; y < maxY + 1; y++) 289 | for (x = minX; x < maxX + 1; x++) { 290 | val = 0; 291 | k = y * w + x; // index of the point 292 | start = radius - x > 0 ? radius - x : 0; 293 | end = endX - x < n ? endX - x : n; // Math.min((((w - 1) - x) + radius) + 1, n); 294 | k1 = k - radius; 295 | // walk through x-neighbors 296 | for (i = start; i < end; i++) { 297 | val += data[k1 + i] * wg[i]; 298 | } 299 | start = radius - y > 0 ? radius - y : 0; 300 | end = endY - y < n ? endY - y : n; // Math.min((((h - 1) - y) + radius) + 1, n); 301 | k1 = k - radius * w; 302 | // walk through y-neighbors 303 | for (i = start; i < end; i++) { 304 | val += data[k1 + i * w] * wg[i]; 305 | } 306 | result[k] = val > 0.5 ? 1 : 0; 307 | } 308 | 309 | return { 310 | data: result, 311 | width: w, 312 | height: h, 313 | bounds: { 314 | minX: minX, 315 | minY: minY, 316 | maxX: maxX, 317 | maxY: maxY 318 | } 319 | }; 320 | }; 321 | 322 | /** Create a border index array of boundary points of the mask with radius-neighbors 323 | * @param {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 324 | * @param {int} blur radius 325 | * @param {Uint8Array} visited: mask of visited points (optional) 326 | * @return {Array} border index array of boundary points with radius-neighbors (only points need for blur) 327 | */ 328 | function createBorderForBlur(mask, radius, visited) { 329 | 330 | var x, i, j, y, k, k1, k2, 331 | w = mask.width, 332 | h = mask.height, 333 | data = mask.data, 334 | visitedData = new Uint8Array(data), 335 | minX = mask.bounds.minX, 336 | maxX = mask.bounds.maxX, 337 | minY = mask.bounds.minY, 338 | maxY = mask.bounds.maxY, 339 | len = w * h, 340 | temp = new Uint8Array(len), // auxiliary array to check uniqueness 341 | border = [], // only border points 342 | x0 = Math.max(minX, 1), 343 | x1 = Math.min(maxX, w - 2), 344 | y0 = Math.max(minY, 1), 345 | y1 = Math.min(maxY, h - 2); 346 | 347 | if (visited && visited.length > 0) { 348 | // copy visited points (only "black") 349 | for (k = 0; k < len; k++) { 350 | if (visited[k] === 1) visitedData[k] = 1; 351 | } 352 | } 353 | 354 | // walk through inner values except points on the boundary of the image 355 | for (y = y0; y < y1 + 1; y++) 356 | for (x = x0; x < x1 + 1; x++) { 357 | k = y * w + x; 358 | if (data[k] === 0) continue; // "white" point isn't the border 359 | k1 = k + w; // y + 1 360 | k2 = k - w; // y - 1 361 | // check if any neighbor with a "white" color 362 | if (visitedData[k + 1] === 0 || visitedData[k - 1] === 0 || 363 | visitedData[k1] === 0 || visitedData[k1 + 1] === 0 || visitedData[k1 - 1] === 0 || 364 | visitedData[k2] === 0 || visitedData[k2 + 1] === 0 || visitedData[k2 - 1] === 0) { 365 | //if (visitedData[k + 1] + visitedData[k - 1] + 366 | // visitedData[k1] + visitedData[k1 + 1] + visitedData[k1 - 1] + 367 | // visitedData[k2] + visitedData[k2 + 1] + visitedData[k2 - 1] == 8) continue; 368 | border.push(k); 369 | } 370 | } 371 | 372 | // walk through points on the boundary of the image if necessary 373 | // if the "black" point is adjacent to the boundary of the image, it is a border point 374 | if (minX == 0) 375 | for (y = minY; y < maxY + 1; y++) 376 | if (data[y * w] === 1) 377 | border.push(y * w); 378 | 379 | if (maxX == w - 1) 380 | for (y = minY; y < maxY + 1; y++) 381 | if (data[y * w + maxX] === 1) 382 | border.push(y * w + maxX); 383 | 384 | if (minY == 0) 385 | for (x = minX; x < maxX + 1; x++) 386 | if (data[x] === 1) 387 | border.push(x); 388 | 389 | if (maxY == h - 1) 390 | for (x = minX; x < maxX + 1; x++) 391 | if (data[maxY * w + x] === 1) 392 | border.push(maxY * w + x); 393 | 394 | var result = [], // border points with radius-neighbors 395 | start, end, 396 | endX = radius + w, 397 | endY = radius + h, 398 | n = radius * 2 + 1; // size of the pattern for radius-neighbors (from -r to +r with the center point) 399 | 400 | len = border.length; 401 | // walk through radius-neighbors of border points and add them to the result array 402 | for (j = 0; j < len; j++) { 403 | k = border[j]; // index of the border point 404 | temp[k] = 1; // mark border point 405 | result.push(k); // save the border point 406 | x = k % w; // calc x by index 407 | y = (k - x) / w; // calc y by index 408 | start = radius - x > 0 ? radius - x : 0; 409 | end = endX - x < n ? endX - x : n; // Math.min((((w - 1) - x) + radius) + 1, n); 410 | k1 = k - radius; 411 | // walk through x-neighbors 412 | for (i = start; i < end; i++) { 413 | k2 = k1 + i; 414 | if (temp[k2] === 0) { // check the uniqueness 415 | temp[k2] = 1; 416 | result.push(k2); 417 | } 418 | } 419 | start = radius - y > 0 ? radius - y : 0; 420 | end = endY - y < n ? endY - y : n; // Math.min((((h - 1) - y) + radius) + 1, n); 421 | k1 = k - radius * w; 422 | // walk through y-neighbors 423 | for (i = start; i < end; i++) { 424 | k2 = k1 + i * w; 425 | if (temp[k2] === 0) { // check the uniqueness 426 | temp[k2] = 1; 427 | result.push(k2); 428 | } 429 | } 430 | } 431 | 432 | return result; 433 | }; 434 | 435 | /** Apply the gauss-blur filter ONLY to border points with radius-neighbors 436 | * Algorithms: http://blog.ivank.net/fastest-gaussian-blur.html 437 | * http://www.librow.com/articles/article-9 438 | * http://elynxsdk.free.fr/ext-docs/Blur/Fast_box_blur.pdf 439 | * @param {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 440 | * @param {int} blur radius 441 | * @param {Uint8Array} visited: mask of visited points (optional) 442 | * @return {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 443 | */ 444 | lib.gaussBlurOnlyBorder = function(mask, radius, visited) { 445 | 446 | var border = createBorderForBlur(mask, radius, visited), // get border points with radius-neighbors 447 | ww, dsq, i, j, k, k1, x, y, val, start, end, 448 | n = radius * 2 + 1, // size of the pattern for radius-neighbors (from -r to +r with center point) 449 | s2 = 2 * radius * radius, 450 | wg = new Float32Array(n), // weights 451 | total = 0, // sum of weights(used for normalization) 452 | w = mask.width, 453 | h = mask.height, 454 | data = mask.data, 455 | minX = mask.bounds.minX, 456 | maxX = mask.bounds.maxX, 457 | minY = mask.bounds.minY, 458 | maxY = mask.bounds.maxY, 459 | len = border.length; 460 | 461 | // calc gauss weights 462 | for (i = 0; i < radius; i++) { 463 | dsq = (radius - i) * (radius - i); 464 | ww = Math.exp(-dsq / s2) / Math.PI; 465 | wg[radius + i] = wg[radius - i] = ww; 466 | total += 2 * ww; 467 | } 468 | // normalization weights 469 | for (i = 0; i < n; i++) { 470 | wg[i] /= total; 471 | } 472 | 473 | var result = new Uint8Array(data), // copy the source mask 474 | endX = radius + w, 475 | endY = radius + h; 476 | 477 | //walk through all border points for blur 478 | for (i = 0; i < len; i++) { 479 | k = border[i]; // index of the border point 480 | val = 0; 481 | x = k % w; // calc x by index 482 | y = (k - x) / w; // calc y by index 483 | start = radius - x > 0 ? radius - x : 0; 484 | end = endX - x < n ? endX - x : n; // Math.min((((w - 1) - x) + radius) + 1, n); 485 | k1 = k - radius; 486 | // walk through x-neighbors 487 | for (j = start; j < end; j++) { 488 | val += data[k1 + j] * wg[j]; 489 | } 490 | if (val > 0.5) { 491 | result[k] = 1; 492 | // check minmax 493 | if (x < minX) minX = x; 494 | if (x > maxX) maxX = x; 495 | if (y < minY) minY = y; 496 | if (y > maxY) maxY = y; 497 | continue; 498 | } 499 | start = radius - y > 0 ? radius - y : 0; 500 | end = endY - y < n ? endY - y : n; // Math.min((((h - 1) - y) + radius) + 1, n); 501 | k1 = k - radius * w; 502 | // walk through y-neighbors 503 | for (j = start; j < end; j++) { 504 | val += data[k1 + j * w] * wg[j]; 505 | } 506 | if (val > 0.5) { 507 | result[k] = 1; 508 | // check minmax 509 | if (x < minX) minX = x; 510 | if (x > maxX) maxX = x; 511 | if (y < minY) minY = y; 512 | if (y > maxY) maxY = y; 513 | } else { 514 | result[k] = 0; 515 | } 516 | } 517 | 518 | return { 519 | data: result, 520 | width: w, 521 | height: h, 522 | bounds: { 523 | minX: minX, 524 | minY: minY, 525 | maxX: maxX, 526 | maxY: maxY 527 | } 528 | }; 529 | }; 530 | 531 | /** Create a border mask (only boundary points) 532 | * @param {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 533 | * @return {Object} border mask: {Uint8Array} data, {int} width, {int} height, {Object} offset 534 | */ 535 | lib.createBorderMask = function(mask) { 536 | 537 | var x, y, k, k1, k2, 538 | w = mask.width, 539 | h = mask.height, 540 | data = mask.data, 541 | minX = mask.bounds.minX, 542 | maxX = mask.bounds.maxX, 543 | minY = mask.bounds.minY, 544 | maxY = mask.bounds.maxY, 545 | rw = maxX - minX + 1, // bounds size 546 | rh = maxY - minY + 1, 547 | result = new Uint8Array(rw * rh), // reduced mask (bounds size) 548 | x0 = Math.max(minX, 1), 549 | x1 = Math.min(maxX, w - 2), 550 | y0 = Math.max(minY, 1), 551 | y1 = Math.min(maxY, h - 2); 552 | 553 | // walk through inner values except points on the boundary of the image 554 | for (y = y0; y < y1 + 1; y++) 555 | for (x = x0; x < x1 + 1; x++) { 556 | k = y * w + x; 557 | if (data[k] === 0) continue; // "white" point isn't the border 558 | k1 = k + w; // y + 1 559 | k2 = k - w; // y - 1 560 | // check if any neighbor with a "white" color 561 | if (data[k + 1] === 0 || data[k - 1] === 0 || 562 | data[k1] === 0 || data[k1 + 1] === 0 || data[k1 - 1] === 0 || 563 | data[k2] === 0 || data[k2 + 1] === 0 || data[k2 - 1] === 0) { 564 | //if (data[k + 1] + data[k - 1] + 565 | // data[k1] + data[k1 + 1] + data[k1 - 1] + 566 | // data[k2] + data[k2 + 1] + data[k2 - 1] == 8) continue; 567 | result[(y - minY) * rw + (x - minX)] = 1; 568 | } 569 | } 570 | 571 | // walk through points on the boundary of the image if necessary 572 | // if the "black" point is adjacent to the boundary of the image, it is a border point 573 | if (minX == 0) 574 | for (y = minY; y < maxY + 1; y++) 575 | if (data[y * w] === 1) 576 | result[(y - minY) * rw] = 1; 577 | 578 | if (maxX == w - 1) 579 | for (y = minY; y < maxY + 1; y++) 580 | if (data[y * w + maxX] === 1) 581 | result[(y - minY) * rw + (maxX - minX)] = 1; 582 | 583 | if (minY == 0) 584 | for (x = minX; x < maxX + 1; x++) 585 | if (data[x] === 1) 586 | result[x - minX] = 1; 587 | 588 | if (maxY == h - 1) 589 | for (x = minX; x < maxX + 1; x++) 590 | if (data[maxY * w + x] === 1) 591 | result[(maxY - minY) * rw + (x - minX)] = 1; 592 | 593 | return { 594 | data: result, 595 | width: rw, 596 | height: rh, 597 | offset: { x: minX, y: minY } 598 | }; 599 | }; 600 | 601 | /** Create a border index array of boundary points of the mask 602 | * @param {Object} mask: {Uint8Array} data, {int} width, {int} height 603 | * @return {Array} border index array boundary points of the mask 604 | */ 605 | lib.getBorderIndices = function(mask) { 606 | 607 | var x, y, k, k1, k2, 608 | w = mask.width, 609 | h = mask.height, 610 | data = mask.data, 611 | border = [], // only border points 612 | x1 = w - 1, 613 | y1 = h - 1; 614 | 615 | // walk through inner values except points on the boundary of the image 616 | for (y = 1; y < y1; y++) 617 | for (x = 1; x < x1; x++) { 618 | k = y * w + x; 619 | if (data[k] === 0) continue; // "white" point isn't the border 620 | k1 = k + w; // y + 1 621 | k2 = k - w; // y - 1 622 | // check if any neighbor with a "white" color 623 | if (data[k + 1] === 0 || data[k - 1] === 0 || 624 | data[k1] === 0 || data[k1 + 1] === 0 || data[k1 - 1] === 0 || 625 | data[k2] === 0 || data[k2 + 1] === 0 || data[k2 - 1] === 0) { 626 | //if (data[k + 1] + data[k - 1] + 627 | // data[k1] + data[k1 + 1] + data[k1 - 1] + 628 | // data[k2] + data[k2 + 1] + data[k2 - 1] == 8) continue; 629 | border.push(k); 630 | } 631 | } 632 | 633 | // walk through points on the boundary of the image if necessary 634 | // if the "black" point is adjacent to the boundary of the image, it is a border point 635 | for (y = 0; y < h; y++) 636 | if (data[y * w] === 1) 637 | border.push(y * w); 638 | 639 | for (x = 0; x < w; x++) 640 | if (data[x] === 1) 641 | border.push(x); 642 | 643 | k = w - 1; 644 | for (y = 0; y < h; y++) 645 | if (data[y * w + k] === 1) 646 | border.push(y * w + k); 647 | 648 | k = (h - 1) * w; 649 | for (x = 0; x < w; x++) 650 | if (data[k + x] === 1) 651 | border.push(k + x); 652 | 653 | return border; 654 | }; 655 | 656 | /** Create a compressed mask with a "white" border (1px border with zero values) for the contour tracing 657 | * @param {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 658 | * @return {Object} border mask: {Uint8Array} data, {int} width, {int} height, {Object} offset 659 | */ 660 | function prepareMask(mask) { 661 | var x, y, 662 | w = mask.width, 663 | data = mask.data, 664 | minX = mask.bounds.minX, 665 | maxX = mask.bounds.maxX, 666 | minY = mask.bounds.minY, 667 | maxY = mask.bounds.maxY, 668 | rw = maxX - minX + 3, // bounds size +1 px on each side (a "white" border) 669 | rh = maxY - minY + 3, 670 | result = new Uint8Array(rw * rh); // reduced mask (bounds size) 671 | 672 | // walk through inner values and copy only "black" points to the result mask 673 | for (y = minY; y < maxY + 1; y++) 674 | for (x = minX; x < maxX + 1; x++) { 675 | if (data[y * w + x] === 1) 676 | result[(y - minY + 1) * rw + (x - minX + 1)] = 1; 677 | } 678 | 679 | return { 680 | data: result, 681 | width: rw, 682 | height: rh, 683 | offset: { x: minX - 1, y: minY - 1 } 684 | }; 685 | }; 686 | 687 | /** Create a contour array for the binary mask 688 | * Algorithm: http://www.sciencedirect.com/science/article/pii/S1077314203001401 689 | * @param {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 690 | * @return {Array} contours: {Array} points, {bool} inner, {int} label 691 | */ 692 | lib.traceContours = function(mask) { 693 | var m = prepareMask(mask), 694 | contours = [], 695 | label = 0, 696 | w = m.width, 697 | w2 = w * 2, 698 | h = m.height, 699 | src = m.data, 700 | dx = m.offset.x, 701 | dy = m.offset.y, 702 | dest = new Uint8Array(src), // label matrix 703 | i, j, x, y, k, k1, c, inner, dir, first, second, current, previous, next, d; 704 | 705 | // all [dx,dy] pairs (array index is the direction) 706 | // 5 6 7 707 | // 4 X 0 708 | // 3 2 1 709 | var directions = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]; 710 | 711 | for (y = 1; y < h - 1; y++) 712 | for (x = 1; x < w - 1; x++) { 713 | k = y * w + x; 714 | if (src[k] === 1) { 715 | for (i = -w; i < w2; i += w2) { // k - w: outer tracing (y - 1), k + w: inner tracing (y + 1) 716 | if (src[k + i] === 0 && dest[k + i] === 0) { // need contour tracing 717 | inner = i === w; // is inner contour tracing ? 718 | label++; // label for the next contour 719 | 720 | c = []; 721 | dir = inner ? 2 : 6; // start direction 722 | current = previous = first = { x: x, y: y }; 723 | second = null; 724 | while (true) { 725 | dest[current.y * w + current.x] = label; // mark label for the current point 726 | // bypass all the neighbors around the current point in a clockwise 727 | for (j = 0; j < 8; j++) { 728 | dir = (dir + 1) % 8; 729 | 730 | // get the next point by new direction 731 | d = directions[dir]; // index as direction 732 | next = { x: current.x + d[0], y: current.y + d[1] }; 733 | 734 | k1 = next.y * w + next.x; 735 | if (src[k1] === 1) // black boundary pixel 736 | { 737 | dest[k1] = label; // mark a label 738 | break; 739 | } 740 | dest[k1] = -1; // mark a white boundary pixel 741 | next = null; 742 | } 743 | if (next === null) break; // no neighbours (one-point contour) 744 | current = next; 745 | if (second) { 746 | if (previous.x === first.x && previous.y === first.y && current.x === second.x && current.y === second.y) { 747 | break; // creating the contour completed when returned to original position 748 | } 749 | } else { 750 | second = next; 751 | } 752 | c.push({ x: previous.x + dx, y: previous.y + dy }); 753 | previous = current; 754 | dir = (dir + 4) % 8; // next dir (symmetrically to the current direction) 755 | } 756 | 757 | if (next != null) { 758 | c.push({ x: first.x + dx, y: first.y + dy }); // close the contour 759 | contours.push({ inner: inner, label: label, points: c }); // add contour to the list 760 | } 761 | } 762 | } 763 | } 764 | } 765 | 766 | return contours; 767 | }; 768 | 769 | /** Simplify contours 770 | * Algorithms: http://psimpl.sourceforge.net/douglas-peucker.html 771 | * http://neerc.ifmo.ru/wiki/index.php?title=%D0%A3%D0%BF%D1%80%D0%BE%D1%89%D0%B5%D0%BD%D0%B8%D0%B5_%D0%BF%D0%BE%D0%BB%D0%B8%D0%B3%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9_%D1%86%D0%B5%D0%BF%D0%B8 772 | * @param {Array} contours: {Array} points, {bool} inner, {int} label 773 | * @param {float} simplify tolerant 774 | * @param {int} simplify count: min number of points when the contour is simplified 775 | * @return {Array} contours: {Array} points, {bool} inner, {int} label, {int} initialCount 776 | */ 777 | lib.simplifyContours = function(contours, simplifyTolerant, simplifyCount) { 778 | var lenContours = contours.length, 779 | result = [], 780 | i, j, k, c, points, len, resPoints, lst, stack, ids, 781 | maxd, maxi, dist, r1, r2, r12, dx, dy, pi, pf, pl; 782 | 783 | // walk through all contours 784 | for (j = 0; j < lenContours; j++) { 785 | c = contours[j]; 786 | points = c.points; 787 | len = c.points.length; 788 | 789 | if (len < simplifyCount) { // contour isn't simplified 790 | resPoints = []; 791 | for (k = 0; k < len; k++) { 792 | resPoints.push({ x: points[k].x, y: points[k].y }); 793 | } 794 | result.push({ inner: c.inner, label: c.label, points: resPoints, initialCount: len }); 795 | continue; 796 | } 797 | 798 | lst = [0, len - 1]; // always add first and last points 799 | stack = [{ first: 0, last: len - 1 }]; // first processed edge 800 | 801 | do { 802 | ids = stack.shift(); 803 | if (ids.last <= ids.first + 1) // no intermediate points 804 | { 805 | continue; 806 | } 807 | 808 | maxd = -1.0; // max distance from point to current edge 809 | maxi = ids.first; // index of maximally distant point 810 | 811 | for (i = ids.first + 1; i < ids.last; i++) // bypass intermediate points in edge 812 | { 813 | // calc the distance from current point to edge 814 | pi = points[i]; 815 | pf = points[ids.first]; 816 | pl = points[ids.last]; 817 | dx = pi.x - pf.x; 818 | dy = pi.y - pf.y; 819 | r1 = Math.sqrt(dx * dx + dy * dy); 820 | dx = pi.x - pl.x; 821 | dy = pi.y - pl.y; 822 | r2 = Math.sqrt(dx * dx + dy * dy); 823 | dx = pf.x - pl.x; 824 | dy = pf.y - pl.y; 825 | r12 = Math.sqrt(dx * dx + dy * dy); 826 | if (r1 >= Math.sqrt(r2 * r2 + r12 * r12)) dist = r2; 827 | else if (r2 >= Math.sqrt(r1 * r1 + r12 * r12)) dist = r1; 828 | else dist = Math.abs((dy * pi.x - dx * pi.y + pf.x * pl.y - pl.x * pf.y) / r12); 829 | 830 | if (dist > maxd) { 831 | maxi = i; // save the index of maximally distant point 832 | maxd = dist; 833 | } 834 | } 835 | 836 | if (maxd > simplifyTolerant) // if the max "deviation" is larger than allowed then... 837 | { 838 | lst.push(maxi); // add index to the simplified list 839 | stack.push({ first: ids.first, last: maxi }); // add the left part for processing 840 | stack.push({ first: maxi, last: ids.last }); // add the right part for processing 841 | } 842 | 843 | } while (stack.length > 0); 844 | 845 | resPoints = []; 846 | len = lst.length; 847 | lst.sort(function(a, b) { return a - b; }); // restore index order 848 | for (k = 0; k < len; k++) { 849 | resPoints.push({ x: points[lst[k]].x, y: points[lst[k]].y }); // add result points to the correct order 850 | } 851 | result.push({ inner: c.inner, label: c.label, points: resPoints, initialCount: c.points.length }); 852 | } 853 | 854 | return result; 855 | }; 856 | 857 | return lib; 858 | })(); 859 | 860 | if (typeof module !== "undefined" && module !== null) module.exports = MagicWand; 861 | if (typeof window !== "undefined" && window !== null) window.MagicWand = MagicWand; 862 | --------------------------------------------------------------------------------