├── .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 | --------------------------------------------------------------------------------