Математика и код искажения изображения

Геометрическое преобразование широко распространено в компьютерном зрении

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

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

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

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

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

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

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

x’ = Ax

where A = [[a_11, a_12, a_13],
           [a_21, a_22, a_23],
           [  0 ,   0 ,   1 ]]

представляет собой матрицу 2x3 или 3x3 в однородной координате, а x - вектор формы [x, y] или [x, y, 1] в однородной координате. В приведенной выше формуле сказано, что A принимает любой вектор x и сопоставляет его с другим вектором x ’.

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

Чтобы сузить круг обсуждения, есть несколько специализированных форм буквы 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 положительно направлена ​​вниз.

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

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

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

Используя то, что мы узнали выше, следующий код ниже можно использовать для преобразования точек [0, 0], [0, 1], [1, 0], [1,1]. Синие точки на рисунке 3.

Python предоставляет удобный сокращенный оператор @ для представления умножения матриц.

# Points generator
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
# Define Transformations
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)
# Apply transformation 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

Следовательно, для преобразований

# Translation and then rotation
coords_composite1 = R1 @ T1 @ coords
# Rotation and then translation
coords_composite2 = T1 @ R1 @ coords

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

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

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

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

  1. Создайте новое изображение I ’(x, y) для вывода точек преобразования.
  2. Примените преобразование A
  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)

Применение к изображению

# Grid to represent image coordinate
coords = get_grid(width, height, True)
x_ori, y_ori = coords[0], coords[1] 
# Apply transformation
warp_coords = np.round(A@coords).astype(np.int)
xcoord2, ycoord2 = warp_coords[0, :], warp_coords[1, :]
# Get pixels within image boundary
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]
# Map the pixel RGB data to new location in another array
canvas = np.zeros_like(image)
canvas[ypix, xpix] = image[yy, xx]

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

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

Как видите, из-за шага 4 полученное изображение (рис. 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), как вы увидите ниже.

2. Определите, где он находится в плоскости исходного изображения.

3. Выполните повторную выборку пикселей RGB из I (x, y) и отобразите их обратно в I ’(x, y).

код

# set up pixel coordinate I'(x, y)
coords = get_grid(width, height, True)
x2, y2 = coords[0], coords[1]
# Apply inverse transform and round it (nearest neighbour interpolation)
warp_coords = (Ainv@coords).astype(np.int)
x1, y1 = warp_coords[0, :], warp_coords[1, :]
# Get pixels within image boundaries
indices = np.where((x1 >= 0) & (x1 < width) &
                   (y1 >= 0) & (y1 < height))
xpix1, ypix1 = x2[indices], y2[indices]
xpix2, ypix2 = x1[indices], y1[indices]
# Map Correspondence
canvas = np.zeros_like(image)
canvas[ypix1, xpix1] = image[ypix2,xpix2]

Выполнение приведенного выше кода должно дать вам плотное изображение без дыр :) Не стесняйтесь загрузить код и поэкспериментировать с параметрами, чтобы применить другие преобразования.

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

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

Есть несколько способов сделать это.

  1. Напишите аффинное преобразование самостоятельно и вызовите cv2.warpAffine(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))

2. Положитесь на OpenCV, чтобы вернуть матрицу аффинного преобразования, используя cv2.getRotationMatrix2D(center, angle, scale).

Эта функция поворачивает изображение вокруг точки центр с углом и масштабирует его с помощью шкалы

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)

Резюме

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

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

Спасибо за прочтение! И я надеюсь, что вы лучше понимаете, как формулы записываются и используются в библиотеках. Подпишитесь, чтобы увидеть больше сообщений о компьютерном зрении и машинном обучении :) Обязательно сообщите в комментариях, если заметите какую-либо ошибку или что-то непонятное!

Еще статьи