PIL projective transform

im.transform(size, PERSPECTIVE, data) image ⇒ image

size = output image size
PERSPECTIVE = Image.PERSPECTIVE (int)
data = 8-tuple (a, b, c, d, e, f, g, h) of coefficients

Questions you may have:
1) How do you know the size of the image after the transform to set the size before doing the transform?
2) How is the transformed image placed within that pre-sized output window?
3) Where does the data 8-tuple come from?
4) How can I use the 8-tuple coefficients to project a point back and forth between original and transformed image.

Answers:
1&2) It depends on how much of the image you want to include. The destination points determine the placement within the new image. You can first calculate the coefficients (forward form, see Ans. 4), then calculate where the image corners will end up, then shift the destination points (or corners) to the desired placement and adjust the image size. Finally, calculate the coefficients a second time (backward form) for the Image.transform function.

3) The coefficients are calculated with some matrix math. You can find an explanation elsewhere. I’ll just show some code:

def get_transform_data(pts8, backward=True ):
    '''This method returns a perspective transform 8-tuple (a,b,c,d,e,f,g,h).

    Use to transform an image:
    X = (a x + b y + c)/(g x + h y + 1)
    Y = (d x + e y + f)/(g x + h y + 1)
    
    Image.transform: Use 4 source coordinates, followed by 4 corresponding 
        destination coordinates. Use backward=True (the default)
        
    To calculate the destination coordinate of a single pixel, either reverse
        the pts (4 dest, followed by 4 source, backward=True) or use the same
        pts but set backward to False.
    
    @arg pts8: four source and four corresponding destination coordinates
    @kwarg backward: True to return coefficients for calculating an originating
        position. False to return coefficients for calculating a destination
        coordinate. (Image.transform calculates originating position.)
    '''
    assert len(pts8) == 8, 'Requires a tuple of eight coordinate tuples (x,y)'
    
    b0,b1,b2,b3,a0,a1,a2,a3 = pts8 if backward else pts8[::-1]
    
    # CALCULATE THE COEFFICIENTS
    A = array([[a0[0], a0[1], 1,     0,     0, 0, -a0[0]*b0[0], -a0[1]*b0[0]],
               [    0,     0, 0, a0[0], a0[1], 1, -a0[0]*b0[1], -a0[1]*b0[1]],
               [a1[0], a1[1], 1,     0,     0, 0, -a1[0]*b1[0], -a1[1]*b1[0]],
               [    0,     0, 0, a1[0], a1[1], 1, -a1[0]*b1[1], -a1[1]*b1[1]],
               [a2[0], a2[1], 1,     0,     0, 0, -a2[0]*b2[0], -a2[1]*b2[0]],
               [    0,     0, 0, a2[0], a2[1], 1, -a2[0]*b2[1], -a2[1]*b2[1]],
               [a3[0], a3[1], 1,     0,     0, 0, -a3[0]*b3[0], -a3[1]*b3[0]],
               [    0,     0, 0, a3[0], a3[1], 1, -a3[0]*b3[1], -a3[1]*b3[1]]] )
    B = array([b0[0], b0[1], b1[0], b1[1], b2[0], b2[1], b3[0], b3[1]])
    
    return linalg.solve(A,B)

If your image has black borders that you want to make transparent, this code could help:

    transparency = 150
    I = asarray(transimage)  # (y, x, band)
    maskmat = (I[:,:,0] | I[:,:,1] | I[:,:,2] == 0)
    mask = ones(size, int).transpose() * transparency
    mask[maskmat] = 0
    mask = Image.fromarray(uint8(mask))
    transimage.putalpha(mask)

This code applies an alpha mask to a transformed image (transimage). Transparency is how transparent the image part will be and the maskmat region is for all the black (non-image) border area. This area of the mask is set to completely transparent (mask[maskmat] = 0). This works with Tkinter where an alpha of zero is transparent and 255 is opaque.

4) When I first saw the equation for calculating the point transform I believed I must have read it wrong. “Why would the equations move a point from the transformed image back to the original? I must be reading this wrong,” I thought. But the data set passed to the transform method must be working in the following way: It first creates a blank image of the specified size and then uses the ‘backwards’ equation to calculate from which pixel in the original image to copy color values into the destination location. Well, it seems counter-intuitive to me so I still call it ‘backwards’ (where ‘backward’ means mapping from destination to the origin, and ‘forward’ is origin to destination). Here is the code to transform a point:

def transform_pt(pt , coeffs ):
    T = coeffs
    x = (T[0]*pt[0] + T[1]*pt[1] + T[2])/(T[6]*pt[0] + T[7]*pt[1] + 1)
    y = (T[3]*pt[0] + T[4]*pt[1] + T[5])/(T[6]*pt[0] + T[7]*pt[1] + 1)
    return x,y

Two ways to use this. Pass a destination coord with the default (backward) transformation coefficients, or pass an origin coord with the forward (backward=False) coefficients.