1

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.

Community
  • 1
  • 1
sg1234
  • 600
  • 4
  • 19
  • PS : I'm not very experienced with SO, but I think that I can "ping" the author of example 2 by doing this, I hope this isn't bad etiquette : @MvG – sg1234 May 20 '16 at 16:39

2 Answers2

0

This answer on stack overflow seems to do what you want, i.e. an "inverse perspective transform" : Redraw image from 3d perspective to 2d

As you can see, the author of the answer (the same one you cited for example 2) uses a different equation for the inverse transform : C = A∙B⁻¹ instead of C = B∙A⁻¹

Community
  • 1
  • 1
  • Actually, the way I coded the example, I switched the source and destination coordinates so that achieves the same thing (I just checked) as using the coordinates in the correct order but inverting the matrix multiplication. But you are right, it is important to look out for this. – sg1234 May 20 '16 at 18:34
0

I don't know if you need to do it from scratch, but you could just try to use something like Homography.js. This way, you could just transform your image this way:

const myHomography = new Homography("projective");
myHomography.setReferencePoints(srcPoints, dstPoints);
const transformedImg = myHomography.warp(srcImg);

Then you can draw directly the image in the canvas by doing... dstC.putImageData(transformedImg, 0, 0); or something like

import { Homography } from "https://cdn.jsdelivr.net/gh/Eric-Canas/Homography.js@1.1/Homography.js";    
const transformedImg = myHomography.warp(srcImg, true)
dstC.drawImage(transformedImg);
Haru Kaeru
  • 41
  • 3