Как работает преобразование перспективы в PIL?

Image.transform PIL имеет режим перспективы, для которого требуется 8-кортеж данных, но я не могу понять, как преобразовать, скажем, правый наклон в 30 градусов в этот кортеж.

Кто-нибудь может это объяснить?


person Hedge    schedule 05.01.2013    source источник
comment
Знакомы ли вы с уравнениями, используемыми в преобразовании перспективы? См. xenia.media.mit.edu/~cwren/interpolator.   -  person mmgp    schedule 06.01.2013


Ответы (4)


Чтобы применить перспективное преобразование, вам сначала нужно знать четыре точки на плоскости A, которые будут сопоставлены с четырьмя точками на плоскости B. С помощью этих точек вы можете получить гомографическое преобразование. Делая это, вы получаете свои 8 коэффициентов, и трансформация может иметь место.

Сайт http://xenia.media.mit.edu/~cwren/interpolator/ (зеркало: веб-архив) , а также во многих других текстах описывается, как можно определить эти коэффициенты. Чтобы упростить задачу, вот прямая реализация по указанной ссылке:

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 — четыре вершины текущей плоскости, а pa — четыре вершины результирующей плоскости.

Итак, предположим, мы преобразуем изображение следующим образом:

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])

Вот пример ввода и вывода с кодом выше:

введите здесь описание изображениявведите здесь описание изображения

Мы можем продолжить последний код и выполнить перспективное преобразование, чтобы вернуть сдвиг:

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])

В результате чего:

введите здесь описание изображения

Вы также можете повеселиться с пунктами назначения:

введите здесь описание изображениявведите здесь описание изображения

person mmgp    schedule 06.01.2013
comment
Ваш ответ очень полезен и ясен. Спасибо. Знаете ли вы о какой-либо реализации def find_coeffs(pa, pb) на чистом питоне? Я надеюсь избежать добавления зависимости numpy для нецентральной части моей системы. Я думаю, я могу решить это сам, но я надеюсь, что это уже где-то там. - person KobeJohn; 04.12.2013
comment
Возможно, для вашего конкретного проекта @kobejohn уже слишком поздно, но я только что опубликовал новый ответ, в котором есть решение на чистом Python для генерации коэффициентов. - person Karim Bahgat; 24.09.2014
comment
@mmgp Ссылка, которую вы указали в своем ответе, теперь не работает, что дает 403. - person bcdan; 03.08.2015
comment
Как удалить черный цвет, появляющийся в открытом пространстве вокруг изображения? Я не могу сказать, должен ли я попытаться обрезать новое изображение или есть простой способ сделать черные части прозрачными. Я хотел бы вставить преобразованное изображение поверх другого изображения, и у меня не может быть лишнего черного по бокам. - person Hartley Brody; 22.11.2015
comment
Ответил на мой собственный вопрос о вставке преобразованного изображения в другое изображение, при этом «прозрачные» изображения не отображаются черными. Ознакомьтесь с этим вопросом: stackoverflow.com/questions/5324647/ Соответствующий бит был background.paste(foreground, (0, 0), foreground), необходимо передать вставленное изображение как первый и третий параметр, установить его как маску - person Hartley Brody; 22.11.2015
comment
Как выглядят коэффициенты для второго с последнего изображения? - person Yuriy Chernyshov; 16.09.2017
comment
Как узнать, что это за магические числа и чем их заменить в моем случае (256)? Как вы думаете, вы могли бы опубликовать использование функции find_coeffs с именами переменных, такими как original_left_corner_y и new_left_corner_y и т. д.? Спасибо! - person Aaron Esau; 21.12.2018

Я собираюсь захватить этот вопрос немного, потому что это единственная вещь в Google, относящаяся к перспективным преобразованиям в Python. Вот немного более общий код, основанный на приведенном выше, который создает матрицу преобразования перспективы и генерирует функцию, которая будет запускать это преобразование в произвольных точках:

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
person David Wolever    schedule 06.06.2014

8 коэффициентов преобразования (a, b, c, d, e, f, g, h) соответствуют следующему преобразованию:

x' = (ax + by + c) / (gx + hy + 1)
y' = (dx + ey + f) / (gx + hy + 1)

Эти 8 коэффициентов обычно можно найти из решения 8 (линейных) уравнений, которые определяют, как 4 точки на плоскости преобразуются (4 точки в 2D -> 8 уравнений), см. ответ mmgp для кода, который решает это, хотя вы могли бы найти более точным изменить строку

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

to

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

то есть нет реальной причины инвертировать матрицу A или умножать ее на ее транспонирование и терять немного точности, чтобы решить уравнения.

Что касается вашего вопроса, то для простого наклона тета-градусов вокруг (x0, y0) искомые коэффициенты:

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])

И вообще любое аффинное преобразование должно иметь (g, h) равным нулю. Надеюсь, это поможет!

person Amir    schedule 01.06.2017

Вот чистая версия Python генерации коэффициентов преобразования (поскольку я видел, что это запрашивалось несколькими). Я сделал и использовал его для создания PyDraw пакета рисования изображений на чистом Python.

Если вы используете его для своего собственного проекта, обратите внимание, что для вычислений требуется несколько расширенных матричных операций, а это означает, что для этой функции требуется другая, к счастью, чисто Python, матричная библиотека под названием matfunc, изначально написанная Рэймондом Хеттингером и которую вы можете скачать здесь или здесь.

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
person Karim Bahgat    schedule 23.09.2014
comment
Это красиво! Спасибо, что оставили мне сообщение. Я закончил свое приложение, выяснив, как самостоятельно вычислить только базовые аффинные коэффициенты, но я могу вернуться к нему в будущем и использовать это для более сложных преобразований. - person KobeJohn; 25.09.2014