Tutorial 08: Image warping

OpenVG specifications state that images, as opposed to paths, can be drawn using a perspective image-user-to-surface transformation. The vgDrawImage function uses a 3x3 projective (or perspective) transformation matrix (representing the image-user-to-surface transformation) that maps user coordinates to surface coordinates.

Lets see what happens.


Perspective transformations

A projective matrix consists of the following 9 entries:

$$ \begin{bmatrix} sx & shx & tx \\ shy & sy & ty \\ w_0 & w_1 & w_2 \end{bmatrix} $$

The projective transformation maps a point (x, y) into the point:

The concatenation of two projective transformations is a projective transformation, whose matrix form is the product of the matrices of the original transformations. Both affine and projective transformations map straight lines to straight lines. However, affine transformations map evenly spaced points along a source line to evenly spaced points in the destination, whereas projective transformations allow the distance between points to vary due to the effect of division by the denominator.

Although OpenVG does not provide support for three-dimensional coordinates, proper setting of the w matrix entries can simulate the effect of placement of images in three dimensions, as well as other warping effects.


Image warping

OpenVG (VGU) provides three utility functions to compute 3x3 projective transform matrices. The first two compute the transformation from an arbitrary quadrilateral onto the unit square, and vice versa. The third computes the transformation from an arbitrary quadrilateral to an arbitrary quadrilateral. The output transformation is stored into matrix argument as 9 elements, in the order sx, shy, w0, shx, sy, w1, tx, ty, w2:

// sets the entries of the given matrix to a projective transformation that maps points:
// (sx0, sy0) to (0, 0)
// (sx1, sy1) to (1, 0)
// (sx2, sy2) to (0, 1)
// (sx3, sy3) to (1, 1)
VGUErrorCode vguComputeWarpQuadToSquare(VGfloat sx0, VGfloat sy0,
                                        VGfloat sx1, VGfloat sy1,
                                        VGfloat sx2, VGfloat sy2,
                                        VGfloat sx3, VGfloat sy3,
                                        VGfloat* matrix);
vguComputeWarpQuadToSquare
vguComputeWarpQuadToSquare
// sets the entries of the given matrix to a projective transformation that maps points:
// (0, 0) to (dx0, dy0)
// (1, 0) to (dx1, dy1)
// (0, 1) to (dx2, dy2)
// (1, 1) to (dx3, dy3)
VGUErrorCode vguComputeWarpSquareToQuad(VGfloat dx0, VGfloat dy0,
                                        VGfloat dx1, VGfloat dy1,
                                        VGfloat dx2, VGfloat dy2,
                                        VGfloat dx3, VGfloat dy3,
                                        VGfloat* matrix);
vguComputeWarpSquareToQuad
vguComputeWarpSquareToQuad
// sets the entries of the given matrix to a projective transformation that maps points:
// (sx0, sy0) to (dx0, dy0)
// (sx1, sy1) to (dx1, dy1)
// (sx2, sy2) to (dx2, dy2)
// (sx3, sy3) to (dx3, dy3)
VGUErrorCode vguComputeWarpQuadToQuad(VGfloat dx0, VGfloat dy0,
                                      VGfloat dx1, VGfloat dy1,
                                      VGfloat dx2, VGfloat dy2,
                                      VGfloat dx3, VGfloat dy3,
                                      VGfloat sx0, VGfloat sy0,
                                      VGfloat sx1, VGfloat sy1,
                                      VGfloat sx2, VGfloat sy2,
                                      VGfloat sx3, VGfloat sy3,
                                      VGfloat* matrix);
vguComputeWarpQuadToQuad
vguComputeWarpQuadToQuad

In all cases, if there is no projective mapping that satisfies the given constraints, or the mapping would be degenerate (i.e., non-invertible), VGU_BAD_WARP_ERROR is returned and the output matrix is unchanged.


The tutorial code

At initialization time, and every time the drawing surface changes size (i.e. due to a window resize / device orientation turn), an image (representing a sporty girl) is generated by drawing several paths with different colors, then pixels are copied from the drawing surface to the VGImage object using the vgGetPixels function. Paths and colors data, defining the vector girl, is stored in a separate file (see girl_data.c / GirlData.java).

The sporty girl image, generated from vector paths
The sporty girl image, generated from vector paths

The tutorial code maps the image outline to a custom quadrilateral, using the vguComputeWarpQuadToQuad function. The quadrilateral is defined by four control points, that can be moved by mouse / touch.

VGImage girlImage;
VGfloat warpMatrix[9];
ControlPoint controlPoints[4];

// calculate warp matrix
vguComputeWarpQuadToQuad(// destination quadrilateral
                         controlPoints[0].x, controlPoints[0].y,
                         controlPoints[1].x, controlPoints[1].y,
                         controlPoints[2].x, controlPoints[2].y,
                         controlPoints[3].x, controlPoints[3].y,
                         // source image bounds
                         0.0f, 0.0f,
                         imageWidth, 0.0f,
                         imageWidth, imageHeight,
                         0.0f, imageHeight,
                         // the output matrix
                         warpMatrix);
// upload the warp matrix to the OpenVG backend
vgSeti(VG_MATRIX_MODE, VG_MATRIX_IMAGE_USER_TO_SURFACE);
vgLoadIdentity();
vgLoadMatrix(warpMatrix);
// draw girl image
vgDrawImage(girlImage);
Warp example 1Warp example 2
Warp example 1Warp example 2