10 Replies Latest reply on Feb 28, 2018 10:45 PM by Lummox JR

    Perspective Transform in Javascript

    Fjardar

      Hej everyone,

       

      I'm working on a script, which should do an automatic perspective correction. Unfortunately the only way I found out until now is some code, generated with the ScriptListener Plugin - and I am having a hard time figuring out what the values / maths are doing. For better understanding what I'm trying to achieve, here is an image:

       

      perspective_correction_Script_001.jpg

      The yellow colored area is the original image. Green is the image I'm trying to get. I need to do an perspective correction on the top anchor points. Doing it manually it works fine. However, when I look at the script, the scriptListener is generating, I don't have any clue what the values in there are meaning. Perhaps there is some kind of math I don't get. I can't get any match regarding the values by doing it manually, and the ones the listener writes.

      Anyone an idea? Is there anywhere an angle or distance of how the anchor points are being moved?

       

      var idTrnf = charIDToTypeID( "Trnf" );
          var desc48 = new ActionDescriptor();
          var idnull = charIDToTypeID( "null" );
              var ref21 = new ActionReference();
              var idLyr = charIDToTypeID( "Lyr " );
              var idOrdn = charIDToTypeID( "Ordn" );
              var idTrgt = charIDToTypeID( "Trgt" );
              ref21.putEnumerated( idLyr, idOrdn, idTrgt );
          desc48.putReference( idnull, ref21 );
          var idFTcs = charIDToTypeID( "FTcs" );
          var idQCSt = charIDToTypeID( "QCSt" );
          var idQcsa = charIDToTypeID( "Qcsa" );
          desc48.putEnumerated( idFTcs, idQCSt, idQcsa );
          var idOfst = charIDToTypeID( "Ofst" );
              var desc49 = new ActionDescriptor();
              var idHrzn = charIDToTypeID( "Hrzn" );
              var idPxl = charIDToTypeID( "#Pxl" );
              desc49.putUnitDouble( idHrzn, idPxl, -16.066570 );
              var idVrtc = charIDToTypeID( "Vrtc" );
              var idPxl = charIDToTypeID( "#Pxl" );
              desc49.putUnitDouble( idVrtc, idPxl, 16.381048 );
          var idOfst = charIDToTypeID( "Ofst" );
          desc48.putObject( idOfst, idOfst, desc49 );
          var idWdth = charIDToTypeID( "Wdth" );
          var idPrc = charIDToTypeID( "#Prc" );
          desc48.putUnitDouble( idWdth, idPrc, 134.475806 );
          var idSkew = charIDToTypeID( "Skew" );
              var desc50 = new ActionDescriptor();
              var idHrzn = charIDToTypeID( "Hrzn" );
              var idAng = charIDToTypeID( "#Ang" );
              desc50.putUnitDouble( idHrzn, idAng, 19.127500 );
              var idVrtc = charIDToTypeID( "Vrtc" );
              var idAng = charIDToTypeID( "#Ang" );
              desc50.putUnitDouble( idVrtc, idAng, 0.000000 );
          var idPnt = charIDToTypeID( "Pnt " );
          desc48.putObject( idSkew, idPnt, desc50 );
          var idUsng = charIDToTypeID( "Usng" );
              var desc51 = new ActionDescriptor();
              var idHrzn = charIDToTypeID( "Hrzn" );
              var idPrc = charIDToTypeID( "#Prc" );
              desc51.putUnitDouble( idHrzn, idPrc, -0.000000 );
              var idVrtc = charIDToTypeID( "Vrtc" );
              var idPrc = charIDToTypeID( "#Prc" );
              desc51.putUnitDouble( idVrtc, idPrc, 0.655242 );
          var idPnt = charIDToTypeID( "Pnt " );
          desc48.putObject( idUsng, idPnt, desc51 );
          var idIntr = charIDToTypeID( "Intr" );
          var idIntp = charIDToTypeID( "Intp" );
          var idbicubicAutomatic = stringIDToTypeID( "bicubicAutomatic" );
          desc48.putEnumerated( idIntr, idIntp, idbicubicAutomatic );
      executeAction( idTrnf, desc48, DialogModes.NO );
      
        • 1. Re: Perspective Transform in Javascript
          c.pfaffenbichler Level 9

          Seems a Skew and the Offset of a point are registered.

           

          Some time ago I used elements from stack support.jsx to achieve something similar.

          Re: scripting support for perspective transformations?

          • 2. Re: Perspective Transform in Javascript
            Fjardar Level 1

            I know of this thread. but it didn't gave me any insight.

             

            To make it a little bit easier:

             

            perspective_correction_Script_002.jpg

            Yellow: Box with 100x100 px dimensions. Doing a perspective transform (manually and listening with scriptListener) of 45°.Top Right Handle. The only values that have changed are die width (from 100% to 300%) and the angle (45°).

             

            Now, this is what the listener gets:

             

            var idTrnf = charIDToTypeID( "Trnf" );
                var desc6 = new ActionDescriptor();
                var idnull = charIDToTypeID( "null" );
                    var ref4 = new ActionReference();
                    var idLyr = charIDToTypeID( "Lyr " );
                    var idOrdn = charIDToTypeID( "Ordn" );
                    var idTrgt = charIDToTypeID( "Trgt" );
                    ref4.putEnumerated( idLyr, idOrdn, idTrgt );
                desc6.putReference( idnull, ref4 );
                var idFTcs = charIDToTypeID( "FTcs" );
                var idQCSt = charIDToTypeID( "QCSt" );
                var idQcsa = charIDToTypeID( "Qcsa" );
                desc6.putEnumerated( idFTcs, idQCSt, idQcsa );
                var idOfst = charIDToTypeID( "Ofst" );
                    var desc7 = new ActionDescriptor();
                    var idHrzn = charIDToTypeID( "Hrzn" );
                    var idPxl = charIDToTypeID( "#Pxl" );
                    desc7.putUnitDouble( idHrzn, idPxl, 0.000000 );
                    var idVrtc = charIDToTypeID( "Vrtc" );
                    var idPxl = charIDToTypeID( "#Pxl" );
                    desc7.putUnitDouble( idVrtc, idPxl, 25.000000 );
                var idOfst = charIDToTypeID( "Ofst" );
                desc6.putObject( idOfst, idOfst, desc7 );
                var idWdth = charIDToTypeID( "Wdth" );
                var idPrc = charIDToTypeID( "#Prc" );
                desc6.putUnitDouble( idWdth, idPrc, 150.000000 );
                var idUsng = charIDToTypeID( "Usng" );
                    var desc8 = new ActionDescriptor();
                    var idHrzn = charIDToTypeID( "Hrzn" );
                    var idPrc = charIDToTypeID( "#Prc" );
                    desc8.putUnitDouble( idHrzn, idPrc, -0.000000 );
                    var idVrtc = charIDToTypeID( "Vrtc" );
                    var idPrc = charIDToTypeID( "#Prc" );
                    desc8.putUnitDouble( idVrtc, idPrc, 1.000000 );
                var idPnt = charIDToTypeID( "Pnt " );
                desc6.putObject( idUsng, idPnt, desc8 );
                var idIntr = charIDToTypeID( "Intr" );
                var idIntp = charIDToTypeID( "Intp" );
                var idbicubicAutomatic = stringIDToTypeID( "bicubicAutomatic" );
                desc6.putEnumerated( idIntr, idIntp, idbicubicAutomatic );
            executeAction( idTrnf, desc6, DialogModes.NO );
            

             

             

            Where are the values, that I've used manually? And how can I use this Script to do something like 20°, for instance?

            • 3. Re: Perspective Transform in Javascript
              c.pfaffenbichler Level 9

              Why do you want to use this particular code when the intended result can be achieved with different code (and more easily it seems to me)?

               

              As far as I can figure it out

              • The descriptor with idOfst refers to the center (the intersection of the diagonals). It it is 0 the centre is fixed.

              • The descriptor with idWdth to the width at the horizontal intersection at the height of the center. If it is 100 the width at the centre stays fixed.

              • The descriptor with idUsng is a bit difficult to interpret for me … starting with a square changing the UnitDouble idVrtc

              – -0,025 results in 1,333 times the width and 1,066 times the height

              – -0,05 results in 2 times the width and 1,333 the height

              – -0,075 in 4 times the original width and 2,2855 times the height

              In this example the results for -0,025, -0,05, -0,075 and -0,09 are overlaid:

              checkerBoardTransform_lay.jpg

              Now that is a long way off from being useful, but I’m not good at Math anymore and the functions and the trigonometry that would be necessary here seem fairly fancy to me …

              • 4. Re: Perspective Transform in Javascript
                Fjardar Level 1

                Thank you for the replay again.

                 

                I think I got a little more insight about what is happening. Besides that: I don't want or need to use the code from the action listener. It is plain the only way I came up with so far.

                 

                If you got an easier way to do do this, I would be more than happy to hear about.

                • 5. Re: Perspective Transform in Javascript
                  c.pfaffenbichler Level 9

                  The code I had posted in the other thread uses a four-point Path as the basis of the transformation, but one could just as well define the Array of four points some other way.

                  • 6. Re: Perspective Transform in Javascript
                    xiaoliuw83456985

                    share the link of your  ”four-point Path ”  code?

                    • 7. Re: Perspective Transform in Javascript
                      c.pfaffenbichler Level 9

                      That code is pretty old, I think the issue could be resolved more easily now.

                       

                      // this script attempts to fit the active layer into an area defined by a 4-point-path’s points;

                      // cs5 on mac;

                      // 2011, use it at your own risk;

                      #target photoshop

                      // from adobe’s terminology.jsx;

                      const classChannel = app.charIDToTypeID('Chnl');

                      const classRectangle = app.charIDToTypeID('Rctn');

                      const enumNone = app.charIDToTypeID('None');

                      const eventSet = app.charIDToTypeID('setd');

                      const eventTransform = app.charIDToTypeID('Trnf');

                      const keySelection = app.charIDToTypeID('fsel');

                      const krectangleStr = app.stringIDToTypeID("rectangle");

                      const kquadrilateralStr = app.stringIDToTypeID("quadrilateral");

                      const keyBottom = app.charIDToTypeID('Btom');

                      const keyLeft = app.charIDToTypeID('Left');

                      const keyNull = app.charIDToTypeID('null');

                      const keyRight = app.charIDToTypeID('Rght');

                      const keyTo = app.charIDToTypeID('T   ');

                      const keyTop = app.charIDToTypeID('Top ');

                      const typeOrdinal = app.charIDToTypeID('Ordn');

                      const unitPixels = app.charIDToTypeID('#Pxl');

                      // from adobe’s geometry.jsx;

                      //

                      // =================================== TPoint ===================================

                      //

                      function TPoint( x, y )

                      {

                        this.fX = x;

                        this.fY = y;

                      }

                      // TPoint Constants

                      const kTPointOrigion = new TPoint( 0, 0 );

                      TPoint.kOrigin = kTPointOrigion;

                       

                       

                      const kTPointInfinite = new TPoint( Infinity, Infinity );

                      TPoint.kInfinite = kTPointInfinite;

                      const kTPointClassname = "TPoint";

                      TPoint.prototype.className = kTPointClassname;

                      // Overloaded math operators

                      TPoint.prototype["=="] = function( Src )

                      {

                        return (this.fX == Src.fX) && (this.fY == Src.fY);

                      }

                       

                       

                      TPoint.prototype["+"] = function( b )

                      {

                        return new TPoint( this.fX + b.fX, this.fY + b.fY );

                      }

                       

                       

                      TPoint.prototype["-"] = function( b, reversed )

                      {

                        if (typeof(b) == "undefined")

                      // unary minus

                        return new TPoint( -this.fX, -this.fY )

                        else

                        {

                        if (reversed)

                        return new TPoint( b.fX - this.fX, by.fY - this.fY );

                        else

                        return new TPoint( this.fX - b.fX, this.fY - b.fY);

                        }

                      }

                      //

                      // Multiply and divide work with scalars as well as points

                      //

                      TPoint.prototype["*"] = function( b )

                      {

                          if (typeof(b) == 'number')

                        return new TPoint( this.fX * b, this.fY * b );

                        else

                        return new TPoint( this.fX * b.fX, this.fY * b.fY );

                      }

                      TPoint.prototype["/"] = function( b, reversed )

                      {

                        if (reversed)

                        {

                        if (typeof(b) == "number")

                        debugger;

                      // Can't divide a number by a point

                        else

                        return new TPoint( b.fX / this.fX, b.fY / this.fY );

                        }

                        else

                        {

                        if (typeof(b) == 'number')

                        return new TPoint( this.fX / b, this.fY / b );

                        else

                        return new TPoint( this.fX / b.fX, this.fY / b.fY );

                        }

                      }

                      TPoint.prototype.toString = function()

                      {

                        return "[" + this.fX.toString() + "," + this.fY.toString() + "]";

                      }

                      TPoint.prototype.vectorLength = function()

                      {

                          return Math.sqrt( this.fX * this.fX + this.fY * this.fY );

                      }

                      ////////////////////////////////////

                      var myDocument = app.activeDocument;

                      // deselect;

                      try {

                      myDocument.selection.deselect();

                      // =======================================================

                      var idDslc = charIDToTypeID( "Dslc" );

                          var desc4 = new ActionDescriptor();

                          var idnull = charIDToTypeID( "null" );

                              var ref2 = new ActionReference();

                              var idPath = charIDToTypeID( "Path" );

                              ref2.putClass( idPath );

                          desc4.putReference( idnull, ref2 );

                      executeAction( idDslc, desc4, DialogModes.NO );

                      }

                      catch (e) {};

                      // switch units to pixels;

                      app.preferences.rulerUnits = Units.PIXELS;

                      // verify document has paths;

                      var thePathList = new Array;

                      // create list of possible paths;

                      for (var g = 0; g < myDocument.pathItems.length; g++) {

                        var checkPath = myDocument.pathItems[g];

                        if (checkPath.subPathItems.length == 1 && checkPath.subPathItems[0].pathPoints.length == 4) {

                        thePathList.push(checkPath);

                        }

                        };

                      // get path;

                      switch (thePathList.length) {

                        case 0:

                        break;

                        case 1:

                        var aPath = thePathList[0];

                        break;

                        default:

                        var aPath = selectAPath (thePathList);

                        break;

                        };

                      //////////// transformation ////////////

                      if (aPath) {

                      try {

                      // paste the image into the document;

                      var theScreenImage = myDocument.activeLayer;

                      //////////// corners ////////////

                      // get the horicontal and vertical coordinates in pixels;

                      var hor1 = Number(aPath.subPathItems[0].pathPoints[0].anchor[0]);

                      var hor2 = Number(aPath.subPathItems[0].pathPoints[1].anchor[0]);

                      var hor3 = Number(aPath.subPathItems[0].pathPoints[2].anchor[0]);

                      var hor4 = Number(aPath.subPathItems[0].pathPoints[3].anchor[0]);

                      var ver1 = Number(aPath.subPathItems[0].pathPoints[0].anchor[1]);

                      var ver2 = Number(aPath.subPathItems[0].pathPoints[1].anchor[1]);

                      var ver3 = Number(aPath.subPathItems[0].pathPoints[2].anchor[1]);

                      var ver4 = Number(aPath.subPathItems[0].pathPoints[3].anchor[1]);

                      // order the horicontal and vertical coordinates;

                      var horList = [hor1, hor2, hor3, hor4];

                      var verList = [ver1, ver2, ver3, ver4];

                      horList.sort(sortNumber);

                      verList.sort(sortNumber);

                      // check the horicontal value;

                      var leftPoints = new Array;

                      var rightPoints = new Array;

                      for (var k=0; k<aPath.subPathItems[0].pathPoints.length; k++) {

                        if (aPath.subPathItems[0].pathPoints[k].anchor[0] == horList[0]

                        ||  aPath.subPathItems[0].pathPoints[k].anchor[0] == horList[1]) {

                        leftPoints = leftPoints.concat(aPath.subPathItems[0].pathPoints[k].anchor)

                        }

                        else {

                        rightPoints = rightPoints.concat(aPath.subPathItems[0].pathPoints[k].anchor)

                        }

                        };

                      // define the four cornerpoints;

                      if (leftPoints[1] <= leftPoints[3]) {

                        var aTopLeft = [leftPoints[0], leftPoints[1]]

                        var aBottomLeft = [leftPoints[2], leftPoints[3]];

                        }

                      else {

                        var aTopLeft = [leftPoints[2], leftPoints[3]]

                        var aBottomLeft = [leftPoints[0], leftPoints[1]];

                        };

                      if (rightPoints[1] <= rightPoints[3]) {

                        var aTopRight = [rightPoints[0], rightPoints[1]]

                        var aBottomRight = [rightPoints[2], rightPoints[3]];

                        }

                      else {

                        var aTopRight = [rightPoints[2], rightPoints[3]]

                        var aBottomRight = [rightPoints[0], rightPoints[1]];

                        }

                      //////////// transform to the new corners ////////////

                      transformActiveLayer( [new TPoint(aTopLeft[0], aTopLeft[1]), new TPoint(aTopRight[0], aTopRight[1]), new TPoint(aBottomRight[0], aBottomRight[1]), new TPoint(aBottomLeft[0], aBottomLeft[1])]);

                      // resets the preferences units;

                      app.preferences.rulerUnits = originalUnits;

                      }

                      catch (e) {}

                      };

                      ////////////////////////////////////

                      ////////////////////////////////////

                      ////////////////////////////////////

                      //////////// the  functions ////////////

                      // the dialog for multiple possible paths;

                      function selectAPath (thePathList) {

                        var dlg = new Window('dialog', "Select a path to use for the perspective", [500,300,800,400])

                        dlg.pathSel = dlg.add('dropdownlist', [12,13,288,35]);

                        for (var m = 0; m < thePathList.length; m++) {

                        dlg.pathSel.add("item", thePathList[m].name)

                        };

                        dlg.pathSel.selection = dlg.pathSel[0];

                        dlg.pathSel.active = true;

                        dlg.buildBtn = dlg.add('button', [13,42,145,62], 'OK', {name:'ok'});

                        dlg.cancelBtn = dlg.add('button', [155,42,288,62], 'Cancel', {name:'cancel'});

                        var myReturn = dlg.show ();

                        if (myReturn == true) {

                        var aPath = app.activeDocument.pathItems.getByName(dlg.pathSel.selection);

                        return aPath

                        }

                        };

                      // sort numbers, found at www.w3schools.com;

                      function sortNumber(a,b) {

                        return a - b;

                        };

                      // from adobe’s stacksupport.jsx;

                      // Apply a perspective transform to the current layer, with the

                      // corner TPoints given in newCorners (starts at top left, in clockwise order)

                      // Potential DOM fix

                      function transformActiveLayer( newCorners )

                      {

                        function pxToNumber( px )

                        {

                        return px.as("px");

                        }

                        var saveUnits = app.preferences.rulerUnits;

                        app.preferences.rulerUnits = Units.PIXELS;

                        var i;

                        var setArgs = new ActionDescriptor();

                        var chanArg = new ActionReference();

                        chanArg.putProperty( classChannel, keySelection );

                      // setArgs.putReference( keyNull, chanArg );

                        var boundsDesc = new ActionDescriptor();

                        var layerBounds = app.activeDocument.activeLayer.bounds;

                        boundsDesc.putUnitDouble( keyTop, unitPixels, pxToNumber( layerBounds[1] ) );

                        boundsDesc.putUnitDouble( keyLeft, unitPixels, pxToNumber( layerBounds[0] ) );

                        boundsDesc.putUnitDouble( keyRight, unitPixels, pxToNumber( layerBounds[2] ) );

                        boundsDesc.putUnitDouble( keyBottom, unitPixels, pxToNumber( layerBounds[3] ) );

                      // setArgs.putObject( keyTo, classRectangle, boundsDesc );

                      // executeAction( eventSet, setArgs );

                        var result = new ActionDescriptor();

                        var args = new ActionDescriptor();

                        var quadRect = new ActionList();

                        quadRect.putUnitDouble( unitPixels, pxToNumber( layerBounds[0] ) );

                      // ActionList put is different from ActionDescriptor put

                        quadRect.putUnitDouble( unitPixels, pxToNumber( layerBounds[1] ) );

                        quadRect.putUnitDouble( unitPixels, pxToNumber( layerBounds[2] ) );

                        quadRect.putUnitDouble( unitPixels, pxToNumber( layerBounds[3] ) );

                        var quadCorners = new ActionList();

                        for (i = 0; i < 4; ++i)

                        {

                        quadCorners.putUnitDouble( unitPixels, newCorners[i].fX );

                        quadCorners.putUnitDouble( unitPixels, newCorners[i].fY );

                        }

                        args.putList( krectangleStr, quadRect );

                        args.putList( kquadrilateralStr, quadCorners );

                        executeAction( eventTransform, args );

                      // Deselect

                        deselArgs = new ActionDescriptor();

                        deselRef = new ActionReference();

                        deselRef.putProperty( classChannel, keySelection );

                        deselArgs.putReference( keyNull, deselRef );

                        deselArgs.putEnumerated( keyTo, typeOrdinal, enumNone );

                        executeAction( eventSet, deselArgs );

                        app.preferences.rulerUnits = saveUnits;

                      }

                      • 8. Re: Perspective Transform in Javascript
                        xiaoliuw83456985 Level 1
                        1. thanks,I will study your code first , if there is a more simple way, please also tell us,(Code is not necessarily required)actually the ideas,steps。。。。。。,thanks again
                        • 9. Re: Perspective Transform in Javascript
                          c.pfaffenbichler Level 9

                          If you have follow-up questions may want to start a new thread.

                          • 10. Re: Perspective Transform in Javascript
                            Lummox JR

                            I'm late to this party, but I wanted to share that I figured out the solution to the numbers for the "Using" command.

                             

                            The units of the numbers don't appear to matter at all. The number is used for a Z-perspective transform, relative to whatever point is your anchor. If you pass in numbers H and V, this is the formula for how points will be transformed:

                             

                            z = 1/3 + xH/width + yV/height

                            x' = x / 3z

                            y' = y / 3z

                             

                            Basically, the focal plane is at z = 1/3. Or you can go with a simpler formula:

                             

                            z = 1 + 3(xH/width + yV/height)

                            x' = x / z

                            y' = y / z

                             

                            Take for instance a square. Anchor it to Qcs6 (bottom middle). Now apply the Using command with H=0 and V=-1/3. You'll have a trapezoid that's just as wide at the bottom as it ever was, but it will be half as high and the top will be half as wide.

                             

                            The top right corner of the square starts at x=width/2 and y=-height relative to the origin. It transforms like so:

                             

                            z = 1 + 3(x/width)(0) + 3(y/height)(-1/3) = 1 + 3(1/2)(0) + 3(-1)(-1/3) = 2

                            x' = x / 2 = width/4

                            y' = y / 2 = -height/2

                             

                            I hope that's helpful, if not to the OP anymore then maybe to others. I racked my brain trying to figure this out. Happy scripting!