diff --git a/front/script/algorithm.js b/front/script/algorithm.js index b142404..f8422f3 100644 --- a/front/script/algorithm.js +++ b/front/script/algorithm.js @@ -1,8 +1,15 @@ -const CIRCLE_START = 0; -const CIRCLE_END = 2 * Math.PI; -const PIXEL_DATA_LENGTH = 4; +const DIAGONAL = Math.sqrt(2); + +const RAYS_COUNT = 32; + +const MAX_PATH_ITERATIONS = 4000; + +const PIX_DATA_LEN = 4; const BLACKWHITE_THRESHOLD = 80; +const BLACKWHITE_MID_THRESHOLD = 128; + +const MIN_LINE_LEN = 40; const cvs1 = document.getElementById('canvas1'); const cvs2 = document.getElementById('canvas2'); @@ -11,8 +18,8 @@ const ctx2 = cvs2.getContext('2d'); cvs1.width = 700; cvs1.height = 700; -const videoW = 640; -const videoH = 480; +const videoW = 933; +const videoH = 700; const videoOffsetX = (cvs1.width - videoW) / 2; const videoOffsetY = (cvs1.height - videoH) / 2; @@ -23,9 +30,9 @@ let correctPercentage = [ {min: 0, max: 0} ]; -let imageMid = {x: 350, y: 350} +const imageMid = new Vector2(350, 350); -radiuses = [10, 100, 300]; +const radiuses = [10, 100, 300]; const circleWidth = 0.5; @@ -35,7 +42,7 @@ let img = new Image(); img.src = './assets/img/scode_example2.png'; function clearCanvas(canvas) { - const ctx = canvas.getContext("2d"); + const ctx = canvas.getContext('2d'); ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); } @@ -44,10 +51,22 @@ const square = (x) => x * x; const checkCircle = (x, y, radius) => Math.abs(Math.sqrt(square(x - imageMid.x) + square(y - imageMid.y)) - radius) < circleWidth; +const lineVec = (line) => line.end.subtract(line.start); + +const lineLen = (line) => lineVec(line).length(); + +const getLineMid = (line) => line.end.add(line.start).divide(2); + +const getOppositeRayIndx = (index, raysCount = RAYS_COUNT) => (index + RAYS_COUNT / 2) % RAYS_COUNT; + +const getLeftPerpendicularRayIndx = (ndex, raysCount = RAYS_COUNT) => (index + RAYS_COUNT / 4) % RAYS_COUNT; + +const getRightPerpendicularRayIndx = (index, raysCount = RAYS_COUNT) => (index + 3 * RAYS_COUNT / 4) % RAYS_COUNT; + function blackwhite(img) { const picLength = img.width * img.height; - for (let i = 0; i < picLength * PIXEL_DATA_LENGTH; i += PIXEL_DATA_LENGTH) { + for (let i = 0; i < picLength * PIX_DATA_LEN; i += PIX_DATA_LEN) { const R = img.data[i]; const G = img.data[i + 1]; const B = img.data[i + 2]; @@ -61,18 +80,20 @@ function blackwhite(img) { } } -function checkIfScode(img){ +function checkIfScode(matrix){ const amounts = [0, 0, 0]; const blackAmounts = [0, 0, 0]; - for(let y = 0; y < img.height; y++){ - for(let x = 0; x < img.width; x++){ - const i = (y * img.width + x) * 4; - const R = img.data[i]; + const h = matrix.length; + const w = matrix[0].length; + + for(let y = 0; y < h; y++){ + for(let x = 0; x < w; x++){ + const pix = matrix[x][y]; for(let i = 0; i < 3; i++){ if(checkCircle(x, y, radiuses[i])){ amounts[i]++; - if(R === 0){ + if(pix){ blackAmounts[i]++; } } @@ -94,6 +115,260 @@ function checkIfScode(img){ return true; } +function getMatrix(img){ + const matrix = []; + const row = img.width; + const column = img.height; + for(let j = 0; j < column; j ++){ + matrix[j] = []; + for(let i = 0; i < row; i++){ + const ind = PIX_DATA_LEN * j * row + PIX_DATA_LEN * i; + const x = i - imageMid.x; + const y = j - imageMid.y; + const pixel = Math.sqrt(x * x + y * y) < radiuses[2] && img.data[ind] < BLACKWHITE_MID_THRESHOLD ? 1 : 0; + matrix[j].push(pixel); + } + } + return matrix; +} + +function exploreLine(matrix, startPos = new Vector2()){ + const h = matrix.length; + const w = matrix[0].length; + const openedNodes = []; + const closedNodes = []; + if(!matrix[startPos.y][startPos.x]){ + console.log('fail'); + return null; + } + const startNode = { pos: startPos, dist: 0, parent: null }; + openedNodes.push(startNode); + let curr = startNode; + let counter = 0; + let bestNode = curr; + while(openedNodes.length > 0 && counter < MAX_PATH_ITERATIONS){ + counter++; + if(counter === MAX_PATH_ITERATIONS) throw(new Error('too many iterations')); + let bestOpenNode = openedNodes[0]; + for (let i = 1; i < openedNodes.length; i++){ + let nextNode = openedNodes[i]; + if (nextNode.dist > bestOpenNode.dist){ + bestOpenNode = nextNode; + } + } + if(bestOpenNode.dist > bestNode.dist){ + bestNode = bestOpenNode; + } + + curr = bestOpenNode; + openedNodes.splice(openedNodes.indexOf(curr), 1); + closedNodes.push(curr); + for(let j = -1; j <= 1; j++){ + for(let i = -1; i <= 1; i++){ + if((i !== 0 || j !== 0)){ + const x = curr.pos.x + i; + const y = curr.pos.y + j; + if(x < 0 || y < 0 || x >= w || y >= h) continue; + if(!matrix[y][x]) continue; + let nodeClosed = false; + for (const node of closedNodes){ + if (node.pos.x === curr.pos.x + i && node.pos.y === curr.pos.y + j) { + nodeClosed = true; + break; + } + } + const newG = curr.dist + (Math.abs(i) + Math.abs(j) === 1 ? 1 : DIAGONAL); + + if (!nodeClosed){ + const neighbour = { pos: curr.pos.add(new Vector2(i, j)), dist: newG , parent: curr }; + + let nodeOpened = false; + for (const node of openedNodes){ + if (Vector2.equals(node.pos, neighbour.pos)){ + nodeOpened = true; + } + } + if(!nodeOpened || newG < neighbour.dist){ + neighbour.dist = newG; + if(!nodeOpened) openedNodes.push(neighbour); + } + } + } + } + } + if (openedNodes.length === 0){ + const res = Object.create(null); + let parent = bestNode; + while (parent.parent) { + parent = parent.parent; + if(!parent.parent) res.start = parent.pos; + } + res.end = bestNode.pos; + return res; + } + } +} + +function getLine(matrix, startPos){ + const line = exploreLine(matrix, startPos); + const endPos = line.end; + const line2 = exploreLine(matrix, endPos); + + ctx2.strokeStyle = 'green'; + ctx2.lineWidth = 4; + + drawline(ctx2, line2.start, line2.end); + + ctx2.strokeStyle = 'red'; + ctx2.lineWidth = 2; + + drawCircle(ctx2, line2.start); + drawCircle(ctx2, line2.end); + + return line2; +} + +function findDistanceToLine(point, linePos, lineNormVector){ + + const A = lineNormVector.x; + const B = lineNormVector.y; + const C = - A * linePos.x - B * linePos.y; + + return Math.abs(A * point.x + B * point.y + C) / Math.sqrt(A * A + B * B); +} + +function findlines(matrix){ + let lines = []; + const h = matrix.length; + const w = matrix[0].length; + + for(let j = 0; j < h; j++){ + for(let i = 0; i < w; i++){ + if(!matrix[j][i]) continue; + + let lineExists = false; + + for(const line of lines){ + + const v = line.end.subtract(line.start); + const n = v.rotate(Math.PI / 2); + + const point = new Vector2(i, j); + + const dist = findDistanceToLine(point, line.start, n); + + const mid = line.start.add(line.end).divide(2); + + const dist2 = findDistanceToLine(point, mid, v); + + const border = 3; + const len = v.length() / 2; + if(dist < border && dist2 <= len + border){ + lineExists = true; + break; + } + } + + if(lineExists) continue; + + const line = getLine(matrix, new Vector2(i, j)); + lines.push(line); + } + } + console.log('Found lines: ' + lines.length); + lines = lines.filter((line) => lineLen(line) > MIN_LINE_LEN); + return lines; +} + +function sortLines(lines){ + const getLinePosAngle = (line) => { + const vector = getLineMid(line).subtract(imageMid); + return Vector2.getAngle(vector); + } + + const compare = (a, b) => getLinePosAngle(a) - getLinePosAngle(b); + lines.sort(compare); +} + +function getLineLengths(linelengths){ + let startLen = linelengths[0]; + let longestLine = startLen; + let shortestLine = startLen; + for(let i = 1; i < linelengths.length; i++){ + const len = linelengths[i]; + longestLine = len > longestLine ? len : longestLine; + shortestLine = len < shortestLine ? len : shortestLine; + } + const maxlen = longestLine - shortestLine; + const rays = []; + for(const line of linelengths){ + let rayValue = Math.round((line - shortestLine) / maxlen * 15); + rays.push(rayValue) + } + return rays; +} + +function normalizeLengths(lines){ + const lengths = lines.map((line) => lineLen(line)); + + let startLen = lineLen(lines[0]) + let longestLine = startLen; + let shortestLine = startLen; + for(let i = 1; i < lines.length; i++){ + const len = lineLen(lines[i]); + longestLine = len > longestLine ? len : longestLine; + shortestLine = len < shortestLine ? len : shortestLine; + } + const maxlen = longestLine - shortestLine; + const rays = []; + for(const line of lines){ + let rayValue = Math.floor((lineLen(line) - shortestLine) / maxlen * 16); + rayValue = rayValue >= 16 ? 15 : rayValue; + rays.push(rayValue) + } + + const len = rays.length; + const sides = 4; + const step = len / sides; + + let startFound = false; + let iterator = 0; + const neededLen = [15, 15, 0, 15]; + + while (!startFound && iterator < len) { + let correct = true; + + neededLen.forEach((item, i) => { + const ind = (iterator + step * i) % len; + if (item !== rays[ind]) { + correct = false; + } + }); + + if (correct) { + startFound = true; + } else { + iterator++; + } + } + + if (!startFound) throw new Error('Something is wrong...'); + const stabilizedLines = lines.slice(iterator).concat(lines.slice(0, iterator)); + const stabilized = lengths.slice(iterator).concat(lengths.slice(0, iterator)); + + const top = 0; + const right = step; + const left = step * 3; + + const coef = ((stabilized[right] + stabilized[left]) / 2) / stabilized[top]; + + for(let i = 0; i < stabilizedLines.length; i++){ + let angle = -Vector2.getAngle(lineVec(stabilizedLines[i])) - -Vector2.getAngle(lineVec(stabilizedLines[0]).rotate(-Math.PI / 2)); + stabilized[i] += stabilized[i] * Math.abs(Math.cos(angle)) * (1 - coef); + } + return stabilized; +} + function onLoad(){ cvs2.width = img.width; cvs2.height = img.height; @@ -106,25 +381,37 @@ function onLoad(){ img = ctx2.getImageData(0, 0, img.width, img.height); blackwhite(img); + ctx2.putImageData(img, 0, 0); + + ctx2.strokeStyle = 'red'; + + for(let i = 0; i < 3; i++){ + drawCircle(ctx2, imageMid, radiuses[i]); + } + + const matrix = getMatrix(img); - if(checkIfScode(img)){ + if(checkIfScode(matrix)){ console.log('Image is scode!'); } else { - console.log('Image is not scode'); + console.log('Image is not scode'); + return; } - ctx2.putImageData(img, 0, 0); + const lines = findlines(matrix); - ctx2.strokeStyle = 'red'; + sortLines(lines); - for(let i = 0; i < 3; i++){ - ctx2.beginPath(); - ctx2.arc(imageMid.x, imageMid.y, radiuses[i], CIRCLE_START, CIRCLE_END); - ctx2.stroke(); - } + const rays = getLineLengths(normalizeLengths(lines)); + console.log('RecognizedRays:', rays); + getLink(rays, (response) => { + console.log(response); + const { link } = response; + $('#response-video').attr('src', link); + }) } -document.getElementById("makePhoto").addEventListener("click", function() { +document.getElementById('makePhoto').addEventListener('click', function() { clearCanvas(cvs1); ctx1.drawImage(video, videoOffsetX, videoOffsetY, videoW, videoH); img = new Image(); diff --git a/front/script/script.js b/front/script/script.js index 13ef939..c728ef0 100644 --- a/front/script/script.js +++ b/front/script/script.js @@ -30,6 +30,7 @@ const onInput = () => { } getRays(link, (data) => { + console.log(data.rays); drawScode(data.rays, bg, color); }) } diff --git a/front/script/utils.js b/front/script/utils.js index 82479b5..570296f 100644 --- a/front/script/utils.js +++ b/front/script/utils.js @@ -1,3 +1,5 @@ +const CIRCLE_START = 0; +const CIRCLE_END = 2 * Math.PI; function drawline(ctx, a, b){ ctx.beginPath(); @@ -5,3 +7,53 @@ function drawline(ctx, a, b){ ctx.lineTo(b.x, b.y); ctx.stroke(); } + +function drawCircle(ctx, pos, radius = 3){ + ctx.beginPath(); + ctx.arc(pos.x, pos.y, radius, CIRCLE_START, CIRCLE_END); + ctx.stroke(); +} + +class Vector2 { + constructor(_x = 0, _y = 0) { + this.x = _x; + this.y = _y; + } + + static getAngle = (vector) => Math.acos(vector.x / vector.length()) * Math.sign(vector.y); + + static makeFromAngle = (magnitude, _angle) => { + return new Vector2(Math.cos(_angle) * magnitude, Math.sin(_angle) * magnitude); + } + + static equals (vector1, vector2){ + return vector1.x === vector2.x && vector1.y === vector2.y; + } + + length = () => Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2)); + normalized = () =>{ + return new Vector2(this.x / this.length(), this.y / this.length()) + } + add = (_vector) => { + return new Vector2(this.x + _vector.x, this.y + _vector.y) + } + subtract = (_vector) => { + return new Vector2(this.x - _vector.x, this.y - _vector.y) + } + neg = () => { + return new Vector2(-this.x, -this.y) + } + + rotate = (_angle) => { + return Vector2.makeFromAngle(this.length(), Vector2.getAngle(this) + _angle); + } + + multiply = (n) => new Vector2(this.x * n, this.y * n); + + divide = (n) => new Vector2(this.x / n, this.y / n); + + ToString = () => { + return '{ ' + Math.round(this.x) + ', ' + Math.round(this.y) + ' }'; + } + +}