Рубрики
Без рубрики

Аффинные преобразования изображений в Python с помощью Numpy, Pillow и OpenCV

Автор оригинала: Adam McQuistan.

В этой статье я опишу, что значит применить аффинное преобразование к изображению и как это сделать в Python. Сначала я продемонстрирую низкоуровневые операции в Numpy , чтобы дать подробную геометрическую реализацию. Затем я перейду к более практическому использованию библиотек Python Pillow и OpenCV .

Эта статья была написана с помощью блокнота Jupyter, а исходный код можно найти в моем репо GitHub так что, пожалуйста, не стесняйтесь клонировать/раскошеливаться и экспериментировать с кодом.

Что такое аффинное преобразование

Согласно Википедия аффинное преобразование-это функциональное отображение между двумя геометрическими (аффинными) пространствами, которые сохраняют точки, прямые и параллельные линии, а также отношения между точками. Вся эта математическая абстрактная формулировка сводится к слабо выраженному линейному преобразованию, которое приводит, по крайней мере в контексте обработки изображений, к одной или нескольким манипуляциям, таким как вращение, переворачивание, масштабирование или сдвиг, применяя матрицу преобразования.

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

Идентичность $$ \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$ $$ $$
Пересчет $$ \begin{bmatrix} c_{x} & 0 & 0 \\ 0 & c_{y} & 0 \\ 0 & 0 & 1 \end{bmatrix} $$ {х} * х$ $ {у} * у$$
Вращение* $$ \begin{bmatrix} cos \Theta & sin \Theta & 0 \\ -sin \Theta & cos \Theta & 0 \\ 0 & 0 & 1 \end{bmatrix} $$ * cos \Theta-у * без \Theta$$ * cos \Theta + у * без \ Theta$$
Перевод $$ \begin{bmatrix} 1 & 0 & t_{x} \\ 0 & 1 & t_{y} \\ 0 & 0 & 1 \end{bmatrix} $$ + t_{x}$ $ + t_{у}$$
Горизонтальный сдвиг $$ \begin{bmatrix} 1 & s_{h} & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$ + s_{v} * и$$ $$
Вертикальный сдвиг $$ \begin{bmatrix} 1 & 0 & 0 \\ s_{v} & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$ $ $ * s_{h} + и$$

* Аффинное преобразование использует угол поворота по часовой стрелке, который в отличие от типичного геометрического единичного круга углов измеряется при вращении против часовой стрелки с 0, начиная с положительной оси X, поэтому вы увидите, что отрицательный угол часто применяется.

' нотация здесь просто относится к преобразованной выходной координате x или y, а не к нотации исчисления для производной

Для простой демонстрации я применю пару преобразований для манипулирования координатами x и y следующих точек, которые имеют трехмерные компоненты индекса символов x, y и ascii, подобно тому, как пиксель изображения имеет 3-мерные компоненты x, y и частоты (или интенсивности).

a = (0, 1, 0) b = (1, 0, 1) c = (0, -1, 2) d = (-1, 0, 3)

Преобразования для этого примера будут масштабироваться на 2 во всех направлениях и поворачиваться на 90 градусов по часовой стрелке. Сначала я выполню преобразования по отдельности, чтобы показать прямое влияние каждого из них на перемещение точек вокруг, затем я объединю преобразования и применю их в одном действии.

Для начала я хочу построить массив Numpy (некоторые могут назвать это матрицей) с каждой строкой, представляющей точку, где первый столбец-это x, второй-y, а третий-индекс его буквы в наборе символов ascii, аналогичном таблице, показанной ниже. Затем я использую Matplotlib для построения точек (после применения неизменного преобразования идентичности), чтобы дать базовое представление о том, где мы находимся.

a 0 1 0
b 1 0 1
c 0 -1 2
d -1 0 3
import matplotlib.pyplot as plt
import numpy as np
import string

# points a, b and, c
a, b, c, d = (0, 1, 0), (1, 0, 1), (0, -1, 2), (-1, 0, 3)

# matrix with row vectors of points
A = np.array([a, b, c, d])

# 3x3 Identity transformation matrix
I = np.eye(3)
color_lut = 'rgbc'
fig = plt.figure()
ax = plt.gca()
xs = []
ys = []
for row in A:
    output_row = I @ row
    x, y, i = output_row
    xs.append(x)
    ys.append(y)
    i = int(i) # convert float to int for indexing
    c = color_lut[i]
    plt.scatter(x, y, color=c)
    plt.text(x + 0.15, y, f"{string.ascii_letters[i]}")
xs.append(xs[0])
ys.append(ys[0])
plt.plot(xs, ys, color="gray", linestyle='dotted')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()
png

Три точки a, b и c нанесены на сетку после применения к ним тождественного преобразования с помощью простого векторного матричного точечного произведения, оставляя их неизменными.

Теперь я перейду к созданию масштабирующей матрицы преобразования \(T_s\) , как показано ниже, которая масштабирует размещение точек во всех направлениях.

$$ T_s = \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$

Теперь я перейду к построению преобразованных точек аналогично тому, что было сделано с исходными точками, не измененными преобразованием идентичности, но на этот раз я применю матрицу масштабирования преобразования, определенную выше. Для лучшей визуализации я рисую пунктирную линию, соединяющую точки.

# create the scaling transformation matrix
T_s = np.array([[2, 0, 0], [0, 2, 0], [0, 0, 1]])

fig = plt.figure()
ax = plt.gca()
xs_s = []
ys_s = []
for row in A:
    output_row = T_s @ row
    x, y, i = row
    x_s, y_s, i_s = output_row
    xs_s.append(x_s)
    ys_s.append(y_s)
    i, i_s = int(i), int(i_s) # convert float to int for indexing
    c, c_s = color_lut[i], color_lut[i_s] # these are the same but, its good to be explicit
    plt.scatter(x, y, color=c)
    plt.scatter(x_s, y_s, color=c_s)
    plt.text(x + 0.15, y, f"{string.ascii_letters[int(i)]}")
    plt.text(x_s + 0.15, y_s, f"{string.ascii_letters[int(i_s)]}'")

xs_s.append(xs_s[0])
ys_s.append(ys_s[0])
plt.plot(xs, ys, color="gray", linestyle='dotted')
plt.plot(xs_s, ys_s, color="gray", linestyle='dotted')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()
png

Из приведенного выше графика должно быть очень ясно, что размеры x и y были просто увеличены в два раза, в то время как третье измерение, ответственное за индекс букв ASCII, было оставлено без изменений. На самом деле, те, кто знаком с матричными алгебрами, заметят, что для всех аффинных преобразований, перечисленных в первой таблице, значение, представленное в третьем измерении, всегда остается неизменным, как указано всеми нулями и одним единственным значением в индексе третьего измерения последнего столбца.

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

$$ грех $$

$$ cos $$

$$ T_r = \begin{bmatrix} 0 & 1 & 0 \\ -1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$

Теперь все, что мне нужно сделать, это применить ту же логику для преобразования и построения точек, вот так:

# create the rotation transformation matrix
T_r = np.array([[0, 1, 0], [-1, 0, 0], [0, 0, 1]])

fig = plt.figure()
ax = plt.gca()
for row in A:
    output_row = T_r @ row
    x_r, y_r, i_r = output_row
    i_r = int(i_r) # convert float to int for indexing
    c_r = color_lut[i_r] # these are the same but, its good to be explicit
    letter_r = string.ascii_letters[i_r]
    plt.scatter(x_r, y_r, color=c_r)
    plt.text(x_r + 0.15, y_r, f"{letter_r}'")

plt.plot(xs, ys, color="gray", linestyle='dotted')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()
png

Надеюсь, вы можете сказать из графика, что все точки были повернуты на 90 градусов вокруг оси вращения в начале координат.

Самое замечательное в том, что аффинные преобразования по существу являются линейными преобразованиями, заключается в том, что вы можете комбинировать преобразования и применять их в один шаг. Чтобы продемонстрировать это, я применю точечное произведение (матричное умножение) моих двух матриц преобразования, например:

$$ T_{comb} = \begin{bmatrix} 0 & 1 & 0 \\ -1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 0 & 2 & 0 \\ -2 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$

Теперь я могу применить эту комбинированную матрицу преобразования к точкам и перестроить их, чтобы показать комбинацию масштабирования на два и поворота на 90 градусов.

# create combined tranformation matrix
T = T_s @ T_r

fig = plt.figure()
ax = plt.gca()

xs_comb = []
ys_comb = []
for row in A:
    output_row = T @ row
    x, y, i = row
    x_comb, y_comb, i_comb = output_row
    xs_comb.append(x_comb)
    ys_comb.append(y_comb)
    i, i_comb = int(i), int(i_comb) # convert float to int for indexing
    c, c_comb = color_lut[i], color_lut[i_comb] # these are the same but, its good to be explicit
    letter, letter_comb = string.ascii_letters[i], string.ascii_letters[i_comb]
    plt.scatter(x, y, color=c)
    plt.scatter(x_comb, y_comb, color=c_comb)
    plt.text(x + 0.15 , y, f"{letter}")
    plt.text(x_comb + 0.15, y_comb, f"{letter_comb}'")
xs_comb.append(xs_comb[0])
ys_comb.append(ys_comb[0])
plt.plot(xs, ys, color="gray", linestyle='dotted')
plt.plot(xs_comb, ys_comb, color="gray", linestyle='dotted')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()
png

Работа с изображением

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

Это также позволяет мне осветить еще одну важную тему аффинных преобразований, которая имеет дело с третьим измерением. Третье измерение данных в изображении представляет фактическое значение пикселя или иногда называется областью интенсивности, тогда как физическое 2D-расположение пикселей в двух других измерениях называется пространственной областью.

Для начала я прочитаю и покажу изображение с помощью matplotlib, которое представляет собой просто большую заглавную букву R.

img = plt.imread('letterR.jpg')
img.shape #  (1000, 1000, 4)

Используя метод imread (...) , я могу прочитать изображение JPG, представляющее заглавную букву R, в numpy ndarray. Затем я показываю размеры массива, которые составляют 1000 строк на 1000 столбцов, вместе составляя 1 000 000 пикселей местоположения в пространственной области. Затем отдельные пиксельные данные представляются в виде массива из 4 целых чисел без знака, представляющих красный, зеленый, синий и альфа-канал (или образец), которые вместе обеспечивают данные интенсивности каждого пикселя.

plt.figure(figsize=(5, 5))
plt.imshow(img)
png

Затем я хотел бы применить предыдущий масштаб и поворот к пространственной области данных изображения, преобразовав таким образом расположение пикселей, аналогичное тому, что я продемонстрировал ранее с данными точек. Однако мне нужно использовать несколько иной подход, потому что данные изображения организованы иначе, чем строки точек данных, с которыми я работал ранее. С помощью данных изображения мне нужно сопоставить индексы для каждого пикселя входных данных с преобразованными выходными индексами, используя матрицу преобразования T, определенную ранее.

# 2x scaling requires a tranformation image array 2x the original image
img_transformed = np.empty((2000, 2000, 4), dtype=np.uint8)
for i, row in enumerate(img):
    for j, col in enumerate(row):
        pixel_data = img[i, j, :]
        input_coords = np.array([i, j, 1])
        i_out, j_out, _ = T @ input_coords
        img_transformed[i_out, j_out, :] = pixel_data

plt.figure(figsize=(5, 5))
plt.imshow(img_transformed)
png

Построение изображения после применения преобразования ясно показывает, что исходное изображение было повернуто на 90 градусов по часовой стрелке и увеличено в 2 раза. Однако теперь результат явно уменьшился, так как вы можете легко увидеть разрывы в интенсивности пикселей.

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

def plot_box(plt, x0, y0, txt, w=1, h=1):
    plt.scatter(x0, y0)
    plt.scatter(x0, y0 + h)
    plt.scatter(x0 + w, y0 + h)
    plt.scatter(x0 + w, y0)
    plt.plot([x0, x0, x0 + w, x0 + w, x0], [y0, y0 + h, y0 + h, y0, y0], color="gray", linestyle='dotted')
    plt.text(x0 + (.33 * w), y0 + (.5 * h), txt)

#             x0, y0, letter
a = np.array((0,  1,  0))
b = np.array((1,  1,  1))
c = np.array((0,  0,  2))
d = np.array((1,  0,  3))

A = np.array([a, b, c, d])
fig = plt.figure()
ax = plt.gca()
for pt in A:
    x0, y0, i = I @ pt
    x0, y0, i = int(x0), int(y0), int(i)
    plot_box(plt, x0, y0, f"{string.ascii_letters[int(i)]} ({x0}, {y0})")

ax.set_xticks(np.arange(-1, 5, 1))
ax.set_yticks(np.arange(-1, 5, 1))
plt.grid()
plt.show()
png

Теперь посмотрите, что происходит, когда я применяю преобразование масштабирования 2X, как показано ниже. Напомним, что:

$$ T_s = \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$

Вы заметите, что такое пространственное преобразование приводит к… ну, “пробелы”, говоря простыми словами, которые я сделал очевидными, построив вопросительные знаки вместе с координатами. Сетка 2х2 преобразуется в сетку 3х3, причем исходные квадраты перемещаются на основе примененного линейного преобразования. Это означает, что (0,0) * \(T_s\) остается (0,0) из-за его свойств как вектора 0, но все остальные масштабируются на два, например (1,1) * \(T_s\) – > (2,2).

fig = plt.figure()
ax = plt.gca()
for pt in A:
    xt, yt, i = T_s @ pt
    xt, yt, i = int(xt), int(yt), int(i)
    plot_box(plt, xt, yt, f"{string.ascii_letters[i]}' ({xt}, {yt})")

delta_w, delta_h = 0.33, 0.5
plt.text(0 + delta_w, 1 + delta_h, "? (0, 1)")
plt.text(1 + delta_w, 0 + delta_h, "? (1, 0)")
plt.text(1 + delta_w, 1 + delta_h, "? (1, 1)")
plt.text(1 + delta_w, 2 + delta_h, "? (1, 2)")
plt.text(2 + delta_w, 1 + delta_h, "? (2, 1)")

ax.set_xticks(np.arange(-1, 5, 1))
ax.set_yticks(np.arange(-1, 5, 1))
plt.grid()
plt.show()
png

Остается вопрос, что делать с теми пробелами, которые были введены? Интуитивной мыслью было бы просто посмотреть на исходное изображение в поисках ответа. Просто так получилось, что если мы применим обратное преобразование к координате на выходе, я получу соответствующее местоположение исходного входа.

В матричных операциях, таких как обратное отображение, это выглядит следующим образом:

$ $ (х, у,^{-1} * (х’ у’ 1) $$

где x’, y’ – координаты в приведенной выше преобразованной сетке 3×3, в частности отсутствующее местоположение, такое как (2, 1), \(T_s^{-1}\) (фактические значения показаны ниже) является обратной матрицей масштабирования 2x \(T_s\), а x, y-координаты, найденные в исходной сетке 2×2.

$$ T_s^{-1} = \begin{bmatrix} 1/2 & 0 & 0 \\ 0 & 1/2 & 0 \\ 0 & 0 & 1 \end{bmatrix}^{-1} $$

Однако вскоре вы поймете, что есть небольшая проблема, которую все еще нужно решить из-за того, что каждая из координат промежутка соответствует дробным значениям системы координат 2×2. В случае с данными изображения вы не можете действительно иметь долю пикселя. Это будет более ясно на примере отображения промежутка (2, 1) обратно в исходное пространство 2×2, например:

$$ T_s^{-1} * (2, 1, 1) = (1, 1/2, 1) $$

В этом случае я округлю/2 до 0 и скажу, что это соответствует (1, 0). В общем смысле этот метод выбора значения в исходной сетке 2х2 для помещения в промежутки преобразованной сетки 3х3 известен как интерполяция, и в этом конкретном примере я использую упрощенную версию метода интерполяции ближайшего соседа.

Хорошо, теперь вернемся к данным изображения. Должно быть достаточно ясно, что теперь нужно сделать, чтобы исправить эти пробелы в масштабированной и повернутой версии буквы R. Я должен разработать реализацию интерполяции ближайшего соседа, основанную на обратном отображении, используя обратную матрицу преобразования T, координат пикселей в преобразованном изображении, чтобы найти либо точное совпадение, либо ближайшего соседа в исходном изображении.

T_inv = np.linalg.inv(T)

# nearest neighbors interpolation
def nearest_neighbors(i, j, M, T_inv):
    x_max, y_max = M.shape[0] - 1, M.shape[1] - 1
    x, y, _ = T_inv @ np.array([i, j, 1])
    if np.floor(x) == x and np.floor(y) == y:
        x, y = int(x), int(y)
        return M[x, y]
    if np.abs(np.floor(x) - x) < np.abs(np.ceil(x) - x):
        x = int(np.floor(x))
    else:
        x = int(np.ceil(x))
    if np.abs(np.floor(y) - y) < np.abs(np.ceil(y) - y):
        y = int(np.floor(y))
    else:
        y = int(np.ceil(y))
    if x > x_max:
        x = x_max
    if y > y_max:
        y = y_max
    return M[x, y,]

img_nn = np.empty((2000, 2000, 4), dtype=np.uint8)
for i, row in enumerate(img_transformed):
    for j, col in enumerate(row):
        img_nn[i, j, :] = nearest_neighbors(i, j, img, T_inv)

plt.figure(figsize=(5, 5))
plt.imshow(img_nn)
png

Не слишком потрепанный, правда?

Я должен отметить, что в большинстве случаев метод ближайшего соседа будет недостаточен. Есть два других более распространенных метода интерполяции, известных как билинейная и бикубическая интерполяция, которые обычно дают гораздо лучшие результаты. Я подробнее расскажу об этих других алгоритмах интерполяции при введении библиотек Pillow и OpenCV в последних разделах. Цель этого раздела-просто построить интуитивное понимание того, как все работает.

Аффинные преобразования с подушкой

В этом разделе я кратко расскажу о том, как использовать превосходную библиотеку обработки изображений Python Pillow для выполнения аффинных преобразований.

Во-первых, нужно будет установить подушку. Для этого я использовал пипа, вот так:

$ pip install pillow

Теперь первым шагом является импорт класса Image из модуля PIL (PIL-это имя модуля Python, связанного с Pillow) и чтение в моем образе.

from PIL import Image

Чтобы прочитать в образце имя файла изображения “letterR.jpg” Я вызываю метод класса Image.open(...) , передавая ему имя файла, которое возвращает экземпляр класса Image , который я затем преобразую в массив numpy и отображаю с помощью matplotlib.

img = Image.open('letterR.jpg')
plt.figure(figsize=(5, 5))
plt.imshow(np.asarray(img))
png

Подушка Класс Image имеет удобный метод под названием transform (...) , который позволяет выполнять мелкозернистые аффинные преобразования, но есть несколько странностей, которые я должен сначала обсудить, прежде чем перейти к его демонстрации. Метод transform(...) начинается с двух обязательных параметров, представляющих size в виде кортежа высоты и ширины, за которыми следует метод применяемого преобразования, которое будет Image.АФФИННЫЙ в данном случае.

Остальные параметры являются необязательными аргументами ключевых слов, которые управляют тем, как должно выполняться преобразование. В этом примере я буду использовать параметр data , который принимает первые две строки аффинной матрицы преобразования.

Например, матрица преобразования масштабирования 2x, с которой я работал, обрезанная только до первых двух строк, выглядит следующим образом:

$$ T_s = \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \end{bmatrix} $$

Последний параметр , который я буду использовать с методом transform (...) , – это resample , который используется для указания типа алгоритма интерполяции пикселей, применяемого из возможных вариантов Изображения.БЛИЖАЙШИЙ (ближайший сосед), Изображение.БИЛИНЕЙНЫЙ , или Образ.БИКУБИЧЕСКИЙ . Этот выбор часто будет варьироваться в зависимости от применяемого преобразования. Однако билинейный и бикубический обычно дают лучшие результаты, чем ближайший сосед, но, как уже было показано в этом примере, ближайший сосед работает довольно хорошо.

Есть несколько особенностей, которые послужили для меня настоящими готчами, когда я впервые использовал метод Image.transform (...) , особенно вокруг построения аффинной матрицы преобразования со странно усеченной последней строкой. Таким образом, я хотел бы потратить некоторое время на то, чтобы понять, почему вещи работают так, как они работают, потому что это своего рода процесс.

Первое, что должно произойти, – это изображение должно быть переведено так, чтобы начало координат (0, 0) находилось в середине изображения. В случае изображения 1000 x 1000 буквы R в этом примере это означает перевод -500 в x и y.

Ниже я показываю общую матрицу преобразования перевода \(T_{translate}\) и ту, которую я буду использовать в примере \(T_{need 500}\).

$$ T_{translate} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} $$

$$ $$ T_{neg 500} = \begin{bmatrix} 1 & 0 & -500 \ 0 & 1 & -500 \ 0 & 0 & 1 \end{bmatrix} $$

Затем есть матрицы 2X scaling \(T_{scale}\) и 90-градусного поворота \(T_{rotation}\) из предыдущих. Однако библиотека подушек на самом деле решила использовать стандартные геометрические углы (то есть против часовой стрелки), а не вращения по часовой стрелке, которые я описал ранее, поэтому знаки на функциях sin переворачиваются. Ниже приведены результирующие индивидуальные матрицы преобразований.

$$ T_{rotate} = \begin{bmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$

$$ T_{scale} = \begin{bmatrix} 2 & 0 & 0 \ 0 & 2 & 0 \ 0 & 0 & 1 \end{bmatrix} $$

Затем необходимо применить другую матрицу перевода, которая действует для изменения пространственной области пикселей, по существу отрицая первую, которая центрировала начало координат. В этом случае мне нужен положительный перевод 1000 в x и y, где 1000 происходит от удвоенного оригинала, потому что он был увеличен на два.

$$ $$ T_{pos 1000} = \begin{bmatrix} 1 & 0 & 1000 \\ 0 & 1 & 1000 \\ 0 & 0 & 1 \end{bmatrix} $$

Они представляют собой отдельные необходимые шаги преобразования, поэтому все, что остается, – это умножить матрицы по порядку (то есть справа налево), вот так:

$$ {$$ {post 1000} * T_{rotate} * T_{scale} * T_{neg 500} $$

Итак, на самом деле есть еще одна последняя странность. Метод Image.transform(...) фактически требует, чтобы обратная матрица преобразования была передана параметру data в виде сплющенного массива (или кортежа), исключающего последнюю строку.

$$ ^{-1} $$

В коде все это работает следующим образом:

# recenter resultant image
T_pos1000 = np.array([
    [1, 0, 1000],
    [0, 1, 1000],
    [0, 0, 1]])
# rotate - opposite angle
T_rotate = np.array([
    [0, -1, 0],
    [1, 0, 0],
    [0, 0, 1]])
# scale
T_scale = np.array([
    [2, 0, 0],
    [0, 2, 0],
    [0, 0, 1]])
# center original to 0,0
T_neg500 = np.array([
    [1, 0, -500],
    [0, 1, -500],
    [0, 0, 1]])
T = T_pos1000 @ T_rotate @ T_scale @ T_neg500
T_inv = np.linalg.inv(T)
img_transformed = img.transform((2000, 2000), Image.AFFINE, data=T_inv.flatten()[:6], resample=Image.NEAREST)
plt.imshow(np.asarray(img_transformed))
png

Аффинные преобразования с OpenCV2

Продолжая, я хотел бы кратко описать, как выполнять эти аффинные преобразования с помощью популярной библиотеки обработки изображений и компьютерного зрения OpenCV. Я использую здесь слово “краткое”, потому что оно в значительной степени совпадает с тем, что требуется в предыдущей демонстрации с использованием подушки.

Перво-наперво, вы должны установить вот так:

$ pip install opencv-python

Как я уже упоминал выше, существует значительное совпадение в методологии между подходом Pillow и использованием OpenCV. Например, вы по-прежнему создаете матрицу преобразования, которая сначала центрирует массив пикселей в начало координат, и используете только первые две строки матрицы преобразования. Основное различие заключается в том, что с OpenCV вы даете ему стандартную матрицу, а не обратную.

Итак, с изложенным пониманием я перейду к коду, начиная с импорта модуля opencv-python, который называется cv2 .

import cv2

Чтение изображения так же просто, как вызов метода cv2.imread (...) , передавая имя файла в качестве аргумента. Это возвращает данные изображения в виде 3D-массива numpy, аналогичного тому, как работает matplotlib, но пиксельные данные в 3-м измерении состоят из массива каналов в порядке синий, зеленый, красный вместо красный, зеленый, синий, альфа, как это было в случае чтения с matplotlib.

Таким образом, чтобы построить данные изображения numpy, исходящие из библиотеки OpenCV, необходимо изменить порядок каналов пикселей. К счастью, OpenCV предоставляет метод convience cvtColor (...) , который можно использовать для этого, как показано ниже (хотя пуристы numpy, скорее всего, знают, что img[:,:,::-1] сделаю то же самое).

img = cv2.imread('letterR.jpg')
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
png

Несколько последних пунктов, которые следует упомянуть, заключаются в том, что OpenCV требует, чтобы данные в матрице преобразования были типа 32 bit float, а не 64 bit float по умолчанию, поэтому обязательно преобразуйте их в 32 bit с помощью numpy.float32(...) . Кроме того, API to cv2.warpAffine(...) не предоставляет возможности указать, какой тип алгоритма интерполяции пикселей применять, и я не смог определить из документов, что используется. Если вы знаете или узнаете, пожалуйста, напишите в комментариях ниже.

T_opencv = np.float32(T.flatten()[:6].reshape(2,3))
img_transformed = cv2.warpAffine(img, T_opencv, (2000, 2000))
plt.imshow(cv2.cvtColor(img_transformed, cv2.COLOR_BGR2RGB))
png

Вывод

В этой статье я рассказал о том, что такое аффинное преобразование и как его можно применить к обработке изображений с помощью Python. Pure numpy и matplotlib были использованы для того, чтобы дать низкоуровневое интуитивное описание того, как работают аффинные преобразования. В заключение я продемонстрировал, как то же самое можно сделать с помощью двух популярных библиотек Python Pillow и OpenCV.

Спасибо за чтение и, как всегда, не стесняйтесь комментировать или критиковать ниже.

Ресурсы