Determining affine transforms from three points
Status: Done
Confidence: Certain
Math in this page not rendering? See the fix
One of my labmates needed to visit a grid of points on a microscopy stage, something the software provided with the microscope wouldn’t handle. I wrote a program to generate grids of points for her. The heart of it is this calculation that three distinct points in the plane determine an affine transformation. This is true in two dimensions. In general, you need a nondegenerate simplex, i.e., in three dimensions you need four non-coplanar points.
In what follows, the thth component of vector is written , where or 1 (we have only two indices since we are working in two dimensions). All the source code is Scheme.
We have a grid of positions with columns labelled 0 to and rows labelled 0 to . It is worth labelling from 0, as it simplifies the mathematics dramatically. The only reason not to is that MATLAB begins counting at 1 in the old FORTRAN manner instead of at 0 as in all other languages. I will show the version starting at 1 and going to or at the end. We transform the labels for the grid positions into a grid of coordinates on a chip we are to observe under the microscope. The grid may be stretched, translated, and rotated from our labelling coordinate system. We can always make a translation tau to make the origins of the two systems coincide. Then we have two vector spaces (since we have found an origin) and the transformation between them is a linear function . The chip coordinates are related to the labels by a transformation of the form
or in coordinate form
This is known as an affine transform. There are six unknown numbers in this equation, four components for T and two for tau. Therefore we need three points to calculate them. We choose the bottom left, bottom right, and top right labels of our grid. Their coordinates in the two systems are
p | q |
---|---|
a | (0,0) |
b | (N,0) |
c | (N,M) |
These points give us a set of linear equations
Now we turn to actually programming the transform. I am representing points in Scheme as functions of an integer argument just as I do mathematically
(define (point x y)
(lambda (i)
(if (= i 0) x y)))
(define (show-point p)
(list (p 0) (p 1)))
Similarly, will be a function of two arguments and tau a function of one. Given such functions, we can produce a function which does the point transform with
(define (generate-point-transform T tau)
(lambda (p)
(apply point (map (lambda (i) (+ (* (T i 0) (p 0))
(* (T i 1) (p 1))
(tau i))) '(0 1)))))
To build ’s and ’s, we use
(define (generate-T a b c M N)
(lambda (i j)
(if (= j 0) (/ (- (b i) (a i)) N)
(/ (- (c i) (b i)) M))))
(define (generate-tau a b c M N)
(lambda (i)
(a i)))
We need to be able to produce grids of points to work with.
generate-grid
does exactly that.
(define (rem a b)
(- a (* (floor (/ a b)) b)))
(define (seq a b)
(if (< b a) '() (cons b (seq a (- b 1)))))
(define (generate-grid max-row max-col)
(map (lambda (n) (point (floor (/ n (+ max-col 1))) (rem n (+ max-col 1))))
(seq 0 (- (* (+ max-row 1) (+ max-col 1)) 1))))
In the following we create a transform with generate-point-transform
and bind it to F
, then use it to transform a number a list
of points. Note that we pass
and
to generate-T
and generate-tau
. Remember that
and
are the last label of the grid, not the number of rows and columns!
(let ((F (generate-point-transform
(generate-T (point 0 0)
(point (/ 1 (sqrt 2)) (/ 1 (sqrt 2)))
(point 0 (sqrt 2))
1 1)
(generate-tau (point 0 0)
(point (/ 1 (sqrt 2)) (/ 1 (sqrt 2)))
(point 0 (sqrt 2))
1 1))))
(map show-point (map F (generate-grid 3 3))))
This approach is much easier than trying to figure out the geometry by hand, as it requires not particular insight or labor, just a clean calculation.
Appendix: Beginning at 1 instead of 0
If you are stuck in an environment where you must begin counting at 1, the matrices above become rather more complicated. To begin with, our points change to
p | q |
---|---|
a | (1,1) |
b | (N,1) |
c | (N,M) |
and the equation that defines T and becomes
= { take the inverse of the central matrix in Maxima, and left multiply
both sides with it }
= { carry out the multiplication }
And we can implement it by providing functions to generate and .
(define (denominator M N) (+ (* M N) (- N) (- M) 1))
(define (generate-base-1-T a b c M N)
(lambda (i j)
(/ (if (= j 0) (* (- M 1) (- (b i) (a i)))
(* (- N 1) (- (c i) (b i))))
(denominator M N))))
(define (generate-base-1-tau a b c M N)
(lambda (i)
(/ (+ (* (a i) (- (* M N) N)) (* (b i) (- N M)) (* (c i) (- 1 N)))
(denominator M N))))
At this point the reader should be wondering what person in their right mind would count from 1 instead of 0. Yet it does work. For example,
(let ((F (generate-point-transform
(generate-base-1-T (point 0 0)
(point (/ 1 (sqrt 2)) (/ 1 (sqrt 2)))
(point 0 (sqrt 2))
2 2)
(generate-base-1-tau (point 0 0)
(point (/ 1 (sqrt 2)) (/ 1 (sqrt 2)))
(point 0 (sqrt 2))
2 2))))
(map show-point (map F (list (point 1 1) (point 2 1) (point 2 2) (point 1 2)))))