Математика и код искажения изображения
Геометрическое преобразование широко распространено в компьютерном зрении
Геометрическое преобразование - важный метод обработки изображений, имеющий широкое применение. Например, простой вариант использования в компьютерной графике - простое масштабирование графического содержимого при его отображении на настольном компьютере по сравнению с мобильным.
Его также можно применять для проективного искажения изображения на другую плоскость изображения. Например, вместо того, чтобы смотреть на сцену прямо перед собой, мы хотим взглянуть на нее с другой точки зрения, для этого в этом сценарии применяется преобразование перспективы.
Еще одно интересное приложение - обучение глубоких нейронных сетей. Для обучения глубокой модели требуется огромное количество данных. И почти во всех случаях модели выигрывают от более высокой производительности обобщения по мере увеличения обучающих данных. Один из способов искусственно сгенерировать больше данных - случайным образом применить аффинное преобразование к входным данным. Техника, также известная как увеличение.
В этой статье я хотел бы рассказать вам о некоторых преобразованиях и о том, как мы можем выполнить их в 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
Что касается изображений, то есть несколько вещей, на которые следует обратить внимание. Во-первых, как упоминалось ранее, мы должны перестроить вертикальную ось. Во-вторых, преобразованные точки необходимо спроецировать на плоскость изображения.
По сути, необходимо предпринять следующие шаги:
- Создайте новое изображение I ’(x, y) для вывода точек преобразования.
- Примените преобразование A
- Спроецируйте точки на новую плоскость изображения, учитывая только те, которые лежат в границах изображения.
Пример: поворот, масштабирование и перенос относительно центра изображения
Давайте посмотрим на преобразование, при котором мы хотим увеличить в 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]
Несколько замечаний в двух приведенных выше фрагментах кода.
- Левый поворот системы координат учитывается заменой знака.
- Поскольку точки вращаются относительно начала координат, мы сначала переводим центр в начало координат, прежде чем выполнять поворот и масштабирование.
- Затем точки переводятся обратно в плоскость изображения.
- Точки преобразования округляются до целых чисел для представления дискретного значения пикселя.
- Далее мы рассматриваем только те пиксели, которые находятся в границах изображения.
- Соответствие карты I (x, y) и I ’(x, y)
Как видите, из-за шага 4 полученное изображение (рис. 4) будет иметь несколько неровностей и дырок. Чтобы устранить это, библиотеки с открытым исходным кодом используют методы интерполяции для заполнения пробелов после преобразования.
Обратное искривление
Другой подход к предотвращению наложения спектров состоит в том, чтобы сформулировать искажение как преобразование исходного изображения I (x, y) с учетом деформированных точек X ’. Это можно сделать, умножив X ’на обратное к A. В качестве предостережения, преобразование должно быть обратимым.
- Примените обратное преобразование к 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 очень просто.
Есть несколько способов сделать это.
- Напишите аффинное преобразование самостоятельно и вызовите
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.
Спасибо за прочтение! И я надеюсь, что вы лучше понимаете, как формулы записываются и используются в библиотеках. Подпишитесь, чтобы увидеть больше сообщений о компьютерном зрении и машинном обучении :) Обязательно сообщите в комментариях, если заметите какую-либо ошибку или что-то непонятное!
Еще статьи