7

I stumbled upon this question and am trying to perform perspective transformation with Python Pillow.

This is specifically what I am trying to do and what the result looks like:

enter image description here

And this is the code that I used to try it:

from PIL import Image
import numpy

# function copy-pasted from https://stackoverflow.com/a/14178717/744230
def find_coeffs(pa, pb):
    matrix = []
    for p1, p2 in zip(pa, pb):
        matrix.append([p1[0], p1[1], 1, 0, 0, 0, -p2[0]*p1[0], -p2[0]*p1[1]])
        matrix.append([0, 0, 0, p1[0], p1[1], 1, -p2[1]*p1[0], -p2[1]*p1[1]])

    A = numpy.matrix(matrix, dtype=numpy.float)
    B = numpy.array(pb).reshape(8)

    res = numpy.dot(numpy.linalg.inv(A.T * A) * A.T, B)
    return numpy.array(res).reshape(8)

# test.png is a 256x256 white square
img = Image.open("./images/test.png")

coeffs = find_coeffs(
    [(0, 0), (256, 0), (256, 256), (0, 256)],
    [(15, 115), (140, 20), (140, 340), (15, 250)])

img.transform((300, 400), Image.PERSPECTIVE, coeffs,
              Image.BICUBIC).show()

I am not exactly sure how the transformation works, but it seems the points move quite into the opposite direction (e.g. I need to do (-15, 115) to make point A move to the right. However, it also won't move 15 pixels but 5).

How can I determine the exact coordinates of the target points to skew the image properly?

Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
Maxim Zubarev
  • 2,403
  • 2
  • 29
  • 48

1 Answers1

15

The answer is very simple: just swap the source and target coordinates. But it's not your fault: the author of the linked answer made it particularly easy to get confused, because target, source is (in this case) a confusing order for function arguments, because function arguments have no helpful names, and because the example does the backward transformation of a shear.

Instead of swapping source and target coordinates, you can also swap the arguments of the find_coeffs function. Even better, rename them too, like

def find_coeffs(source_coords, target_coords):
    matrix = []
    for s, t in zip(source_coords, target_coords):
        matrix.append([t[0], t[1], 1, 0, 0, 0, -s[0]*t[0], -s[0]*t[1]])
        matrix.append([0, 0, 0, t[0], t[1], 1, -s[1]*t[0], -s[1]*t[1]])
    A = numpy.matrix(matrix, dtype=numpy.float)
    B = numpy.array(source_coords).reshape(8)
    res = numpy.dot(numpy.linalg.inv(A.T * A) * A.T, B)
    return numpy.array(res).reshape(8)

Leaving the rest of your code the same, only using a different image, I get this transformation:

Walter with red corners    ⇒    Walter with red corners in perspective

Walter Tross
  • 12,237
  • 2
  • 40
  • 64
  • As classical as your test image is, I think it's time for something else to take its place. In my own answers I try to use pictures I've taken myself. – Mark Ransom Oct 31 '18 at 22:32
  • I disagree, but don't want to discuss this here, so I'm replacing the image with an image of myself (taken by my son) – Walter Tross Oct 31 '18 at 22:51
  • stupidme just realised i didn't see `where pb is the four vertices in the current plane, and pa contains four vertices in the resulting plane` in the original SO answer so i assumed pb was the resulting plane and pa the current one. i was wondering why nobody else had run into issues with that code but i am apparently not able to read :) thank you! – Maxim Zubarev Nov 01 '18 at 09:56
  • you are welcome! But, as I wrote, IMHO it's not your fault. BTW, I polished a bit that `find_coeffs()` swapping the loop variables in the third line. – Walter Tross Nov 01 '18 at 14:22