HELP WARP_2D David Young July 1993 LIB *WARP_2D provides procedures to apply an affine transformation to an array of numbers, which might represent an image. An affine transformation is one in which straight lines stay straight and parallel lines stay parallel; also if a line is divided in a given ratio than the transformed line is divided in the same ratio. An image subjected to an affine transformation may undergo translation, expansion or contraction, rotation and shearing. CONTENTS - (Use g to access required sections) -- The main procedure -- Specifying the transformation -- ... Using matrix elements -- ... Using corresponding points -- ... Using expansion, rotation and aspect ratio -- ... Using first-order optic flow parameters -- ... Warning -- Note on aliasing -- Ancillary procedures -- Examples -- ... An optic flow example -- ... An image distortion example -- The main procedure ------------------------------------------------- warp_2d(ARRIN, REGIONIN, T, ARROUT, REGIONOUT, OPTIONS) -> ARROUT ARRIN is the input array, which must only contain numbers. The procedure is most efficient if this is a packed array of single-precision floats, e.g. an array created with *newsfloatarray or array_of_real (see *EXTERNAL). REGIONIN is for future use. Give false for this argument for now. T is a list or vector specifying the affine transformation. See the section below for what this may contain. ARROUT is the output array. If this is an array on entry to the procedure, efficiency is highest if it is a packed array of floats. In this case, the results are put into this array and it is returned (so no garbage is created). If it is an array of another type, then its contents are copied to a new array made with newsfloatarray and the new array is returned. If ARROUT is false, a new array with boundslist (REF * ARRAYS) given by REGIONOUT is created and returned with the results in it. Finally, ARROUT can be a 5-element list. In this case a new array is created with a boundslist given by the last 4 elements of the list, and initialised to the value given by the head of the list. REGIONOUT is a 4-element list giving a boundslist-type specification of the region of ARROUT in which to place the results. If REGIONOUT is false, then the whole of ARROUT is filled. (One of ARROUT and REGIONOUT must be non-false.) OPTIONS is a list of words or a single word. It can be the empty list to take all the defaults or a single word to specify only one option and accept defaults for others. The list can contain (or the word can be): Either interpolate or nearest (default is interpolate). interpolate causes values to be obtained by bilinear interpolation between the 4 nearest neighbours to the "true" (non-integer) sample position in the input array. nearest means that the value of the nearest element to the "true" sample position is taken. The nearest option is a little faster, but interpolation usually provides better results on real images. Either mishap or ignore (default is mishap). mishap means that if data from outwith the input array would be needed in order to fill the output region, a mishap will occur. ignore means that only those parts of the output region that can be filled from the input array are changed; any point that needs data from outwith the input array is left alone. mishap is faster because only the corners of the input region need be checked, and this is done once before looping. ignore is implemented by applying a test on every output point. warp_2d(ARRIN, REGIONIN, Tx, Ty, Txx, Txy, Tyx, Tyy, ARROUT, REGIONOUT, OPTIONS) -> ARROUT In this form, all the arguments are as before except that the transformation is specified by 6 separate parameters in order to avoid the need to build a list or vector. See below for what the parameters mean. -- Specifying the transformation -------------------------------------- Position in the input array is given by x and y. These denote both array indices and position in the image plane - that is, ARRIN(x,y) holds the grey level at (x,y) in the image plane. ARRIN(x,y) is only defined if x and y are integers, but non-integer values of x and y can be thought of as referring to positions in the image plane between the pixel centres. Similarly, x' and y' refer to array elements in ARROUT and positions in the output image plane. The affine transformation is a rule that maps each point P = (x,y) in the input image to some point P' = (x',y') in the output image. It is specified by 6 parameters, but there are various ways to state them. The procedure provides a choice of ways to set up the transformation using the T argument. There are some references to rotations and angles below - the parameters phi, theta and R specify angular quantities. If in the image plane the (x,y) coordinates run the conventional way for graphs, thus: y ^ | | +-------> x then anticlockwise angles and rotations are positive. If the coordinates run the conventional way for image display, thus: +-------> | x | y V then clockwise angles and rotations are positive. -- ... Using matrix elements ------------------------------------------ The basic equation for an affine transformation is - - - - - - - - | x' | | Tx | | Txx Txy | | x | | | = | | + | | | | | y' | | Ty | | Tyx Tyy | | y | - - - - - - - - The six parameters can be specified to the procedure as a list or vector, in which the name of each is followed by its value, for example: {Tx 3 Ty 2 Txx 1.3 Txy 0.25 Tyx -0.4 Tyy 0.9} If some of the pairs are omitted, the values default to 0 except for Txx and Tyy which default to 1. The pairs can be given in any order as long as each value follows its name. Alternatively, a vector (but not a list) containing just the six values can be given, e.g. {3 2 1.3 0.25 -0.4 0.9} In this case, all the values must be present and in the order Tx Ty Txx Txy Tyx Tyy. Finally, the parameters can be given as individual arguments in the second form of the routine call. -- ... Using corresponding points ------------------------------------- The transformation is uniquely determined by stating the effect on 3 non-collinear points in the image. This is specified by a list or vector of the form { P 2 3 -> 7 6 P 3 10 -> -4 8 P 1 -9 -> 2 2 } In each line, the first pair of numbers is the coordinates of a point P in the input image, and the second pair is the coordinates of a point P' in the output image. The transform is set up so that each P gets moved to the corresponding P'. (The words "P" and "->" must occur in the vector or list in the positions shown.) It is sufficient to give two correspondences. In this case the procedure assumes that the origin is fixed - i.e. "P 0 0 -> 0 0" is used as the third point. The points used to specify the mapping do not have to lie inside the input or output regions, or even inside the arrays. -- ... Using expansion, rotation and aspect ratio --------------------- Possibly a more understandable way to specify the mapping is to use quantities with simple geometrical interpretations. The expansion, E, gives the change in size of the image - e.g. E = 2 doubles the size. If E < 1 then a contraction results. The angle phi specifies a rotation of the image. See the note above about the sense of rotations. This angle must be given in degrees or radians depending on the value of *popradians. The aspect ratio change A specifies a change in shape, with expansion in one direction and shrinkage in the orthogonal direction. A gives the relative amounts of expansion and contraction - if a circle is transformed, then it will end up as an ellipse with one axis A times as long as the other. The angle theta, measured from the x axis, gives the direction of expansion if A > 1 or of contraction if A < 1. This part of the transform is applied, in effect, before any rotations, so theta refers to a direction in the input image. Theta must be in degrees or radians as appropriate to popradians. The aspect ratio change operates on top of any overall expansion or contraction given by E. In addition to these quantities, it is necessary to fix the translation of the image by giving one point correspondence. An example of a specification using these quantities is therefore: {P 1 1 -> 2 2 E 1.5 phi 45 A 2 theta -30} If parts are omitted, the defaults used are: (0,0) -> (0,0), E = 1, phi = 0, A = 1, theta = 0. The single correspondence can also be given by setting a fixed point with the P0 parameter, e.g. {P0 1 2 E 1.5 phi 45 A 2 theta -30} This is equivalent to P 1 2 -> 1 2. The point at P0 in the input image is mapped to the point with the same coordinates in the output image. The relationship between these parameters and the matrix parameters is given by - - - - - - - - - - | Txx Txy | | cp -sp | | ct -st | | Ex 0 | | ct st | | | = | | | | | | | | | Tyx Tyy | | sp cp | | st ct | | 0 Ey | | -st ct | - - - - - - - - - - where cp = cos phi, sp = sin phi, ct = cos theta, st = sin theta, Ex = E * sqrt(abs(A)), Ey = sign(A) * E / sqrt(abs(A)). The translation is given by - - - - - - - - | Tx | | x' | | Txx Txy | | x | | | = | | - | | | | | Ty | | y' | | Tyx Tyy | | y | - - - - - - - - where P x y -> x' y' has been specified. -- ... Using first-order optic flow parameters ------------------------ The change between two images of a surface patch viewed by a moving camera can be approximated by an affine transformation. The transformation can be specified in terms of "first-order flow parameters". The dilation, D, specifies an expansion or contraction. Positive D means an expansion and negative D means a contraction. (For |D| << 1, E is approximately 1 + D.) R specifies the amount of rotation. See the note above about the sense of the rotation. The effect of R is only close to a pure rotation for |R| << 1, when R specifies a rotation in radians. The way the procedure uses R is not affected by popradians. This is because the proper definition of R is not as an angle but as part of the transformation matrix shown below. S specifies the amount of shear. (For |S| << 1, A is approximately 1 + 2S.) The axis of shear is given by theta. Theta must be in degrees or radians as appropriate to popradians. Finally, the translation of a single point must also be specified. The coordinates of the point are given by P0 and its translation from the input to the output image by Vx and Vy. Thus the overall specification might look like: {P0 223 127 Vx 13 Vy -8 D 0.05 R -0.02 S 0.025 theta 50} If any of these parameters is omitted it defaults to 0. Instead of S and theta, the shear can be specified by S1 and S2, where S1 = S cos(2*theta) and S2 = S sin(2*theta). It is also possible to give, instead, the optic flow differential parameters Vxx, Vxy, Vyx, Vyy. These are related to dilation etc. by - - - - | Vxx Vxy | | D+S1 S2-R | | | = | | | Vyx Vyy | | S2+R D-S1 | - - - - Thus one can give an argument like {P0 50 88 Vx 7 Vy 2 Vxx -0.03 Vxy 0.015 Vyx 0 Vyy -0.02} The flow parameters are related to the matrix parameters by - - - - | Txx Txy | | 1+D+S1 S2-R | | | = | | | Tyx Tyy | | S2+R 1+D-S1 | - - - - and - - - - - - - - | Tx | | x + Vx | | Txx Txy | | x | | | = | | - | | | | | Ty | | y + Vy | | Tyx Tyy | | y | - - - - - - - - where x and y are the coordinates following P0. Since the flow parameters are suited to describing instantaneous image velocities rather than a finite transformation, the representation really only makes sense if the absolute values of D, R and S (or Vxx, Vxy, Vyx and Vyy) are all much less than one. -- ... Warning -------------------------------------------------------- Do not mix up different kinds of specifications. You must select one set of parameters to work with: Tx, Ty, Txx, Txy, Tyx, Tyy or 3 correspondences of the form P x y -> x' y' or P or P0, E, phi, A, theta or P0, Vx, Vy, D, R, (S, theta) or (S1, S2) or P0, Vx, Vy, Vxx, Vxy, Vyx, Vyy If you do mix up parameters from different sets (e.g. you have E and R together) then the procedure will quietly ignore some of them. If you let enough parameters default that it is not apparent which set you are using, you will get a result that is consistent with every possible set (because all the defaults select the no-effect case). -- Note on aliasing --------------------------------------------------- Because the procedure never averages over more than four points, aliasing will occur if drastic shrinkage is applied. If this is a problem, a solution in some cases is to smooth the array first using *CONVOLVE_GAUSS_2D or something similar. You may need to generate your own anisotropic smoothing mask and use *CONVOLVE_2D if the transform has a lot of shear (A very different from 1, or equivalent). Alternatively it may help to carry out the shrinkage in several steps, each shrinking by a factor of 2 or less, making sure that the sample points occur between pixel centres, and using interpolation sampling. In some applications aliasing won't matter anyway. -- Ancillary procedures ----------------------------------------------- warp_trans(T) -> (TRANSF, TRANSB) warp_trans(Tx, Ty, Txx, Txy, Tyx, Tyy) -> (TRANSF, TRANSB) This takes a transform specification in any of the forms described above, and returns two procedures: TRANSF(X, Y) -> (XP, YP) Transforms from coordinates in the input image to coordinates in the output image. TRANSB(XP, YP) -> (X, Y) Transforms from coordinates in the output image to coordinates in the input image. warp_params(T) -> (Tx, Ty, Txx, Txy, Tyx, Tyy) warp_params(Tx, Ty, Txx, Txy, Tyx, Tyy) -> (Tx, Ty, Txx, Txy, Tyx, Tyy) Takes a transform specification in any of the forms described above, and returns the separate matrix elements. The second form of the procedure call is for completeness and does nothing. If warp_2d is to be called many times to carry out the same transform, there will be a small efficiency gain if warp_params is used to get the matrix elements once to start with, and these are then passed separately or in a 6-element vector to warp_2d. warp_params has to be called explicitly to use the procedures that follow. warp_xy(X, Y, TVEC) -> (XP, YP) warp_xy(X, Y, Tx, Ty, Txx, Txy, Tyx, Tyy) -> (XP, YP) In the first form, TVEC is a 6-element vector containing the values of the parameters Tx..Tyy in order. The procedure applies the transform specified by the parameters to X and Y. It may often be more convenient to use warp_trans, which returns closures of warp_xy. warp_invert(TVEC) -> (Ux, Uy, Uxx, Uxy, Uyx, Uyy) warp_invert(Tx, Ty, Txx, Txy, Tyx, Tyy) -> (Ux, Uy, Uxx, Uxy, Uyx, Uyy) Returns the parameters of the inverse transform. TVEC is a 6-element vector containing Tx..Tyy. Ux..Uyy specify the mapping from the output image to the input image. Unless the parameters are needed explicitly, it may be more convenient to use the backward procedure produced by warp_trans. -- Examples ----------------------------------------------------------- The best way to understand this program, and affine transformations in general, is to run the examples, and then to play with the transform parameters. -- ... An optic flow example ------------------------------------------ Load some libraries: uses warp_2d uses rci_show ;;; for display uses float_arrayprocs ;;; for image adding and differencing Create a test array with a sparse set of random dots (1 pixel in 50 is set to 1, the rest to 0): vars bounds = [50 250 -125 125]; vars image = newsfloatarray(bounds, erase <> erase <> random(%50%) <> nonop div(%50%)); Note that the bounds do not start at 0 or 1 - warp_2d does not mind. Assuming you are using a suitable terminal, look at it with rci_show(image) -> ; Fix a point in the image, and apply some dilation, using the "nearest" option because we want to look at individual dots: vars imout; warp_2d(image, false, [P0 125 0 D 0.02], false, bounds, [nearest mishap]) -> imout; If you just display the output image you will not be able to see what has happened. But if you overlay the input and output images by adding them you can see the flow pattern: 2 -> rci_show_scale; rci_show(float_arraysum(image, imout, false)) -> ; The bright dots in the middle are where the movement is less than a pixel so the input and output points coincide. Now try a more general flow, with contraction, rotation and shearing, and a different fixed point: false -> popradians; ;;; give theta in degrees warp_2d(image, false, [P0 150 30 D -0.03 R 0.03 S 0.02 theta 45], 1.0 :: bounds, bounds, [nearest ignore]) -> imout; rci_show(float_arraysum(image, imout, false)) -> ; The "ignore" option had to be used because a region larger than the input array gets mapped onto the output array. The "mishap" option would indeed have mishapped. You can see the transformed outline of the input array as a grey border - this was produced by initialising the output array elements to 1.0 in the ARROUT argument. Inspecting the border confirms that the rotation was clockwise (y is downwards when images are displayed with rci_show) and that stretch axis for shear is about 45o clockwise from the x axis. -- ... An image distortion example ------------------------------------ Load the libraries used above if you have not already done so. Also get an image to play with: uses arrayfile vars image; arrayfile(popvision_data dir_>< 'stereo1.pic') -> image; 2 -> rci_show_scale; rci_show(image) -> ; This image has a boundslist of boundslist(image) => ** [80 176 64 191] For the sake of illustration, we will use a standard output size of 200 x 200 pixels. vars bounds = [1 200 1 200]; First, map the original image onto the new square image, with no distortion. The easiest way to do this is with point correspondences. vars imout; warp_2d(image, false, [P 80 64 -> 1 1 ;;; top left corner P 80 191 -> 1 200 ;;; bottom left P 176 64 -> 200 1 ], ;;; top right false, bounds, [interpolate mishap]) -> imout; rci_show(imout) -> ; The "mishap" option was OK because this was set up to only use data from the input array - though it does use all of it. Repeat this with "nearest" instead of "interpolate" to see graphically the difference between these two options. Note that *arraysample provides an alternative way of doing transformations as simple as this. What it won't do is something like warp_2d(image, false, [P 80 64 -> 50 50 ;;; top left corner P 80 191 -> 1 200 ;;; bottom left P 176 64 -> 200 1 ], ;;; top right false, bounds, [interpolate ignore]) -> imout; rci_show(imout) -> ; Here is an example of using the finite deformation parameters to do the same sort of thing. We put the centre of the image at the centre of the output array and change its aspect ratio, stretching along the diagonal: false -> popradians; ;;; theta in degrees warp_2d(image, false, [P 128 128 -> 100 100 ;;; centre (approx) A 2 theta -45], false, bounds, [interpolate ignore]) -> imout; rci_show(imout) -> ; and rotated and shrunk a bit as well: warp_2d(image, false, [P 128 128 -> 100 100 ;;; centre (approx) A 2 theta -45 phi -90 E 0.75], false, bounds, [interpolate ignore]) -> imout; rci_show(imout) -> ; --- $popvision/help/warp_2d --- Copyright University of Sussex 1993. All rights reserved.