web-dev-qa-db-fra.com

Comment fonctionne la transformation de perspective dans PIL?

PIL Image.transform a un mode perspective qui nécessite un 8-Tuple de données mais je ne peux pas comprendre comment convertir disons une inclinaison à droite de 30 degrés à ce Tuple.

Quelqu'un peut-il l'expliquer?

40
Hedge

Pour appliquer une transformation de perspective, vous devez d'abord connaître quatre points dans un plan A qui seront mappés à quatre points dans un plan B. Avec ces points, vous pouvez dériver la transformation homographique. Ce faisant, vous obtenez vos 8 coefficients et la transformation peut avoir lieu.

Le site http://xenia.media.mit.edu/~cwren/interpolator/ (miroir: WebArchive ), ainsi que de nombreux autres textes, décrit comment ces coefficients peut être déterminé. Pour vous faciliter la tâche, voici une implémentation directe selon le lien mentionné:

import numpy

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)

pb est les quatre sommets du plan actuel et pa contient quatre sommets du plan résultant.

Supposons donc que nous transformions une image comme:

import sys
from PIL import Image

img = Image.open(sys.argv[1])
width, height = img.size
m = -0.5
xshift = abs(m) * width
new_width = width + int(round(xshift))
img = img.transform((new_width, height), Image.AFFINE,
        (1, m, -xshift if m > 0 else 0, 0, 1, 0), Image.BICUBIC)
img.save(sys.argv[2])

Voici un exemple d'entrée et de sortie avec le code ci-dessus:

enter image description hereenter image description here

Nous pouvons continuer sur le dernier code et effectuer une transformation de perspective pour inverser le cisaillement:

coeffs = find_coeffs(
        [(0, 0), (256, 0), (256, 256), (0, 256)],
        [(0, 0), (256, 0), (new_width, height), (xshift, height)])

img.transform((width, height), Image.PERSPECTIVE, coeffs,
        Image.BICUBIC).save(sys.argv[3])

Résultant en:

enter image description here

Vous pouvez également vous amuser avec les points de destination:

enter image description hereenter image description here

68
mmgp

Je vais détourner cette question juste un tout petit pe parce que c'est la seule chose sur Google concernant les transformations de perspective en Python. Voici un code légèrement plus général basé sur ce qui précède qui crée une matrice de transformation en perspective et génère une fonction qui exécutera cette transformation sur des points arbitraires:

import numpy as np

def create_perspective_transform_matrix(src, dst):
    """ Creates a perspective transformation matrix which transforms points
        in quadrilateral ``src`` to the corresponding points on quadrilateral
        ``dst``.

        Will raise a ``np.linalg.LinAlgError`` on invalid input.
        """
    # See:
    # * http://xenia.media.mit.edu/~cwren/interpolator/
    # * http://stackoverflow.com/a/14178717/71522
    in_matrix = []
    for (x, y), (X, Y) in Zip(src, dst):
        in_matrix.extend([
            [x, y, 1, 0, 0, 0, -X * x, -X * y],
            [0, 0, 0, x, y, 1, -Y * x, -Y * y],
        ])

    A = np.matrix(in_matrix, dtype=np.float)
    B = np.array(dst).reshape(8)
    af = np.dot(np.linalg.inv(A.T * A) * A.T, B)
    return np.append(np.array(af).reshape(8), 1).reshape((3, 3))


def create_perspective_transform(src, dst, round=False, splat_args=False):
    """ Returns a function which will transform points in quadrilateral
        ``src`` to the corresponding points on quadrilateral ``dst``::

            >>> transform = create_perspective_transform(
            ...     [(0, 0), (10, 0), (10, 10), (0, 10)],
            ...     [(50, 50), (100, 50), (100, 100), (50, 100)],
            ... )
            >>> transform((5, 5))
            (74.99999999999639, 74.999999999999957)

        If ``round`` is ``True`` then points will be rounded to the nearest
        integer and integer values will be returned.

            >>> transform = create_perspective_transform(
            ...     [(0, 0), (10, 0), (10, 10), (0, 10)],
            ...     [(50, 50), (100, 50), (100, 100), (50, 100)],
            ...     round=True,
            ... )
            >>> transform((5, 5))
            (75, 75)

        If ``splat_args`` is ``True`` the function will accept two arguments
        instead of a Tuple.

            >>> transform = create_perspective_transform(
            ...     [(0, 0), (10, 0), (10, 10), (0, 10)],
            ...     [(50, 50), (100, 50), (100, 100), (50, 100)],
            ...     splat_args=True,
            ... )
            >>> transform(5, 5)
            (74.99999999999639, 74.999999999999957)

        If the input values yield an invalid transformation matrix an identity
        function will be returned and the ``error`` attribute will be set to a
        description of the error::

            >>> tranform = create_perspective_transform(
            ...     np.zeros((4, 2)),
            ...     np.zeros((4, 2)),
            ... )
            >>> transform((5, 5))
            (5.0, 5.0)
            >>> transform.error
            'invalid input quads (...): Singular matrix
        """
    try:
        transform_matrix = create_perspective_transform_matrix(src, dst)
        error = None
    except np.linalg.LinAlgError as e:
        transform_matrix = np.identity(3, dtype=np.float)
        error = "invalid input quads (%s and %s): %s" %(src, dst, e)
        error = error.replace("\n", "")

    to_eval = "def perspective_transform(%s):\n" %(
        splat_args and "*pt" or "pt",
    )
    to_eval += "  res = np.dot(transform_matrix, ((pt[0], ), (pt[1], ), (1, )))\n"
    to_eval += "  res = res / res[2]\n"
    if round:
        to_eval += "  return (int(round(res[0][0])), int(round(res[1][0])))\n"
    else:
        to_eval += "  return (res[0][0], res[1][0])\n"
    locals = {
        "transform_matrix": transform_matrix,
    }
    locals.update(globals())
    exec to_eval in locals, locals
    res = locals["perspective_transform"]
    res.matrix = transform_matrix
    res.error = error
    return res
9
David Wolever

Voici une version pure-Python de génération des coefficients de transformation (comme je l'ai vu cela demandé par plusieurs). Je l'ai créé et utilisé pour créer le package de dessin d'images PyDraw pure-Python.

Si vous l'utilisez pour votre propre projet, notez que les calculs nécessitent plusieurs opérations de matrice avancées, ce qui signifie que cette fonction nécessite une autre bibliothèque de matrice, en pur Python, appelée matfunc écrite à l'origine par Raymond Hettinger et que vous pouvez - télécharger ici ou ici .

import matfunc as mt

def perspective_coefficients(self, oldplane, newplane):
    """
    Calculates and returns the transform coefficients needed for a perspective 
    transform, ie tilting an image in 3D.
    Note: it is not very obvious how to set the oldplane and newplane arguments
    in order to tilt an image the way one wants. Need to make the arguments more
    user-friendly and handle the oldplane/newplane behind the scenes.
    Some hints on how to do that at http://www.cs.utexas.edu/~fussell/courses/cs384g/lectures/lecture20-Z_buffer_pipeline.pdf

    | **option** | **description**
    | --- | --- 
    | oldplane | a list of four old xy coordinate pairs
    | newplane | four points in the new plane corresponding to the old points

    """
    # first find the transform coefficients, thanks to http://stackoverflow.com/questions/14177744/how-does-perspective-transformation-work-in-pil
    pb,pa = oldplane,newplane
    grid = []
    for p1,p2 in Zip(pa, pb):
        grid.append([p1[0], p1[1], 1, 0, 0, 0, -p2[0]*p1[0], -p2[0]*p1[1]])
        grid.append([0, 0, 0, p1[0], p1[1], 1, -p2[1]*p1[0], -p2[1]*p1[1]])

    # then do some matrix magic
    A = mt.Matrix(grid)
    B = mt.Vec([xory for xy in pb for xory in xy])
    AT = A.tr()
    ATA = AT.mmul(A)
    gridinv = ATA.inverse()
    invAT = gridinv.mmul(AT)
    res = invAT.mmul(B)
    a,b,c,d,e,f,g,h = res.flatten()

    # finito
    return a,b,c,d,e,f,g,h
6
Karim Bahgat

Les 8 coefficients de transformation (a, b, c, d, e, f, g, h) correspondent à la transformation suivante:

x '= (a x + b y + c)/(g x + h y + 1)
y '= (d x + e y + f)/(g x + h y + 1)

Ces 8 coefficients peuvent en général être trouvés en résolvant 8 équations (linéaires) qui définissent comment 4 points sur la transformation plane (4 points en 2D -> 8 équations), voir la réponse par mmgp pour un code qui résout cela, bien que vous puissiez trouver un peu plus précis pour changer la ligne

res = numpy.dot(numpy.linalg.inv(A.T * A) * A.T, B)

à

res = numpy.linalg.solve(A, B)

c'est-à-dire qu'il n'y a aucune raison réelle d'inverser réellement la matrice A ou de la multiplier par sa transposition et de perdre un peu de précision, afin de résoudre les équations.

Quant à votre question, pour une simple inclinaison des degrés thêta autour de (x0, y0), les coefficients que vous recherchez sont:

def find_rotation_coeffs(theta, x0, y0):
    ct = cos(theta)
    st = sin(theta)
    return np.array([ct, -st, x0*(1-ct) + y0*st, st, ct, y0*(1-ct)-x0*st,0,0])

Et en général, toute transformation Affine doit avoir (g, h) égal à zéro. J'espère que cela pourra aider!

4
Amir