Геометрическое преобразование изображения в Numpy и OpenCV

глубокое обучение

вводить

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

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

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

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

Типы аффинных преобразований

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

х' = топор

где A — матрица 2x3 или 3x3 в однородной системе координат, а x — вектор вида (x, y) или (x, y, 1) в однородной системе координат. Эта формула говорит, что A отображает любой вектор x в другой вектор x'.

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

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

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

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

A = array([[cos(angle),  -sin(angle), tx],
            [sin(angle), cos(angle),  ty],
            [0,          0,           1]])

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

В Python и OpenCV начало координат 2D-матрицы находится в левом верхнем углу, начиная с x, y = (0, 0). Система координат левосторонняя, ось X направлена ​​вправо, а ось Y направлена ​​прямо вниз.

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

Общие преобразования в евклидовом пространстве

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

Используя приведенные выше знания, следующий код можно использовать для преобразования точек в (0,0), (0,1), (1,0), (1,1). Кроме того, Python предоставляет полезный сокращенный оператор @ для представления матричного умножения.

# 点生成器
def get_grid(x, y, homogenous=False):
    coords = np.indices((x, y)).reshape(2, -1)
    return np.vstack((coords, np.ones(coords.shape[1]))) if 
homogenous else coords
# 定义变换
def get_rotation(angle):
    angle = np.radians(angle)
    return np.array([
        [np.cos(angle), -np.sin(angle), 0],
        [np.sin(angle),  np.cos(angle), 0],
        [0, 0, 1]
    ])
def get_translation(tx, ty):
    return np.array([
        [1, 0, tx],
        [0, 1, ty],
        [0, 0, 1]
    ])
def get_scale(s):
    return np.array([
        [s, 0, 0],
        [0, s, 0],
        [0, 0, 1]
    ])

R1 = get_rotation(135)
T1 = get_translation(-2, 2)
S1 = get_scale(2)
# 应用变换x'=Ax
coords_rot = R1 @ coords
coords_trans = T1 @ coords
coords_scale = S1 @ coords
coords_composite1 = R1 @ T1 @ coords
coords_composite2 = T1 @ R1 @ coords

Важно отметить, что, за некоторыми исключениями, матрицы обычно не обмениваются. который

A1 @ A2 != A2 @ A1

Поэтому для преобразования

# 平移然后旋转
coords_composite1 = R1 @ T1 @ coords
# 旋转然后平移
coords_composite2 = T1 @ R1 @ coords

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

Преобразования в Numpy

Теперь о картинках, есть несколько замечаний. Во-первых, как упоминалось ранее, мы должны изменить масштаб вертикальной оси. Во-вторых, преобразованные точки должны быть спроецированы на плоскость изображения.

По сути, шаги, которые необходимо предпринять, следующие:

  1. Создайте новое изображение I'(x,y) для выходного преобразования
  2. применить трансформацию
  3. Проецирование точек на новую плоскость изображения с учетом только тех точек, которые лежат в пределах границ изображения.

Пример. Вращение, масштабирование и панорамирование вокруг центра изображения.

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

Этого можно достичь, применяя следующую составную матрицу.

height, width = image.shape[:2]
tx, ty = np.array((width // 2, height // 2))
angle = np.radians(45)
scale = 2.0
R = np.array([
    [np.cos(angle), np.sin(angle), 0],
    [-np.sin(angle), np.cos(angle), 0],
    [0, 0, 1]
])
T = np.array([
    [1, 0, tx],
    [0, 1, ty],
    [0, 0, 1]
])
S = np.array([
    [scale, 0, 0],
    [0, scale, 0],
    [0, 0, 1]
])
A = T @ R @ S @ np.linalg.inv(T)

применить к изображению

# 表示图像坐标的网格
coords = get_grid(width, height, True)
x_ori, y_ori = coords[0], coords[1] 
# 应用变换
warp_coords = np.round(A@coords).astype(np.int)
xcoord2, ycoord2 = warp_coords[0, :], warp_coords[1, :]
# 获取图像边界内的像素
indices = np.where((xcoord >= 0) & (xcoord < width) &
                   (ycoord >= 0) & (ycoord < height))
xpix2, ypix2 = xcoord2[indices], ycoord2[indices]
xpix, ypix = x_ori[indices], y_ori[indices]
# 将像素RGB数据映射到另一个数组中的新位置
canvas = np.zeros_like(image)
canvas[ypix, xpix] = image[yy, xx]

В двух приведенных выше фрагментах кода следует отметить несколько моментов.

  1. Вращение левосторонней системы координат достигается заменой символов.
  2. Так как точка вращается вокруг начала координат, мы сначала переводим центр в начало координат, затем поворачиваем и масштабируем
  3. Затем преобразуйте точки обратно в плоскость изображения.
  4. Круговое преобразование указывает на целые числа для представления дискретных значений пикселей.
  5. Далее мы рассматриваем только пиксели, лежащие в границах изображения.
  6. Отобразите соответствующие I(x, y) и I'(x, y).

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

Обратная деформация

Другой способ предотвратить вышеперечисленное — представить деформацию как передискретизацию исходного изображения I(x,y) для заданной точки деформации x'. Этого можно добиться, умножив X' на величину, обратную A. Здесь важно отметить, что преобразование должно быть обратимым.

  1. Примените обратное преобразование к X'.
X = np.linalg.inv(A) @ X'

Примечание. Для изображений обратное искривление X' - это просто перепроекция I'(X, y) на I(X, y). Итак, мы просто обратно преобразуем координаты пикселя I'(x,y), как показано ниже.

  1. определить его положение в исходной плоскости изображения
  2. Передискретизируйте пиксели RGB для I (x, y) и сопоставьте их обратно с I' (x, y)
# 设置像素坐标I'(x,y)
coords = get_grid(width, height, True)
x2, y2 = coords[0], coords[1]
# 应用逆变换并舍入(最近邻插值)
warp_coords = (Ainv@coords).astype(np.int)
x1, y1 = warp_coords[0, :], warp_coords[1, :]
# 获取图像边界内的像素
indices = np.where((x1 >= 0) & (x1 < width) &
                   (y1 >= 0) & (y1 < height))
xpix1, ypix1 = x2[indices], y2[indices]
xpix2, ypix2 = x1[indices], y1[indices]
# 映射对应的像素
canvas = np.zeros_like(image)
canvas[ypix1, xpix1] = image[ypix2,xpix2]
coords = get_grid(width, height, True)
x2, y2 = coords[0], coords[1]

Выполнение приведенного выше кода должно дать плотное непористое изображение.

Преобразования в OpenCV

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

Есть несколько способов сделать это. Возможный способ заключается в том, что вы можете сами написать аффинное преобразование и вызвать cv2.warfaffine(image, A, output_shape)

В приведенном ниже коде показана вся аффинная матрица, и он даст тот же результат, что и выше. Хорошее упражнение — вывести формулу самостоятельно!

def get_affine_cv(t, r, s):
    sin_theta = np.sin(r)
    cos_theta = np.cos(r)
    
    a_11 = s * cos_theta
    a_21 = -s * sin_theta
    
    a_12 = s * sin_theta
    a_22 = s * cos_theta
        
    a_13 = t[0] * (1 - s * cos_theta) - s * sin_theta * t[1]
    a_23 = t[1] * (1 - s * cos_theta) + s * sin_theta * t[0]
    return np.array([[a_11, a_12, a_13],
                 [a_21, a_22, a_23]])

A2 = get_affine_cv((tx, ty), angle, scale)
warped = cv2.warpAffine(image, A2, (width, height))

Другой способ - полагаться на OpenCV для возврата матрицы аффинного преобразования с использованием cv2.getRotationMatrix2D (центр, угол, масштаб). Эта функция поворачивает изображение вокруг центра точки, используя угол, и масштабирует изображение, используя масштаб.

A3 = cv2.getRotationMatrix2D((tx, ty), np.rad2deg(angle), scale)
warped = cv2.warpAffine(image, b3, (width, height), 
flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, 
borderValue=0)

Суммировать

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

Сводная станция блога о технологиях искусственного интеллекта Panchuang: http://docs.panchuang.net/PyTorch, официальная китайская учебная станция: http://pytorch.panchuang.net/OpenCV, официальный китайский документ: http://woshicver.com/