Автор оригинала: 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()
Три точки 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()
Из приведенного выше графика должно быть очень ясно, что размеры 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()
Надеюсь, вы можете сказать из графика, что все точки были повернуты на 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()
Работа с изображением
К настоящему времени я надеюсь, что мне удалось создать некоторую интуицию о том, как аффинные преобразования используются для простого перемещения точек в двумерном пространстве, поэтому с этим я хотел бы начать работать с некоторыми реальными данными изображения, чтобы дать более конкретную демонстрацию того, как все это работает.
Это также позволяет мне осветить еще одну важную тему аффинных преобразований, которая имеет дело с третьим измерением. Третье измерение данных в изображении представляет фактическое значение пикселя или иногда называется областью интенсивности, тогда как физическое 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)
Затем я хотел бы применить предыдущий масштаб и поворот к пространственной области данных изображения, преобразовав таким образом расположение пикселей, аналогичное тому, что я продемонстрировал ранее с данными точек. Однако мне нужно использовать несколько иной подход, потому что данные изображения организованы иначе, чем строки точек данных, с которыми я работал ранее. С помощью данных изображения мне нужно сопоставить индексы для каждого пикселя входных данных с преобразованными выходными индексами, используя матрицу преобразования 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)
Построение изображения после применения преобразования ясно показывает, что исходное изображение было повернуто на 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()
Теперь посмотрите, что происходит, когда я применяю преобразование масштабирования 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()
Остается вопрос, что делать с теми пробелами, которые были введены? Интуитивной мыслью было бы просто посмотреть на исходное изображение в поисках ответа. Просто так получилось, что если мы применим обратное преобразование к координате на выходе, я получу соответствующее местоположение исходного входа.
В матричных операциях, таких как обратное отображение, это выглядит следующим образом:
$ $ (х, у,^{-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)
Не слишком потрепанный, правда?
Я должен отметить, что в большинстве случаев метод ближайшего соседа будет недостаточен. Есть два других более распространенных метода интерполяции, известных как билинейная и бикубическая интерполяция, которые обычно дают гораздо лучшие результаты. Я подробнее расскажу об этих других алгоритмах интерполяции при введении библиотек 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))
Подушка Класс 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))
Аффинные преобразования с 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))
Несколько последних пунктов, которые следует упомянуть, заключаются в том, что 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))
Вывод
В этой статье я рассказал о том, что такое аффинное преобразование и как его можно применить к обработке изображений с помощью Python. Pure numpy и matplotlib были использованы для того, чтобы дать низкоуровневое интуитивное описание того, как работают аффинные преобразования. В заключение я продемонстрировал, как то же самое можно сделать с помощью двух популярных библиотек Python Pillow и OpenCV.
Спасибо за чтение и, как всегда, не стесняйтесь комментировать или критиковать ниже.