├── .gitignore
├── .npmignore
├── LICENSE.md
├── README.md
├── demo
├── index.html
├── index.js
├── package-lock.json
└── package.json
├── index.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | demo/.cache/
4 | demo/dist/
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | demo/
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Szymon Kaliski
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 | # Interactive Shape Recognition
2 |
3 | Implementaion of "A Simple Approach to Recognise Geometric Shapes Interactively" by Joaquim A. Jorge and Manuel J. Fonseca.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm i interactive-shape-recognition
9 | ```
10 |
11 | ## Usage
12 |
13 | ```js
14 | const detectShape = require("interactive-shape-recognition");
15 |
16 | const points = [
17 | // .. [x, y] positions ..
18 | ];
19 |
20 | const { shape } = detectShape(points)
21 |
22 | // shape is one of: "CIRCLE", "LINE", "RECTANGLE", "UNKNOWN"
23 | ```
24 |
25 |
26 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Interactive Shape Recognition Demo
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | const detectShape = require("../");
2 |
3 | const [width, height] = [600, 600];
4 |
5 | const canvas = document.createElement("canvas");
6 | canvas.width = width;
7 | canvas.height = height;
8 | canvas.style.border = "1px solid #eee";
9 | document.body.appendChild(canvas);
10 |
11 | const sidebar = document.createElement("div");
12 | sidebar.style.fontFamily = "sans-serif";
13 | sidebar.style.marginLeft = "20px";
14 | document.body.appendChild(sidebar);
15 |
16 | document.body.style.display = "flex";
17 | document.body.style.padding = 0;
18 | document.body.style.margin = 0;
19 |
20 | const state = {
21 | isDown: false,
22 | points: []
23 | };
24 |
25 | const updateSidebar = () => {
26 | sidebar.innerText = `
27 | detected shape: ${state.detected.shape}
28 | time: ${state.detectionTime}ms
29 | `;
30 | };
31 |
32 | canvas.addEventListener("mousedown", e => {
33 | state.isDown = true;
34 | state.points = [[e.clientX, e.clientY]];
35 | });
36 |
37 | canvas.addEventListener("mousemove", e => {
38 | if (state.isDown) {
39 | state.points.push([e.clientX, e.clientY]);
40 | }
41 | });
42 |
43 | canvas.addEventListener("mouseup", () => {
44 | state.isDown = false;
45 |
46 | const t = Date.now();
47 | state.detected = detectShape(state.points);
48 | const dt = Date.now() - t;
49 |
50 | state.detectionTime = dt;
51 |
52 | updateSidebar();
53 | });
54 |
55 | const ctx = canvas.getContext("2d");
56 |
57 | const draw = () => {
58 | ctx.fillStyle = "#fff";
59 | ctx.fillRect(0, 0, width, height);
60 |
61 | if (state.points.length > 0) {
62 | ctx.beginPath();
63 | ctx.moveTo(state.points[0][0], state.points[0][1]);
64 |
65 | for (let i = 1; i < state.points.length; i++) {
66 | ctx.lineTo(state.points[i][0], state.points[i][1]);
67 | }
68 |
69 | ctx.stroke();
70 | }
71 |
72 | requestAnimationFrame(draw);
73 | };
74 |
75 | draw();
76 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interactive-shape-recognition-demo",
3 | "version": "1.0.0",
4 | "private": true,
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel index.html --port 3000"
8 | },
9 | "keywords": [],
10 | "author": "Szymon Kaliski (http://szymonkaliski.com)",
11 | "license": "MIT",
12 | "devDependencies": {
13 | "parcel": "^1.12.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const calculateHull = require("convex-hull");
2 | const calculateArea = require("area-polygon");
3 |
4 | const SHAPE = {
5 | CIRCLE: "CIRCLE",
6 | LINE: "LINE",
7 | RECTANGLE: "RECTANGLE",
8 | UNKNOWN: "UNKNOWN"
9 | };
10 |
11 | const dist = ([ax, ay], [bx, by]) => Math.hypot(bx - ax, by - ay);
12 |
13 | const last = xs => xs[xs.length - 1];
14 |
15 | const calcPerimeter = points => {
16 | return points
17 | .slice(0, points.length - 1)
18 | .map((p, i) => [p, points[i + 1]])
19 | .map(([a, b]) => dist(a, b))
20 | .reduce((memo, d) => memo + d, 0);
21 | };
22 |
23 | const calcLargestTriangle = points => {
24 | const len = points.length;
25 |
26 | let maxArea = 0;
27 | let maxPoints = null;
28 |
29 | for (let i = 0; i < len; i++) {
30 | for (let j = 0; j < len; j++) {
31 | for (let k = 0; k < len; k++) {
32 | if (i !== j && i !== k && k !== j) {
33 | const triangle = [points[i], points[j], points[k]];
34 | const triangleArea = calculateArea(triangle);
35 |
36 | if (triangleArea > maxArea) {
37 | maxArea = triangleArea;
38 | maxPoints = triangle.slice(0);
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
45 | return {
46 | area: maxArea,
47 | points: maxPoints
48 | };
49 | };
50 |
51 | const calcEnclosingRect = points => {
52 | const minX = Math.min(...points.map(p => p[0]));
53 | const minY = Math.min(...points.map(p => p[1]));
54 | const maxX = Math.max(...points.map(p => p[0]));
55 | const maxY = Math.max(...points.map(p => p[1]));
56 |
57 | return [[minX, minY], [maxX, minY], [maxX, maxY], [minX, maxY]];
58 | };
59 |
60 | const detectShape = points => {
61 | const hullIdxs = calculateHull(points);
62 |
63 | const hull = hullIdxs
64 | .map(h => points[h[0]])
65 | .concat([points[last(hullIdxs)[1]]]);
66 |
67 | const hullPerimeter = calcPerimeter(hull); // Pch
68 | const pointsPerimeter = calcPerimeter(points); // Len
69 |
70 | const hullArea = calculateArea(hull); // Ach
71 | const hullPerimterSq = hullPerimeter * hullPerimeter; // Pch^2
72 |
73 | const largestHullTriangle = calcLargestTriangle(hull).area; // Alt
74 |
75 | const enclosingRect = calcEnclosingRect(points);
76 | const enclosingRectPerimetr = calcPerimeter(enclosingRect); // Per
77 |
78 | const rLenPch = pointsPerimeter / hullPerimeter;
79 | const rThinness = hullPerimterSq / hullArea;
80 | const rAltAch = largestHullTriangle / hullArea;
81 | const rPchPer = hullPerimeter / enclosingRectPerimetr;
82 |
83 | const maybeCircle =
84 | rThinness > 11 && rThinness < 14 && Math.abs(1 - rLenPch) < 0.1;
85 | const maybeLine = rThinness > 100;
86 | const maybeRectangle = Math.abs(1 - rLenPch) < 0.1 && rAltAch > 0.5;
87 |
88 | const shape = maybeCircle
89 | ? SHAPE.CIRCLE
90 | : maybeLine
91 | ? SHAPE.LINE
92 | : maybeRectangle
93 | ? SHAPE.RECTANGLE
94 | : SHAPE.UNKNOWN;
95 |
96 | return {
97 | shape,
98 |
99 | maybeCircle,
100 | maybeRectangle,
101 | maybeLine,
102 |
103 | enclosingRect,
104 | hull,
105 |
106 | rLenPch,
107 | rThinness,
108 | rAltAch,
109 | rPchPer
110 | };
111 | };
112 |
113 | module.exports = detectShape;
114 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interactive-shape-recognition",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "affine-hull": {
8 | "version": "1.0.0",
9 | "resolved": "https://registry.npmjs.org/affine-hull/-/affine-hull-1.0.0.tgz",
10 | "integrity": "sha1-dj/x040GPOt+Jy8X7k17vK+QXF0=",
11 | "requires": {
12 | "robust-orientation": "^1.1.3"
13 | }
14 | },
15 | "area-polygon": {
16 | "version": "1.0.1",
17 | "resolved": "https://registry.npmjs.org/area-polygon/-/area-polygon-1.0.1.tgz",
18 | "integrity": "sha1-gCp1QIOL5Bi4rhzZnB5Mu7ktbRs="
19 | },
20 | "bit-twiddle": {
21 | "version": "1.0.2",
22 | "resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz",
23 | "integrity": "sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4="
24 | },
25 | "convex-hull": {
26 | "version": "1.0.3",
27 | "resolved": "https://registry.npmjs.org/convex-hull/-/convex-hull-1.0.3.tgz",
28 | "integrity": "sha1-IKOqbOh/St6i/30XlxyfwcZ+H/8=",
29 | "requires": {
30 | "affine-hull": "^1.0.0",
31 | "incremental-convex-hull": "^1.0.1",
32 | "monotone-convex-hull-2d": "^1.0.1"
33 | }
34 | },
35 | "incremental-convex-hull": {
36 | "version": "1.0.1",
37 | "resolved": "https://registry.npmjs.org/incremental-convex-hull/-/incremental-convex-hull-1.0.1.tgz",
38 | "integrity": "sha1-UUKMFMudmmFEv+abKFH7N3M0vh4=",
39 | "requires": {
40 | "robust-orientation": "^1.1.2",
41 | "simplicial-complex": "^1.0.0"
42 | }
43 | },
44 | "monotone-convex-hull-2d": {
45 | "version": "1.0.1",
46 | "resolved": "https://registry.npmjs.org/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz",
47 | "integrity": "sha1-R/Xa6t88Sv03dkuqGqh4ekDu4Iw=",
48 | "requires": {
49 | "robust-orientation": "^1.1.3"
50 | }
51 | },
52 | "robust-orientation": {
53 | "version": "1.1.3",
54 | "resolved": "https://registry.npmjs.org/robust-orientation/-/robust-orientation-1.1.3.tgz",
55 | "integrity": "sha1-2v9bANO+TmByLw6cAVbvln8cIEk=",
56 | "requires": {
57 | "robust-scale": "^1.0.2",
58 | "robust-subtract": "^1.0.0",
59 | "robust-sum": "^1.0.0",
60 | "two-product": "^1.0.2"
61 | }
62 | },
63 | "robust-scale": {
64 | "version": "1.0.2",
65 | "resolved": "https://registry.npmjs.org/robust-scale/-/robust-scale-1.0.2.tgz",
66 | "integrity": "sha1-d1Ey7QlULQKOWLLMecBikLz3jDI=",
67 | "requires": {
68 | "two-product": "^1.0.2",
69 | "two-sum": "^1.0.0"
70 | }
71 | },
72 | "robust-subtract": {
73 | "version": "1.0.0",
74 | "resolved": "https://registry.npmjs.org/robust-subtract/-/robust-subtract-1.0.0.tgz",
75 | "integrity": "sha1-4LFk4e2LpOOl3aRaEgODSNvtPpo="
76 | },
77 | "robust-sum": {
78 | "version": "1.0.0",
79 | "resolved": "https://registry.npmjs.org/robust-sum/-/robust-sum-1.0.0.tgz",
80 | "integrity": "sha1-FmRuUlKStNJdgnV6KGlV4Lv6U9k="
81 | },
82 | "simplicial-complex": {
83 | "version": "1.0.0",
84 | "resolved": "https://registry.npmjs.org/simplicial-complex/-/simplicial-complex-1.0.0.tgz",
85 | "integrity": "sha1-bDOk7Wn81Nkbe8rdOzC2NoPq4kE=",
86 | "requires": {
87 | "bit-twiddle": "^1.0.0",
88 | "union-find": "^1.0.0"
89 | }
90 | },
91 | "two-product": {
92 | "version": "1.0.2",
93 | "resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz",
94 | "integrity": "sha1-Z9ldSyV6kh4stL16+VEfkIhSLqo="
95 | },
96 | "two-sum": {
97 | "version": "1.0.0",
98 | "resolved": "https://registry.npmjs.org/two-sum/-/two-sum-1.0.0.tgz",
99 | "integrity": "sha1-MdPzIjnk9zHsqd+RVeKyl/AIq2Q="
100 | },
101 | "union-find": {
102 | "version": "1.0.2",
103 | "resolved": "https://registry.npmjs.org/union-find/-/union-find-1.0.2.tgz",
104 | "integrity": "sha1-KSusQV5q06iVNdI3AQ20pTYoTlg="
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interactive-shape-recognition",
3 | "version": "1.0.1",
4 | "description": "Implementaion of \"A Simple Approach to Recognise Geometric Shapes Interactively\" by Joaquim A. Jorge and Manuel J. Fonseca.",
5 | "main": "index.js",
6 | "keywords": [],
7 | "author": "Szymon Kaliski (http://szymonkaliski.com)",
8 | "license": "MIT",
9 | "dependencies": {
10 | "area-polygon": "^1.0.1",
11 | "convex-hull": "^1.0.3"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------