У меня есть исходное (src
) изображение (я), которое я хочу выровнять по целевому (dst
) изображению с помощью аффинного преобразования, сохраняя при этом полный экстент обоих изображений во время выравнивания (даже неперекрывающиеся области).
Я уже могу рассчитать матрицу поворота и смещения аффинного преобразования, которую я передаю в scipy.ndimage.interpolate.affine_transform
для восстановления dst
-выровненного src
изображения.
Проблема в том, что, когда изображения не перекрываются полностью, результирующее изображение обрезается только до общей площади двух изображений. Что мне нужно, так это полный экстент обоих изображений, размещенных в одной и той же системе координат пикселей. Этот вопрос почти дублирует Вот этот, и отличный ответ и репозиторий предоставляют эту функциональность для преобразований OpenCV. К сожалению, мне это нужно для реализации scipy
.
Слишком поздно, после того, как я неоднократно натыкался на кирпичную стену, пытаясь перевести ответ на приведенный выше вопрос в scipy
, я наткнулся на Эта проблема, а затем последовал на этот вопрос. Последний вопрос действительно дал некоторое представление о чудесном мире аффинной трансформации scipy
, но я пока не смог раскрыть свои конкретные потребности.
Преобразования из src
в dst
могут иметь переводы и повороты. Я могу заставить работать только переводы (пример показан ниже) и я могу заставить работать только вращения (в основном взламывая приведенное ниже и черпая вдохновение из использования аргумента reshape
в scipy.ndimage.interpolation.rotate
). Тем не менее, я полностью теряюсь, объединяя два. Я попытался рассчитать, что должно быть правильным offset
(см. ответы на этот вопрос еще раз), но я не могу заставить его работать во всех сценариях.
Рабочий пример аффинного преобразования с дополнением только для перевода, который в значительной степени следует это репо, описанному в этот ответ:
from scipy.ndimage import rotate, affine_transform
import numpy as np
import matplotlib.pyplot as plt
nblob = 50
shape = (200, 100)
buffered_shape = (300, 200) # buffer for rotation and translation
def affine_test(angle=0, translate=(0, 0)):
np.random.seed(42)
# Maxiumum translation allowed is half difference between shape and buffered_shape
# Generate a buffered_shape-sized base image with random blobs
base = np.zeros(buffered_shape, dtype=np.float32)
random_locs = np.random.choice(np.arange(2, buffered_shape[0] - 2), nblob * 2, replace=False)
i = random_locs[:nblob]
j = random_locs[nblob:]
for k, (_i, _j) in enumerate(zip(i, j)):
# Use different values, just to make it easier to distinguish blobs
base[_i - 2 : _i + 2, _j - 2 : _j + 2] = k + 10
# Impose a rotation and translation on source
src = rotate(base, angle, reshape=False, order=1, mode = "constant")
bsc = (np.array(buffered_shape) / 2).astype(int)
sc = (np.array(shape) / 2).astype(int)
src = src[
bsc[0] - sc[0] + translate[0] : bsc[0] + sc[0] + translate[0],
bsc[1] - sc[1] + translate[1] : bsc[1] + sc[1] + translate[1],
]
# Cut-out destination from the centre of the base image
dst = base[bsc[0] - sc[0] : bsc[0] + sc[0], bsc[1] - sc[1] : bsc[1] + sc[1]]
src_y, src_x = src.shape
def get_matrix_offset(centre, angle, scale):
"""Follows OpenCV.getRotationMatrix2D"""
angle = angle * np.pi / 180
alpha = scale * np.cos(angle)
beta = scale * np.sin(angle)
return (
np.array([[alpha, beta], [-beta, alpha]]),
np.array(
[
(1 - alpha) * centre[0] - beta * centre[1],
beta * centre[0] + (1 - alpha) * centre[1],
]
),
)
# Obtain the rotation matrix and offset that describes the transformation
# between src and dst
matrix, offset = get_matrix_offset(np.array([src_y / 2, src_x / 2]), angle, 1)
offset = offset - translate
# Determine the outer bounds of the new image
lin_pts = np.array([[0, src_x, src_x, 0], [0, 0, src_y, src_y]])
transf_lin_pts = np.dot(matrix.T, lin_pts) - offset[::-1].reshape(2, 1)
# Find min and max bounds of the transformed image
min_x = np.floor(np.min(transf_lin_pts[0])).astype(int)
min_y = np.floor(np.min(transf_lin_pts[1])).astype(int)
max_x = np.ceil(np.max(transf_lin_pts[0])).astype(int)
max_y = np.ceil(np.max(transf_lin_pts[1])).astype(int)
# Add translation to the transformation matrix to shift to positive values
anchor_x, anchor_y = 0, 0
if min_x < 0:
anchor_x = -min_x
if min_y < 0:
anchor_y = -min_y
shifted_offset = offset - np.dot(matrix, [anchor_y, anchor_x])
# Create padded destination image
dst_h, dst_w = dst.shape[:2]
pad_widths = [anchor_y, max(max_y, dst_h) - dst_h, anchor_x, max(max_x, dst_w) - dst_w]
dst_padded = np.pad(
dst,
((pad_widths[0], pad_widths[1]), (pad_widths[2], pad_widths[3])),
"constant",
constant_values=-1,
)
dst_pad_h, dst_pad_w = dst_padded.shape
# Create the aligned and padded source image
source_aligned = affine_transform(
src,
matrix.T,
offset=shifted_offset,
output_shape=(dst_pad_h, dst_pad_w),
order=3,
mode = "constant",
cval=-1,
)
# Plot the images
fig, axes = plt.subplots(1, 4, figsize=(10, 5), sharex=True, sharey=True)
axes[0].imshow(src, cmap = "viridis", vmin=-1, vmax=nblob)
axes[0].set_title("Source")
axes[1].imshow(dst, cmap = "viridis", vmin=-1, vmax=nblob)
axes[1].set_title("Dest")
axes[2].imshow(source_aligned, cmap = "viridis", vmin=-1, vmax=nblob)
axes[2].set_title("Source aligned to Dest padded")
axes[3].imshow(dst_padded, cmap = "viridis", vmin=-1, vmax=nblob)
axes[3].set_title("Dest padded")
plt.show()
например.:
affine_test(0, (-20, 40))
дает:
С увеличением, показывающим выровненные в дополненных изображениях:
Мне нужен полный экстент изображений src
и dst
, выровненных по одним и тем же пиксельным координатам, с поворотами и переводами.
Любая помощь приветствуется сильно!
Функция affine_test
выше создаст src
и dst
, применив поворот (angle
) и перевод (translation
) на основе переданных аргументов.
Для меня ваши изображения немного сбивают с толку, так как я не вижу выравнивания. Не могли бы вы рассказать о них немного, чтобы я мог найти ответ?
На изображениях «Источник» — это переведенная версия «Пункт назначения»: если вы переместите «Источник» на -20 пикселей по оси y и на +40 пикселей по оси x, вы получите «Пункт назначения». Дополненные версии могут выглядеть не выровненными, потому что глаз обманывает, но они идеально выровнены, и они - принципиально - были дополнены значениями -1, так что каждое соответствующее изображение сохраняет свой полный начальный след и отпечаток другого изображения. В этом смысле, если я наложу их друг на друга, я получу изображение, покрывающее объединение их исходного следа. Нижний ряд — это просто увеличение того же ряда изображений.
Если у вас есть два похожих (или одинаковых) изображения и вы хотите их выровнять, вы можете сделать это, используя обе функции rotate и shift :
from scipy.ndimage import rotate, shift
Вам нужно найди сначала разность углов между двумя изображениями angle_to_rotate
, при этом вы применить вращение в src:
angle_to_rotate = 25
rotated_src = rotate(src, angle_to_rotate , reshape=True, order=1, mode = "constant")
С помощью reshape=True
вы избегаете потери информации из исходной матрицы src и дополняет результат, чтобы изображение можно было перевести вокруг индексов 0,0. Вы можете рассчитать этот перевод как есть (x*cos(angle),y*sin(angle)
, где x и y — размеры изображения, но, вероятно, это не будет иметь значения.
Теперь вам нужно перевести изображение в источник, для этого вы можете использовать функция сдвига:
rot_translated_src = shift(rotated_src , [distance_x, distance_y])
В этом случае нет изменения формы (потому что иначе у вас не было бы реального перевода), поэтому, если изображение не было предварительно дополнено, некоторая информация будет потеряна.
Но вы можете сделать некоторые дополнения с помощью
np.pad(src, number, mode='constant')
Чтобы рассчитать distance_x
и distance_y
, вам нужно будет найти точку, которая служит вам ориентиром между rotated_src
и пунктом назначения, а затем просто вычислить расстояние по осям x и y.
Резюме
src
и dst
src
с scipy.ndimage.rotate с использованием reshape=Truedistance_x, distance_y
между повернутым изображением и dstКод
from scipy.ndimage import rotate, shift
import matplotlib.pyplot as plt
import numpy as np
Сначала мы делаем целевое изображение:
# make and plot dest
dst = np.ones([40,20])
dst = np.pad(dst,10)
dst[17,[14,24]]=4
dst[27,14:25]=4
dst[26,[14,25]]=4
rotated_dst = rotate(dst, 20, order=1)
plt.imshow(dst) # plot it
plt.imshow(rotated_dst)
plt.show()
Делаем Исходный образ:
# make_src image and plot it
src = np.zeros([40,20])
src = np.pad(src,10)
src[0:20,0:20]=1
src[7,[4,14]]=4
src[17,4:15]=4
src[16,[4,15]]=4
plt.imshow(src)
plt.show()
Затем мы выравниваем src по месту назначения:
rotated_src = rotate(src, 20, order=1) # find the angle 20, reshape true is by default
plt.imshow(rotated_src)
plt.show()
distance_y = 8 # find this distances from rotated_src and dst
distance_x = 12 # use any visual reference or even the corners
translated_src = shift(rotated_src, [distance_y,distance_x])
plt.imshow(translated_src)
plt.show()
пд: Если вы обнаружите проблемы с нахождением угла и расстояний программным способом, оставьте комментарий, предоставляющий немного больше информации о том, что можно использовать в качестве эталона, например, кадр изображения или некоторые функции изображения. / данные)
Задача состоит в том, чтобы определить три параметра
Предположим, что у вас есть сетка для углов, смещений по осям x и y, каждое из которых имеет размер O(n)
, и что ваши изображения имеют размер O(n x n)
, поэтому вращение, перемещение и сравнение изображений занимают O(n^2)
, поскольку у вас есть O(n^3)
преобразования-кандидаты, чтобы попробовать , вы получаете сложность O(n^5)
, и, вероятно, поэтому вы задаете вопрос.
Однако часть смещения можно вычислить несколько более эффективно, если вычислить максимальную корреляцию с использованием преобразований Фурье. Преобразования Фурье могут быть выполнены со сложностью O(n log n)
по каждой оси, и мы должны выполнить их для двух пространственных измерений, полная корреляционная матрица может быть вычислена в O(n^2 log^2 n)
, затем мы найдем максимум со сложностью O(n^2)
, поэтому общая временная сложность определения лучший расклад O(n^2 log^2 n)
. Однако вы все равно хотите найти лучший ракурс, так как у нас есть O(n)
ракурсы-кандидаты, общая сложность этого поиска будет O(n^3 log^2 n)
. Помните, что мы используем Python, и у нас могут быть значительные накладные расходы, поэтому эта сложность дает нам только представление о том, насколько это будет сложно, и я справлялся с такими проблемами раньше, поэтому я начинаю уверенно.
Я начну с загрузки изображения и применения поворота и центрирования изображения нулями.
def centralized(a, width, height):
'''
Image centralized to the given width and height
by padding with zeros (black)
'''
assert width >= a.shape[0] and height >= a.shape[1]
ap = np.zeros((width, height) + a.shape[2:], a.dtype)
ccx = (width - a.shape[0])//2
ccy = (height - a.shape[1])//2
ap[ccx:ccx+a.shape[0], ccy:ccy+a.shape[1], ...] = a
return ap
def image_pair(im, width, height, displacement=(0,0), angle=0):
'''
this build an a pair of images as numpy arrays
from the input image.
Both images will be padded with zeros (black)
and roughly centralized.
and will have the specified shape
make sure that the width and height chosen are enough
to fit the rotated image
'''
a = np.array(im)
a1 = centralized(a, width, height)
a2 = centralized(ndimage.rotate(a, angle), width, height)
a2 = np.roll(a2, displacement, axis=(0,1))
return a1, a2
def random_transform():
angle = np.random.rand() * 360
displacement = np.random.randint(-100, 100, 2)
return displacement, angle
a1, a2 = image_pair(im, 512, 512, *random_transform())
plt.subplot(121)
plt.imshow(a1)
plt.subplot(122)
plt.imshow(a2)
Первое, что нужно сделать, это вычислить корреляцию изображения
def compute_correlation(a1, a2):
A1 = np.fft.rfftn(a1, axes=(0,1))
A2 = np.fft.rfftn(a2, axes=(0,1))
C = np.fft.irfftn(np.sum(A1 * np.conj(A2), axis=2))
return C
Затем давайте создадим пример без поворота и подтвердим, что с индексом максимальной корреляции мы можем найти смещение, которое соответствует одному изображению другому.
displacement, _ = random_transform()
a1, a2 = image_pair(im, 521, 512, displacement, angle=0)
C = compute_correlation(a1, a2)
np.unravel_index(np.argmax(C), C.shape), displacement
a3 = np.roll(a2, np.unravel_index(np.argmax(C), C.shape), axis=(0,1))
assert np.all(a3 == a1)
С вращением или интерполяцией этот результат может быть неточным, но он дает смещение, которое даст нам самое близкое возможное выравнивание.
Давайте поместим это в функцию для будущего использования
def get_aligned(a1, a2, angle):
a1_rotated = ndimage.rotate(a1, angle, reshape=False)
C = compute_correlation(a2, a1_rotated)
found_displacement = np.unravel_index(np.argmax(C), C.shape)
a1_aligned = np.roll(a1_rotated, found_displacement, axis=(0,1))
return a1_aligned
Теперь мы можем сделать что-то в два шага,
в одном мы вычисляем корреляцию для каждого угла, затем с углом, который дает максимальную корреляцию, находим выравнивание.
displacement, angle = random_transform()
a1, a2 = image_pair(im, 521, 512, displacement, angle)
C_max = []
C_argmax = []
angle_guesses = np.arange(0, 360, 5)
for angle_guess in angle_guesses:
a1_rotated = ndimage.rotate(a1, angle_guess, reshape=False)
C = compute_correlation(a1_rotated, a2)
i = np.argmax(C)
v = C.reshape(-1)[i]
C_max.append(v)
C_argmax.append(i)
Посмотрим, как будет выглядеть корреляция
plt.plot(angle_guesses, C_max);
У нас есть явный победитель, глядя на эту кривую, даже если у подсолнуха есть какая-то вращательная симметрия.
Давайте применим преобразование к исходному изображению и посмотрим, как оно выглядит.
a1_aligned = get_aligned(a1, a2, angle_guesses[np.argmax(C_max)])
plt.subplot(121)
plt.imshow(a2)
plt.subplot(122)
plt.imshow(a1_aligned)
Отлично, я бы не сделал лучше, чем это вручную.
Я использую изображение подсолнуха из соображений красоты, но процедура одинакова для любого типа изображения. Я использую RGB, показывая, что изображение может иметь одно дополнительное измерение, то есть оно использует вектор признаков, вместо скалярного признака, вы можете изменить форму своих данных на (width, height, 1)
, если ваш признак является скалярным.
Хорошо, я думаю, мой вопрос не так ясен, как я думал. Я уже есть преобразование между двумя моими изображениями. Я знаю это очень точно - на самом деле это не одно и то же выравниваемое изображение, и они очень большие, поэтому ffts становятся дорогими и неточными. Моя единственная забота — сохранить контур моих соответствующих изображений, когда я применяю преобразование, чтобы поместить их в одну и ту же пиксельную систему координат. Ваш пример здесь сделал это, просто искусственно дополнив изображения большим количеством. Это может быть вариант, но вопрос правильно применяется [1/2]
affine_transform остается, учитывая взаимодействие между смещением и матрицей поворота. Кроме того, изображения, как уже упоминалось, очень большие, и заполнение их на величину, покрывающую наибольшее потенциальное преобразование, вероятно, невозможно [2/2].
Не могли бы вы более четко определить, что вы подразумеваете под сохранением посадочного места и как вы выполняете преобразование, которое не сохраняет посадочное место?
Возьмем пример вопроса. Если я не попытаюсь учесть преобразование, при использовании scipy.ndimage.affine_transform
на исходном (src
) изображении мне будет возвращено изображение формы (100 200), где единственная сохранившаяся часть изображения — это та, которая перекрывается с местом назначения (dst
) . Мне нужно сохранить все части источника, независимо от того, пересекаются ли они с пунктом назначения или нет (и наоборот)
Итак, вам нужно преобразование, которое сохраняет перекос и вращение, но может переводить изображение так, чтобы оно перемещало все в целевую область изображения?
Типа, да. Мне нужно изменить преобразование и дополнить изображение dst
, чтобы преобразованное src
и дополненное dst
находились в одних и тех же пиксельных координатах. Ответ и репозиторий реализации openCV, связанные с вопросом, показывают, как это можно сделать, но, как показывает проблема scipy
, также связанная, scipy
, похоже, делает некоторые странные вещи с преобразованием, так что метод, который работает для аффинного преобразования OpenCV , не работает.
Но заполнение может только увеличить координаты пикселей. Предположим, что координаты пикселей для выровненного dst
больше, чем координаты пикселей для src
, как вы ожидаете, что координаты пикселей будут выровнены? Также похоже, что вас интересуют точки, не можете передать массив с координатами точек и найти преобразование для их выравнивания? Тогда вы работаете только с координатами и нет тяжелых манипуляций с изображениями.
Рабочий код ниже на случай, если кому-то еще понадобятся аффинные преобразования scipy
:
def affine_test(angle=0, translate=(0, 0), shape=(200, 100), buffered_shape=(300, 200), nblob=50):
# Maxiumum translation allowed is half difference between shape and buffered_shape
np.random.seed(42)
# Generate a buffered_shape-sized base image
base = np.zeros(buffered_shape, dtype=np.float32)
random_locs = np.random.choice(np.arange(2, buffered_shape[0] - 2), nblob * 2, replace=False)
i = random_locs[:nblob]
j = random_locs[nblob:]
for k, (_i, _j) in enumerate(zip(i, j)):
base[_i - 2 : _i + 2, _j - 2 : _j + 2] = k + 10
# Impose a rotation and translation on source
src = rotate(base, angle, reshape=False, order=1, mode = "constant")
bsc = (np.array(buffered_shape) / 2).astype(int)
sc = (np.array(shape) / 2).astype(int)
src = src[
bsc[0] - sc[0] + translate[0] : bsc[0] + sc[0] + translate[0],
bsc[1] - sc[1] + translate[1] : bsc[1] + sc[1] + translate[1],
]
# Cut-out destination from the centre of the base image
dst = base[bsc[0] - sc[0] : bsc[0] + sc[0], bsc[1] - sc[1] : bsc[1] + sc[1]]
src_y, src_x = src.shape
def get_matrix_offset(centre, angle, scale):
"""Follows OpenCV.getRotationMatrix2D"""
angle_rad = angle * np.pi / 180
alpha = np.round(scale * np.cos(angle_rad), 8)
beta = np.round(scale * np.sin(angle_rad), 8)
return (
np.array([[alpha, beta], [-beta, alpha]]),
np.array(
[
(1 - alpha) * centre[0] - beta * centre[1],
beta * centre[0] + (1 - alpha) * centre[1],
]
),
)
matrix, offset = get_matrix_offset(np.array([((src_y - 1) / 2) - translate[0], ((src_x - 1) / 2) - translate[
1]]), angle, 1)
offset += np.array(translate)
M = np.column_stack((matrix, offset))
M = np.vstack((M, [0, 0, 1]))
iM = np.linalg.inv(M)
imatrix = iM[:2, :2]
ioffset = iM[:2, 2]
# Determine the outer bounds of the new image
lin_pts = np.array([[0, src_y-1, src_y-1, 0], [0, 0, src_x-1, src_x-1]])
transf_lin_pts = np.dot(matrix, lin_pts) + offset.reshape(2, 1) # - np.array(translate).reshape(2, 1) # both?
# Find min and max bounds of the transformed image
min_x = np.floor(np.min(transf_lin_pts[1])).astype(int)
min_y = np.floor(np.min(transf_lin_pts[0])).astype(int)
max_x = np.ceil(np.max(transf_lin_pts[1])).astype(int)
max_y = np.ceil(np.max(transf_lin_pts[0])).astype(int)
# Add translation to the transformation matrix to shift to positive values
anchor_x, anchor_y = 0, 0
if min_x < 0:
anchor_x = -min_x
if min_y < 0:
anchor_y = -min_y
dot_anchor = np.dot(imatrix, [anchor_y, anchor_x])
shifted_offset = ioffset - dot_anchor
# Create padded destination image
dst_y, dst_x = dst.shape[:2]
pad_widths = [anchor_y, max(max_y, dst_y) - dst_y, anchor_x, max(max_x, dst_x) - dst_x]
dst_padded = np.pad(
dst,
((pad_widths[0], pad_widths[1]), (pad_widths[2], pad_widths[3])),
"constant",
constant_values=-10,
)
dst_pad_y, dst_pad_x = dst_padded.shape
# Create the aligned and padded source image
source_aligned = affine_transform(
src,
imatrix,
offset=shifted_offset,
output_shape=(dst_pad_y, dst_pad_x),
order=3,
mode = "constant",
cval=-10,
)
Например. Бег:
affine_test(angle=-25, translate=(10, -40))
покажет:
и увеличил:
Извините, код не очень хорошо написан как есть.
Обратите внимание, что, запуская это в дикой природе, я заметил, что он не может обрабатывать какие-либо изменения масштаба изображений, но я не уверен, что это не связано с тем, как я вычисляю преобразование, поэтому предостережение, которое стоит отметить и проверить, если вы выравниваете изображения с разными масштабами.
Вам следует подумать о присуждении награды за другой ответ, потому что она не может перейти к задавшему вопрос, даже если он ответит), так что репутация не будет потрачена впустую.
@richardec Нет, репутация не будет «напрасной». Репутация, потраченная на вознаграждение, предназначена для привлечения большего внимания к посту, и именно это и было сделано.
@Jdog Как хорошо, не каждый день пользователь изо всех сил старается ответить на свой вопрос на благо будущих читателей! Кстати, можешь исправить отступ первой функции? Спасибо :)
Пожалуйста, опубликуйте образцы входных srcs и dsts, чтобы мы могли с ними работать.