├── .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 | [](https://www.npmjs.org/package/magic-wand-tool)
3 | [](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 |
--------------------------------------------------------------------------------