Loading...
Logo
Processing Forum
I've been playing with a projector, trying to project from processing onto various surfaces (a cardboard box, for example).  After looking around the forums I found the keystone library, but it has a big problem..  After adjusting the keystone rectangle to fit the cardboard box, the grid squares are not consistently sized.  They are wider on one side of the projection than they are on the other.  The result is that if you move a circle (or any other shape) across the keystoned region, it will slowly stretch horizontally.  Not only does this look bad, but it makes any attempt to match the rendering with any real world features virtually impossible.  For example, if I wanted to project onto a wall that had a grid painted on it, the projected grid wouldn't line up due to the horizontal stretching.  Has anybody else noticed this problem?  Is there an easy solution?


Additionally:

After lots more googling, I found a technique called homography which is a way to create a projection matrix which solves the same problem, but seems to work much better.  The tool vvvv has a node that does this, and it works perfectly in my test setup.  The problem is, my brain just doesn't work with patch-based tools so I'd like to stick with processing if at all possible.  I looked around on the forum and it seems that the projection matrices for the rendering system aren't accessible (not that I know enough math to implement the homography solution anyway.. :| ).


So.. thoughs? ideas?  While I'm obviously interested in a solution I'm also just hoping to spur some discussion on this topic since I couldn't find much about it on the forums so far.

Replies(7)

Hello!

I would be interested in working toward a solution on this issue.  I'm very curious about automatic projector calibration, and particularly interested in projection mapping techniques.  I am not terribly strong in math, but am somewhat competent at writing code.  I'll take a closer look at the keystone library.  I've played around with the MatLab procam lib ( http://code.google.com/p/procamcalib/) that's kind of like the "square one" for most CV applications.  Perhaps there is some scope to apply those principles to the keystone lib and extend it to do what we need.

What do you think?

Shaheeb Roshan
www.shaheeb.com
Shaheeb,

Good news, I've found a solution to this problem!

I've used the java advanced imaging library to create a quad-to-quad transformation based on a source rectangle and a set of four arbitrary destination points.  I'm building what I have into a class that will function in a similar fashion to the keystone library, but will include proper perspective (instead of simply making equally spaced grids within a given quad).

Once I have a working sketch I'll post it here.  In the mean time, check out the java advanced imaging stuff if you're curious.  I'm using PerspectiveTransform.getQuadToQuad() to set up the transform, then I just use the transform() method with each set of grid coordinates to create the warped, textured quad.

I'll post more soon, with code samples..
I thought I'd take some screenshots to better illustrate what I'm talking about in this thread.

Here's the keystone library, with the problem I've described.  Notice that the vertical grid lines are spaced evenly from left to right.  But if you actually look at a rectangular grid along an angle, you don't see this, you'll see that the vertical grid lines get closer together as they approach the vanishing point.  So, using the keystone library will result in distorted images when projecting onto a surface that isn't perpendicular to the projector.






Here's my sketch so far, using the PerspectiveTransform class in the Java Advanced Imaging library.  You can see that the vertical lines are spaced correctly according to the perspective of the rectangle.

I'm going to keep working on this and attach the sketch later, or maybe even make a library.  Stay tuned..

Another interesting bit..

I was looking for a way to find the mouse cursor position translated onto the grid, and the PerspectiveTransform class provides a handy method to compute the inverse of the transform, so it's really easy to go from screen coordinates back to the transformed quad coordinates. See the docs here..


Also, texturing the quad using an offscreen PGraphics instance is now working..




Hi!

I'm looking for exactily the same thing!

Would you mind posting the sample sketch? I'm new to Java and processing, so I'm a bit lost about how to use JAI inside processing.

Thanks!
Ok, here's the sketch so far...  It's kind of getting huge, I might have to switch to eclipse soon and split this out into a bunch of different files so I don't loose my mind.

The JAI files you need are the two that are used for the PerspectiveTransform class.  They're jai_core.jar and jai_codec.jar, you should be able to find them if you install JAI and search for those file names.

In the sketch, pressing the '1' key will toggle in and out of "configuration" mode. Left-click-drag corners or center point, right-click-drag will move in 1/10th pixel increments for fine adjustment.  If you drag a corner far enough to make the shape convex it really freaks out, just so you know...

My plan is still to get this into a library once I get the ProjectionManager class fleshed out, so this is a work-in-progress, but you asked for it! :)

Copy code
  1. import processing.opengl.*;
  2. import javax.media.jai.*;
  3. import codeanticode.glgraphics.*;
  4. import processing.core.*;

  5. ProjectedSurface q;

  6. GLGraphicsOffScreen offscreen, offscreen2;

  7. int offset;

  8. void setup()
  9. {
  10.   size(1024, 768, GLConstants.GLGRAPHICS);

  11.   offscreen = new GLGraphicsOffScreen(this, 720, 540);
  12.   
  13.   q = new ProjectedSurface( this, 720, 540 );
  14.   q.setTextureRect( 0, 0, 720, 540 );
  15. }

  16. // Render some random blocks of color..
  17. void drawOffscreen()
  18. {
  19.   offset++;
  20.   
  21.   offscreen.beginDraw();
  22.     
  23.   offscreen.background(0);
  24.   offscreen.fill(255);
  25.   offscreen.noStroke();
  26.     
  27.   int stepX = offscreen.width / 12;
  28.   int stepY = offscreen.height / 9;
  29.     
  30.   for(int x = 0; x < 12; x++)
  31.   {
  32.     for(int y = 0; y < 9; y++)
  33.     {
  34.       float v = noise(x*0.1, y*0.1, offset*0.01);
  35.       if(v > 0.5)
  36.       {
  37.         offscreen.fill(v * 255);
  38.         offscreen.rect( x * stepX, y * stepY, stepX, stepY);
  39.       }
  40.       else
  41.       {
  42.         offscreen.fill(0, 255-(v * 255), 0);
  43.         offscreen.rect( x * stepX, y * stepY, stepX, stepY);
  44.       }

  45.     }
  46.   }

  47.   offscreen.endDraw();
  48. }


  49. void draw()
  50. {
  51.   drawOffscreen();
  52.   GLTexture t = offscreen.getTexture();
  53.   
  54.   background(0);
  55.   
  56.   q.draw( t );
  57.   q.update();
  58. }


  59. void keyPressed()
  60. {
  61.   switch(key)
  62.   {
  63.     case '1': 
  64.       q.toggleMode();
  65.      break;
  66.   }
  67. }

  68. // Class for managing lots of projected surfaces at once.
  69. class ProjectionMapper
  70. {
  71.   ArrayList<ProjectedSurface> surfaces;
  72.   PApplet parentApplet;
  73.   
  74.   ProjectionMapper(PApplet parent)
  75.   {
  76.     this.parentApplet = parent;
  77.     this.parentApplet.registerDispose(this);
  78.   }
  79.   
  80.   public ProjectedSurface createSurface(String name, int width, int height)
  81.   {
  82.     ProjectedSurface surf = new ProjectedSurface(this.parentApplet, width, height);
  83.     surf.name = name;
  84.     
  85.     this.surfaces.add(surf);
  86.     
  87.     return surf;
  88.   }
  89.   
  90.   public ProjectedSurface getSurface( String name )
  91.   {
  92.     for(int i = 0; i < this.surfaces.size(); i++)
  93.     {
  94.        if(this.surfaces.get(i).name.equals(name))
  95.        {
  96.          return this.surfaces.get(i);
  97.        }
  98.     }
  99.     return null;
  100.   }
  101.   
  102.   void update()
  103.   {
  104.     for(int i = 0; i < this.surfaces.size(); i++)
  105.     {
  106.       this.surfaces.get(i).update();
  107.     }
  108.   }
  109.   
  110.   public void dispose()
  111.   {
  112.   }
  113. }


  114. class ProjectedSurface
  115. {
  116.   final private int MODE_RENDER = 0;
  117.   final private int MODE_CALIBRATE = 1;
  118.   PApplet parentApplet;
  119.   
  120.   public String name = "Projected Quad";

  121.   private int MODE = MODE_RENDER;

  122.   private int activePoint = -1; // Which corner point is selected?  
  123.   private final float SELECTION_DISTANCE = 15; // Maximum distance from cursor to corner point before considering it "selected"
  124.   private final int GRID_LINE_COLOR = color(128,128,128);
  125.   private final int CORNER_MARKER_COLOR = color(255,255,255);
  126.   private final int SELECTED_CORNER_MARKER_COLOR = color(255,0,0);
  127.   
  128.   int GRID_RESOLUTION = 30; // Number of grid lines.. Increasing this will mean better looking results but will decrease performance.
  129.   
  130.   // Dimensions of the source rectangle.
  131.   float sourceWidth = 100;
  132.   float sourceHeight = 100;
  133.   
  134.   // Metrics for the projected texture..
  135.   float textureX = 0;
  136.   float textureY = 0;
  137.   float textureWidth = 0;
  138.   float textureHeight = 0;
  139.   
  140.   // The four corners of the transformed quad (in 2d screen space)
  141.   Point2D[] destinationPoints;
  142.   
  143.   // The entire list of transformed grid points are stored in this array (from left to right, top to bottom, like pixels..).
  144.   // This list is updated whenever the updateTransform() method is invoked.
  145.   Point2D[] gridPoints;
  146.   
  147.   // The raw list of verticies to be pumped out each frame.  This array
  148.   // holds the pre-computed list, including duplicates, to save on computation during rendering.
  149.   Point2D[] vertexPoints;
  150.   
  151.   // A list of draggable control points (corners and center point).  This list is only used 
  152.   // for calibration mode to make thing easier when checking the mouse cursor position.
  153.   // Ordered clock-wise starting with top-left.
  154.   Point2D[] controlPoints;
  155.   
  156.   // The transform!  Thank you Java Advanced Imaging, now I don't have to learn a bunch of math..
  157.   // Docs: http://download.oracle.com/docs/cd/E17802_01/products/products/java-media/jai/forDevelopers/jai-apidocs/javax/media/jai/PerspectiveTransform.html
  158.   PerspectiveTransform transform;
  159.   
  160.   // Constructor, initialize the varous arrays of points.
  161.   ProjectedSurface(PApplet parent, int sourceWidth, int sourceHeight)
  162.   {
  163.     this.parentApplet = parent;
  164.     
  165.     this.sourceWidth = sourceWidth;
  166.     this.sourceHeight = sourceHeight;
  167.     
  168.     println(g.width + ", " + g.height);
  169.     this.destinationPoints = new Point2D[4];
  170.     this.controlPoints = new Point2D[5];
  171.     this.gridPoints = new Point2D[ this.GRID_RESOLUTION * this.GRID_RESOLUTION ];
  172.     this.vertexPoints = new Point2D[this.gridPoints.length * 4];
  173.     
  174.     for(int i = 0; i < this.gridPoints.length; i++)
  175.     {
  176.       this.gridPoints[i] = new Point2D();
  177.     }
  178.     
  179.     for(int i = 0; i < this.vertexPoints.length; i++)
  180.     {
  181.       this.vertexPoints[i] = new Point2D();
  182.     }
  183.     
  184.     for(int i = 0; i < this.controlPoints.length; i++)
  185.     {
  186.       this.controlPoints[i] = new Point2D();
  187.     }
  188.     
  189.     for(int i = 0; i < this.destinationPoints.length; i++)
  190.     {
  191.       this.destinationPoints[i] = new Point2D();
  192.     }
  193.     
  194.     this.destinationPoints[0].x = 0;
  195.     this.destinationPoints[0].y = 0;
  196.     
  197.     this.destinationPoints[1].x = this.sourceWidth;
  198.     this.destinationPoints[1].y = 0;
  199.     
  200.     this.destinationPoints[2].x = this.sourceWidth;
  201.     this.destinationPoints[2].y = this.sourceHeight;
  202.     
  203.     this.destinationPoints[3].x = 0;
  204.     this.destinationPoints[3].y = this.sourceHeight;
  205.     
  206.     this.textureWidth = sourceWidth;
  207.     this.textureHeight = sourceHeight;
  208.     
  209.     this.updateTransform();
  210.   }
  211.   
  212.   void setParentApplet(PApplet applet)
  213.   {
  214.     this.parentApplet = applet;
  215.   }
  216.   
  217.   // Sets the coordinates of the texture.  This allows for a single texture to span
  218.   // more than one surface rather than have to draw on multiple offscreen buffers.
  219.   void setTextureRect(float x, float y, float w, float h)
  220.   {
  221.     this.textureX = x;
  222.     this.textureY = y;
  223.     this.textureWidth = w;
  224.     this.textureHeight = h;
  225.     
  226.     this.updateTransform();
  227.   }
  228.   
  229.   // Set the coordinates of one of the target corner points.
  230.   void setCornerPoint(int pointIndex, float x, float y)
  231.   {
  232.     this.destinationPoints[pointIndex].x = x;
  233.     this.destinationPoints[pointIndex].y = y;
  234.     
  235.     this.updateTransform();
  236.   }
  237.   
  238.   void setModeCalibrate()
  239.   {
  240.     this.MODE = this.MODE_CALIBRATE;
  241.   }

  242.   void setModeRender()
  243.   {
  244.     this.MODE = this.MODE_RENDER;
  245.   }
  246.   
  247.   void toggleMode()
  248.   {
  249.     if(this.MODE == this.MODE_RENDER)
  250.     {
  251.       this.MODE = this.MODE_CALIBRATE;
  252.     }
  253.     else
  254.     {
  255.       this.MODE = this.MODE_RENDER;
  256.     }
  257.   }  
  258.   
  259.   // Update the UI (only needed in calibration mode..)
  260.   void update()
  261.   {
  262.     if(this.MODE == this.MODE_CALIBRATE)
  263.     {
  264.       if( mousePressed && (this.activePoint != -1) )
  265.       {
  266.         float deltaX = mouseX - pmouseX;
  267.         float deltaY = mouseY - pmouseY;
  268.         
  269.         // Right mouse button drags very slowly.
  270.         if(mouseButton == RIGHT)
  271.         {
  272.           deltaX *= 0.1;
  273.           deltaY *= 0.1;
  274.         }
  275.         
  276.         // special case.
  277.         // index 4 is the center point so move all four corners.
  278.         if(this.activePoint == 4)
  279.         {
  280.           for(int i = 0; i < 4; i++)
  281.           {
  282.             this.setCornerPoint( i, 
  283.                                  this.destinationPoints[i].x + deltaX, 
  284.                                  this.destinationPoints[i].y + deltaY );
  285.           }
  286.         }
  287.         else
  288.         {
  289.           // Move a single corner point.
  290.           this.setCornerPoint( this.activePoint, 
  291.                                this.destinationPoints[this.activePoint].x + deltaX, 
  292.                                this.destinationPoints[this.activePoint].y + deltaY );
  293.         }
  294.       }
  295.       else
  296.       {
  297.         this.activePoint = this.getActiveControlPointIndex();
  298.       }
  299.     }
  300.   }
  301.   
  302.   void draw(PImage tex)
  303.   {
  304.     this.drawQuad( tex );
  305.     
  306.     if(this.MODE == this.MODE_CALIBRATE)
  307.     {
  308.       this.drawGrid();
  309.     }
  310.   }
  311.   
  312.   // Get the index of the control point that is close to the mouse cursor.
  313.   // Only used in calibration mode.
  314.   int getActiveControlPointIndex()
  315.   {
  316.     for(int i = 0; i < this.controlPoints.length; i++)
  317.     {
  318.       if( dist( mouseX, mouseY, this.controlPoints[i].x, this.controlPoints[i].y ) < this.SELECTION_DISTANCE)
  319.       {
  320.         return i;
  321.       }
  322.     }
  323.     
  324.     return -1;
  325.   }
  326.   
  327.   // Set all four corners of the destination quad.
  328.   void setDestionationPoints(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3)
  329.   {
  330.     this.destinationPoints[0].x = x0;
  331.     this.destinationPoints[0].y = y0;
  332.     
  333.     this.destinationPoints[1].x = x1;
  334.     this.destinationPoints[1].y = y1;
  335.     
  336.     this.destinationPoints[2].x = x2;
  337.     this.destinationPoints[2].y = y2;
  338.     
  339.     this.destinationPoints[3].x = x3;
  340.     this.destinationPoints[3].y = y3;
  341.     
  342.     this.updateTransform();
  343.   }
  344.   
  345.   // Update the various coordinates using the perspective transform.
  346.   // This must be called whenever the source dimensions or destination coordinates are changed.
  347.   void updateTransform()
  348.   {
  349.     // Update the PerspectiveTransform with the current width, height, and destination coordinates.
  350.     this.transform = PerspectiveTransform.getQuadToQuad(
  351.       0, 0,
  352.       this.sourceWidth, 0,
  353.       this.sourceWidth, this.sourceHeight,
  354.       0, this.sourceHeight,
  355.       
  356.       this.destinationPoints[0].x, this.destinationPoints[0].y,
  357.       this.destinationPoints[1].x, this.destinationPoints[1].y,
  358.       this.destinationPoints[2].x, this.destinationPoints[2].y,
  359.       this.destinationPoints[3].x, this.destinationPoints[3].y
  360.     );
  361.     
  362.     // calculate the x and y interval to subdivide the source rectangle into the desired resolution.
  363.     float stepX = this.sourceWidth / float(this.GRID_RESOLUTION-1);
  364.     float stepY = this.sourceHeight / float(this.GRID_RESOLUTION-1);
  365.     
  366.     // figure out the number of points in the whole grid.
  367.     int numPoints = this.GRID_RESOLUTION * this.GRID_RESOLUTION;
  368.     
  369.     // create the array of floats (used for input into the transform method, it requires a single array of floats)
  370.     float[] srcPoints = new float[numPoints * 2];
  371.     
  372.     // calculate the source coordinates of the grid points, as well as the texture coordinates of the destination points.
  373.     int i = 0;
  374.     for(int y = 0; y < this.GRID_RESOLUTION; y++)
  375.     {
  376.       for(int x = 0; x < this.GRID_RESOLUTION; x++)
  377.       {
  378.         float percentX = (x * stepX) / this.sourceWidth;
  379.         float percentY = (y * stepY) / this.sourceHeight;
  380.         
  381.         this.gridPoints[x + y * this.GRID_RESOLUTION].u = this.textureWidth * percentX + this.textureX;
  382.         this.gridPoints[x + y * this.GRID_RESOLUTION].v = this.textureHeight * percentY + this.textureY; // y * stepY;
  383.         
  384.         srcPoints[i++] = x * stepX;
  385.         srcPoints[i++] = y * stepY;
  386.       }
  387.     }
  388.     
  389.     // create an array for the transformed points (populated by PerspectiveTransform.transform())
  390.     float[] transformed = new float[srcPoints.length];
  391.     
  392.     // perform the transformation.
  393.     this.transform.transform( srcPoints, 0, transformed, 0, numPoints);
  394.     
  395.     // convert the array of float values back into x/y pairs in the Point2D class for ease of use later.
  396.     for(int p = 0; p < numPoints; p++)
  397.     {
  398.       this.gridPoints[p].x = transformed[p*2];
  399.       this.gridPoints[p].y = transformed[p*2+1];
  400.     }
  401.     
  402.     
  403.     // Precompute the verticies for use in rendering.
  404.     int offset = 0;
  405.     int vertextIndex = 0;
  406.     for(int y = 0; y < this.GRID_RESOLUTION - 1; y++)
  407.     {
  408.       for(int x = 0; x < this.GRID_RESOLUTION - 1; x++)
  409.       {
  410.         offset = x + y * this.GRID_RESOLUTION;
  411.         
  412.         this.vertexPoints[vertextIndex++].copyPoint( this.gridPoints[offset] );
  413.         this.vertexPoints[vertextIndex++].copyPoint( this.gridPoints[offset+1] );
  414.         
  415.         offset = x + (y + 1) * this.GRID_RESOLUTION;
  416.         
  417.         this.vertexPoints[vertextIndex++].copyPoint( this.gridPoints[offset+1] );
  418.         this.vertexPoints[vertextIndex++].copyPoint( this.gridPoints[offset] );
  419.       }
  420.     }
  421.     
  422.     
  423.     
  424.     // keep track of the four transformed corner points for use in calibration mode.
  425.     this.controlPoints[0].x = this.gridPoints[0].x;
  426.     this.controlPoints[0].y = this.gridPoints[0].y;
  427.     
  428.     this.controlPoints[1].x = this.gridPoints[this.GRID_RESOLUTION - 1].x;
  429.     this.controlPoints[1].y = this.gridPoints[this.GRID_RESOLUTION - 1].y;
  430.     
  431.     this.controlPoints[2].x = this.gridPoints[this.gridPoints.length - 1].x;
  432.     this.controlPoints[2].y = this.gridPoints[this.gridPoints.length - 1].y;
  433.     
  434.     this.controlPoints[3].x = this.gridPoints[this.gridPoints.length - this.GRID_RESOLUTION].x;
  435.     this.controlPoints[3].y = this.gridPoints[this.gridPoints.length - this.GRID_RESOLUTION].y;    
  436.     
  437.     
  438.     // Find the average position of all the control points, use that as the center point.
  439.     float avgX = 0;
  440.     float avgY = 0;
  441.     for(int c = 0; c < 4; c++)
  442.     {
  443.       avgX += this.controlPoints[c].x;
  444.       avgY += this.controlPoints[c].y;
  445.     }
  446.     avgX /= 4;
  447.     avgY /= 4;
  448.     
  449.     this.controlPoints[4].x = avgX;
  450.     this.controlPoints[4].y = avgY;
  451.   }
  452.   
  453.   // Translate a point on the screen into a point on the quad.
  454.   public PVector screenCoordinatesToQuad(float x, float y)
  455.   {
  456.     double[] srcPts = new double[2];
  457.     srcPts[0] = x;
  458.     srcPts[1] = y;
  459.     
  460.      double[] dstPts = new double[2];
  461.     
  462.     try
  463.     {
  464.       this.transform.inverseTransform(srcPts, 0, dstPts, 0, 1);
  465.     }
  466.     catch(Exception e)
  467.     {
  468.       return new PVector(0,0);
  469.     }
  470.     
  471.     return new PVector((float)dstPts[0], (float)dstPts[1]);
  472.   }
  473.   
  474.   // Draw the textured transformed quad.
  475.   public void drawQuad(PImage tex)
  476.   {
  477.     this.parentApplet.noStroke();
  478.     this.parentApplet.beginShape( QUADS );
  479.     
  480.     this.parentApplet.texture( tex );
  481.     
  482.     // Blast through the precomputed list of verticies (see updateTransform)
  483.     int c = this.vertexPoints.length;
  484.     for(int i = 0; i < c; i++)
  485.     {
  486.       this.parentApplet.vertex(this.vertexPoints[i].x, this.vertexPoints[i].y,this.vertexPoints[i].u,this.vertexPoints[i].v);
  487.     }
  488.     
  489.     this.parentApplet.endShape(CLOSE);
  490.   }
  491.  
  492.   public void drawGrid()
  493.   {
  494.     this.parentApplet.noFill();
  495.     this.parentApplet.stroke(this.GRID_LINE_COLOR);

  496.     // Draw vertial grid lines    
  497.     int rowOffset = (this.GRID_RESOLUTION * this.GRID_RESOLUTION) - this.GRID_RESOLUTION;
  498.     for(int i = 0; i < this.GRID_RESOLUTION; i++)
  499.     {
  500.       this.parentApplet.line( this.gridPoints[i].x, this.gridPoints[i].y, 
  501.                               this.gridPoints[i+rowOffset].x, this.gridPoints[i+rowOffset].y );
  502.     }
  503.     
  504.     // Draw horezontal grid lines
  505.     for(int y = 0; y < this.GRID_RESOLUTION; y++)
  506.     {
  507.       int row = this.GRID_RESOLUTION * y;
  508.       this.parentApplet.line( this.gridPoints[row].x, this.gridPoints[row].y, 
  509.                               this.gridPoints[row + this.GRID_RESOLUTION - 1].x, this.gridPoints[row + this.GRID_RESOLUTION - 1].y );
  510.     }
  511.     
  512.     // Draw the control points.
  513.     for(int i = 0; i < this.controlPoints.length; i++)
  514.     {
  515.       this.drawControlPoint(this.controlPoints[i].x, this.controlPoints[i].y, (this.activePoint == i));
  516.     }
  517.   }
  518.   
  519.   void drawControlPoint(float x, float y, boolean selected )
  520.   {
  521.     if(selected)
  522.     {
  523.       this.parentApplet.stroke( this.SELECTED_CORNER_MARKER_COLOR );
  524.     }
  525.     else
  526.     {
  527.       this.parentApplet.stroke(this.CORNER_MARKER_COLOR);
  528.     }
  529.     
  530.     this.parentApplet.ellipse( x, y, 16, 16);
  531.     this.parentApplet.line( x, y - 8, x, y + 8);
  532.     this.parentApplet.line( x - 8, y, x + 8, y);
  533.   }
  534. }

  535. class Point2D
  536. {
  537.   float x = 0;
  538.   float y = 0;
  539.   
  540.   float u = 0;
  541.   float v = 0;
  542.   
  543.   void copyPoint( Point2D other )
  544.   {
  545.     this.x = other.x;
  546.     this.y = other.y;
  547.     this.v = other.v;
  548.     this.u = other.u;
  549.   }
  550. }
Hey everyone,


Just letting you know that the problem discussed above has been fixed in the Keystone library, using the JAI perspective transform method.  The cursor position code works as well..

Better late than never,
d.