I wish to correct the perspective of an image by selecting 4 points of a 3d representation of a square and map those to points to a 2d square.
I have followed two very rich examples but have been unable to reproduce the desired results, here are the original examples as well as my adaptation of those on jsfiddle:
example 1 (article describing the solution): this example shows (in CoffeeScript) how to correct the perspective of a video, so this example is very similar to what I need. Here is my adaptation of it, you must click on the 4 corners clockwise starting with the upper left corner of the chessboard: my jsfiddle adaptation
var srcImg, width, height, srcCvs, srcC, srcRect, dstCvs, dstC, widthToHeight; var nbClicks = 0, coordinates = Array( 8 ); srcImg = document.getElementById( 'sourceImg' ); widthToHeight = srcImg.width / srcImg.height; srcCvs = document.getElementById( 'sourceCanvas' ); srcC = srcCvs.getContext( '2d' ); dstCvs = document.getElementById( 'destinationCanvas' ); dstC = dstCvs.getContext( '2d' ); width = srcCvs.width = dstCvs.width = srcCvs.clientWidth; height = srcCvs.height = dstCvs.height = srcCvs.clientWidth / widthToHeight; srcRect = srcCvs.getBoundingClientRect(); srcC.strokeStyle = '#0f0'; srcC.drawImage( srcImg, 0, 0, width, height ); srcCvs.addEventListener( 'click', doClick, false ); function doClick( event ) { if ( nbClicks < 4 ) { coordinates[ nbClicks * 2 ] = ( event.clientX - srcRect.left ) * width / srcRect.width; coordinates[ nbClicks * 2 + 1 ] = ( event.clientY - srcRect.top ) * height / srcRect.height; srcC.strokeRect( coordinates[ nbClicks * 2 ] - 10, coordinates[ nbClicks * 2 + 1 ] - 10, 20, 20 ); } if ( ++nbClicks == 4 ) { dstC.beginPath(); dstC.moveTo( coordinates[ 0], coordinates[ 1 ] ); for( i = 1; i < 4; i ++ ) { dstC.lineTo( coordinates[ i*2 ], coordinates[ i*2 + 1 ] ); } dstC.closePath(); dstC.clip(); var t = getTransform( left, top, w, h ); dstCvs.style.visibility = 'visible'; dstC.drawImage( srcImg, 0, 0, width, height ); var left = 0, top = 0, w = width, h = width; srcC.strokeStyle = '#f00'; srcC.strokeRect( left - 10, top - 10, 20, 20 ); srcC.strokeRect( left + w - 10, top - 10, 20, 20 ); srcC.strokeRect( left + w - 10, top + h - 10, 20, 20 ); srcC.strokeRect( left - 10, top + h - 10, 20, 20 ); alert( 'Clipped image is now drawn, going to apply transform after this alert.' ); var t = getTransform( left, top, w, w ); dstCvs.style.transform = t; } }; function getTransform( left, top, w, h ) { var minX = Math.min( coordinates[ 0 ], coordinates[ 6 ] ); var minY = Math.min( coordinates[ 1 ], coordinates[ 3 ] ); var w = Math.max( Math.abs( coordinates[ 2 ] - coordinates[ 0 ] ), Math.abs( coordinates[ 6 ] - coordinates[ 4 ] ) ); var h = Math.max( Math.abs( coordinates[ 3 ] - coordinates[ 1 ] ), Math.abs( coordinates[ 7 ] - coordinates[ 5 ] ) ); var c = coordinates; for ( var i = 0; i < 4; i ++ ) { c[ i * 2 ] = coordinates[ i ] - minX; c[ i * 2 + 1 ] = coordinates[ i * 2 + 1 ] - minY; } var l=t=0; var from = c; var to = [ left, top, left + w, top, left + w, top + h, left, top + h ]; A = []; b = []; for ( var i = 0; i < 4; i ++ ) { A.push( [ from[ i * 2 ], from[ i * 2 + 1 ], 1, 0, 0, 0, -from[ i * 2 ] * to[ i * 2 ], -from[ i * 2 + 1 ] * to[ i * 2 ] ] ); A.push( [ 0, 0, 0, from[ i * 2 ], from[ i * 2 + 1 ], 1, -from[ i * 2 ] * to[ i * 2 + 1 ], -from[ i * 2 + 1 ] * to[ i * 2 + 1 ] ] ); b.push( to[ i * 2 ] ); b.push( to[ i * 2 + 1 ] ); } h = numeric.solve(A, b); H = [[h[0], h[1], 0, h[2]], [h[3], h[4], 0, h[5]], [ 0, 0, 1, 0], [h[6], h[7], 0, 1]]; return "matrix3d(" + H.join(", ") + ")"; }
example 2 (article describing the solution): this example shows how to apply perspective to an element that has none, so my problem is the reverse of this. However, it seems that the general transform applies one set of points to another so my understanding is that this should be equivalent... but I'm probably wrong somewhere ! Here again you must click on the 4 corners clockwise starting with the upper left corner of the chessboard: my jsfiddle adaptation
var srcImg, width, height, srcCvs, srcC, srcRect, dstCvs, dstC, widthToHeight; var nbClicks = 0, coordinates = Array( 8 ); srcImg = document.getElementById( 'sourceImg' ); widthToHeight = srcImg.width / srcImg.height; srcCvs = document.getElementById( 'sourceCanvas' ); srcC = srcCvs.getContext( '2d' ); dstCvs = document.getElementById( 'destinationCanvas' ); dstC = dstCvs.getContext( '2d' ); width = srcCvs.width = dstCvs.width = srcCvs.clientWidth; height = srcCvs.height = dstCvs.height = srcCvs.clientWidth / widthToHeight; srcRect = srcCvs.getBoundingClientRect(); srcC.strokeStyle = '#0f0'; srcC.drawImage( srcImg, 0, 0, width, height ); srcCvs.addEventListener( 'click', doClick, false ); function doClick( event ) { if ( nbClicks < 4 ) { coordinates[ nbClicks * 2 ] = ( event.clientX - srcRect.left ) * width / srcRect.width; coordinates[ nbClicks * 2 + 1 ] = ( event.clientY - srcRect.top ) * height / srcRect.height; srcC.strokeRect( coordinates[ nbClicks * 2 ] - 10, coordinates[ nbClicks * 2 + 1 ] - 10, 20, 20 ); } if ( ++nbClicks == 4 ) { dstC.beginPath(); dstC.moveTo( coordinates[ 0], coordinates[ 1 ] ); for( i = 1; i < 4; i ++ ) { dstC.lineTo( coordinates[ i*2 ], coordinates[ i*2 + 1 ] ); } dstC.closePath(); dstC.clip(); dstCvs.style.visibility = 'visible'; dstC.drawImage( srcImg, 0, 0, width, height ); var left = 0, top = 0, w = width, h = width; srcC.strokeStyle = '#f00'; srcC.strokeRect( left - 10, top - 10, 20, 20 ); srcC.strokeRect( left + w - 10, top - 10, 20, 20 ); srcC.strokeRect( left + w - 10, top + h - 10, 20, 20 ); srcC.strokeRect( left - 10, top + h - 10, 20, 20 ); alert( 'Clipped image is now drawn, going to apply transform after this alert. On the left canvas, the positions of the mapped points are drawn in red.' ); var t = getTransform( left, top, w, h ); dstCvs.style.transform = t; } }; function adj(m) { // Compute the adjugate of m return [ m[4]*m[8]-m[5]*m[7], m[2]*m[7]-m[1]*m[8], m[1]*m[5]-m[2]*m[4], m[5]*m[6]-m[3]*m[8], m[0]*m[8]-m[2]*m[6], m[2]*m[3]-m[0]*m[5], m[3]*m[7]-m[4]*m[6], m[1]*m[6]-m[0]*m[7], m[0]*m[4]-m[1]*m[3] ]; } function multmm(a, b) { // multiply two matrices var c = Array(9); for (var i = 0; i != 3; ++i) { for (var j = 0; j != 3; ++j) { var cij = 0; for (var k = 0; k != 3; ++k) { cij += a[3*i + k]*b[3*k + j]; } c[3*i + j] = cij; } } return c; } function multmv(m, v) { // multiply matrix and vector return [ m[0]*v[0] + m[1]*v[1] + m[2]*v[2], m[3]*v[0] + m[4]*v[1] + m[5]*v[2], m[6]*v[0] + m[7]*v[1] + m[8]*v[2] ]; } function pdbg(m, v) { var r = multmv(m, v); return r + " (" + r[0]/r[2] + ", " + r[1]/r[2] + ")"; } function basisToPoints(x1, y1, x2, y2, x3, y3, x4, y4) { var m = [ x1, x2, x3, y1, y2, y3, 1, 1, 1 ]; var v = multmv(adj(m), [x4, y4, 1]); return multmm(m, [ v[0], 0, 0, 0, v[1], 0, 0, 0, v[2] ]); } function general2DProjection( x1s, y1s, x1d, y1d, x2s, y2s, x2d, y2d, x3s, y3s, x3d, y3d, x4s, y4s, x4d, y4d ) { var s = basisToPoints(x1s, y1s, x2s, y2s, x3s, y3s, x4s, y4s); var d = basisToPoints(x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d); return multmm(d, adj(s)); } function project(m, x, y) { var v = multmv(m, [x, y, 1]); return [v[0]/v[2], v[1]/v[2]]; } function getTransform( left, top, w, h ) { var x1 = coordinates[ 0 ], y1 = coordinates[ 1 ]; var x2 = coordinates[ 2 ], y2 = coordinates[ 3 ]; var x3 = coordinates[ 4 ], y3 = coordinates[ 5 ]; var x4 = coordinates[ 6 ], y4 = coordinates[ 7 ]; var t = general2DProjection (left, top, x1, y1, w, top, x2, y2, w, h, x3, y3, left, h, x4, y4); for(i = 0; i != 9; ++i) t[i] = t[i]/t[8]; t = [t[0], t[3], 0, t[6], t[1], t[4], 0, t[7], 0 , 0 , 1, 0 , t[2], t[5], 0, t[8]]; t = "matrix3d(" + t.join(", ") + ")"; return t; }
Just to try to be as clear as possible : in both of my adaptations, I click on the 4 corners of the chessboard which is distorted due to perspective, the 4th click triggers the clipping to that quadrilateral then calculates and applies a transformation whose goal is to make that distorted quadrilateral back into a 2d square.
Thanks, Tepp.