////////////////////////////////////////// // Sketchy.js // ////////////////////////////////////////// // // JavaScript Shape Matching // Works best with Raphael SketchPad // Development started March 2013 // // Immediately invoke an anonymous function to keep the global scope clean. // Parameters: // - global: will be the global object; called with "this" from global scope // - undefined: keeps "undefined" undefined; no 2nd arg will make it undefined (function(global, undefined) { // Namespace everything global.Sketchy = {}; /* Jordan's Algorithms */ // Test function for front-end application development Sketchy.randomShapeMatch = function(shape1, shape2) { return Math.random(); }; /* Kyle's Algorithms */ // Takes in SVG data (from Raphael SketchPad) and outputs an array of paths, // each of which is an array of points in {x: Number, y: Number} format. // This is useful for preprocessing for Simplify.js or any other algorithm // operating on simple paths. Sketchy.convertSVGtoPointArrays = function(json) { var i, splitPath, j, point, paths; paths = []; json = JSON.parse(json); for(i=0; i\n\n"; svgXML = ""; // width=\"100%\" height=\"100%\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"> "; if(!parsed) json = JSON.parse(json); for(i=0; i"; } svgXML += "\n"; return svgXML; }; // shape1 and shape2 should be stringified JSON data from Raphael SketchPad Sketchy.shapeContextMatch = function(shape1, shape2) { var pointsPerShape = 25, // constant... 25 is pretty fast... 50 is probably best points1, points2, // 0.125 gives a bin out to points 2x the average distanceBinSmallest = 0.125, distanceBinCount = 5, distanceMatrix1, distanceTotal1, distanceMean1, distanceBins1, distanceMatrix2, distanceTotal2, distanceMean2, distanceBins2, angleBinCount = 12, angleMatrix1, angleBins1, angleMatrix2, angleBins2, costMatrix, distanceBinNumber1, angleBinNumber1, distanceBinNumber2, angleBinNumber2, ksum, compare, i, j, k, result; // Scatter points around each of the paths. The algorithm // will only be using these points (as feature descriptors), // not the shapes. points1 = Sketchy.scatterPoints(Sketchy.convertSVGtoPointArrays(shape1), pointsPerShape); points2 = Sketchy.scatterPoints(Sketchy.convertSVGtoPointArrays(shape2), pointsPerShape); // Create a square 2D array and initialize it with 0s in the diagonal distanceMatrix1 = []; distanceMatrix2 = []; for(i=0; i= 2, we will manually add the first and last points // // Add the first // point = path[0]; // result = [{x:point.x, y:point.y}]; // for(i=1; i= case (yes, it could happen in if or else) if(distanceToNextPoint <= delta-distanceCovered) { // Simply move to the next point currX = path[nextPathIndex].x; currY = path[nextPathIndex].y; nextPathIndex++; distanceCovered += distanceToNextPoint; } else { // Move partially angleToNextPoint = Math.atan2(path[nextPathIndex].y - currY, path[nextPathIndex].x - currX); currX = currX + Math.cos(angleToNextPoint) * (delta-distanceCovered); currY = currY + Math.sin(angleToNextPoint) * (delta-distanceCovered); distanceCovered = delta; } } while(distanceCovered < delta); // TODO: discretize currX and currY before pushing? result.push({x: currX, y: currY}); } // Manually add on the last point result.push(path[path.length-1]); return result; }; /* Betim's Algorithms */ // Compute the directed hausdorff distance of pixels1 and pixels2. // Calculate the lowest upper bound over all points in shape1 // of the distances to shape2. // TODO: Make it faster! Sketchy.hausdorff = function(points1, points2, vector2D) { var h_max = Number.MIN_VALUE, h_min, dis; for (var i = 0; i < points1.length; i++) { h_min = Number.MAX_VALUE; for (var j = 0; j < points2.length; j++) { dis = Sketchy.euclideanDistance(points1[i].x,points1[i].y,points2[j].x+vector2D.x,points2[j].y+vector2D.y); if (dis < h_min) { h_min = dis; } else if (dis == 0) {break;} } if (h_min > h_max) h_max = h_min; } return h_max; } // Compute hausdorffDistance hausdorff(shape1, shape2) and hausdorff(shape2, shape1) and return // the maximum value. Sketchy.hausdorffDistance = function(shape1, shape2, center1, center2) { var points1 = [], points2 = []; var c1 = document.getElementById(shape1); var c2 = document.getElementById(shape2); var ctx1 = c1.getContext('2d'); var ctx2 = c2.getContext('2d'); var idata1 = ctx1.getImageData(0,0,c1.width,c1.height); var idata2 = ctx2.getImageData(0,0,c2.width,c2.height); for (var y1=0; y10) { points1.push({x:x1, y:y1}); } if (idata2.data[(x1+y1*c1.width)*4+3]>0) { points2.push({x:x1, y:y1}); } } } var vector2D = {x:center1.x-center2.x, y:center1.y-center2.y}; var h1 = Sketchy.hausdorff(points1, points2, vector2D); vector2D.x = -1*vector2D.x; vector2D.y = -1*vector2D.y; var h2 = Sketchy.hausdorff(points2, points1, vector2D); var accuracy =Math.max(h1,h2) ; return 1 - Math.pow(accuracy*Math.sqrt(2)/300, 1/1.4); }; Sketchy.secondMoment = function(shape) { var c = document.getElementById(shape); var ctx = c.getContext('2d'); var idata = ctx.getImageData(0,0,c.width,c.height); var d = idata.data; var x, y; var moment = {x:0, y:0}; var _x=0, _y=0, size=0; for (y=0; y0) { _x+=x; _y+=y; size++; } } } moment.x = _x/size; moment.y = _y/size; return moment; }; // TODO: Opening and Closing Operation Sketchy.morphologyOperation = function(shape, frame_size, operation) { return 0; }; Sketchy.bottleneckDistance = function(shape1, shape2) { return 0; }; // The Hungarian Algorithm // This is also published as a standalone library: Hungarian.js // See: https://github.com/kjkjava/Hungarian.js // This version is modified to return the function instead of set // it as a global variable. // This algorithm is used in shapeContextMatch to pair up related // points with minimum cost (which is a normalized distance error). Sketchy.hungarian = (function() { // Expose just the hgAlgorithm method // Usage: hungarian(matrix, [isProfitMatrix=false], [returnSum=false]) return hgAlgorithm; // isProfitMatrix is optional, but if it exists and is the value // true, the costs will be treated as profits // returnSum is also optional, but will // sum up the chosen costs/profits and return that instead // of the assignment matrix. function hgAlgorithm(matrix, isProfitMatrix, returnSum) { var cost, maxWeight, i, j, mask = [], // the mask array: [matrix.length] x [matrix[0].length] rowCover = [], // the row covering vector: [matrix.length] colCover = [], // the column covering vector: [matrix[0].length] zero_RC = [0,0], // position of last zero from Step 4: [2] path = [], // [matrix.length * matrix[0].length + 2] x [2] step = 1, done = false, // Number.MAX_VALUE causes overflow on profits. // Should be larger or smaller than all matrix values. (i.e. -1 or 999999) forbiddenValue = -1, assignments = [], // [min(matrix.length, matrix[0].length)] x [2] assignmentsSeen; // Create the cost matrix, so we can work without modifying the // original input. cost = copyOf(matrix); // If it's a rectangular matrix, pad it with a forbidden value (MAX_VALUE). // Whether they are chosen first or last (profit or cost, respectively) // should not matter, as we will not include assignments out of range anyway. makeSquare(cost, forbiddenValue); if(isProfitMatrix === true) { maxWeight = findLargest(cost); for(i=0; i cost[i][j]) { minVal = cost[i][j]; } } for(j=0; j= mask.length) { step = 7; } else { step = 4; } return step; } function hg_step4(step, cost, mask, rowCover, colCover, zero_RC) { // Find an uncovered zero in cost and prime it (if none, go to Step 6). // Check for star in same row: if yes, cover the row and uncover the // star's column. Repeat until no uncovered zeros are left and go to // Step 6. If not, save location of primed zero and go to Step 5. var row_col = [0,0], // size: 2, holds row and column of uncovered zero done = false, j, starInRow; while(!done) { row_col = findUncoveredZero(row_col, cost, rowCover, colCover); if(row_col[0] === -1) { done = true; step = 6; } else { // Prime the found uncovered zero mask[row_col[0]][row_col[1]] = 2; starInRow = false; for(j=0; j= cost.length) { done = true; } } return row_col; } function hg_step5(step, mask, rowCover, colCover, zero_RC, path) { // Construct series of alternating primes and stars. Start with prime // from step 4. Take star in the same column. Next, take prime in the // same row as the star. Finish at a prime with no star in its column. // Unstar all stars and star the primes of the series. Erase any other // primes. Reset covers. Go to Step 3. var count, done, r, c; count = 0; // Counts rows of the path matrix path[count][0] = zero_RC[0]; // Row of last prime path[count][1] = zero_RC[1]; // Column of last prime done = false; while(!done) { r = findStarInCol(mask, path[count][1]); if(r >= 0) { count = count+1; path[count][0] = r; // Row of starred zero path[count][1] = path[count-1][1]; // Column of starred zero } else { done = true; } if(!done) { c = findPrimeInRow(mask, path[count][0]); count = count+1; path[count][0] = path[count-1][0]; // Row of primed zero path[count][1] = c; } } convertPath(mask, path, count); clearCovers(rowCover, colCover); erasePrimes(mask); step = 3; return step; } // Auxiliary function for hg_step5 function findStarInCol(mask, col) { var r, i; // Again, this is a check value r = -1; for(i=0; i cost[i][j]) { minVal = cost[i][j]; } } } return minVal; } // Takes in a 2D array and finds the largest element // This is used in the Hungarian algorithm if the user chooses "max" // (indicating their matrix values represent profit) so that cost values // are subtracted from the largest value. function findLargest(matrix) { var i, j, largest = Number.MIN_VALUE; for(i=0; i largest) { largest = matrix[i][j]; } } } return largest; } // Copies all elements of a 2D array to a new array function copyOf(original) { var i, j, copy = []; for(i=0; i cols) { // Pad on some extra columns on the right. for(i=0; i