Отображение сшитых изображений вместе без обрезки с помощью warpAffine

Я пытаюсь сшить 2 изображения вместе, используя сопоставление шаблонов. Найдите 3 набора точек, которые я передаю cv2.getAffineTransform(), чтобы получить матрицу деформации, которую я передаю cv2.warpAffine(), чтобы выровнять мои изображения.

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

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

Это полученный мной окончательный результат. Вот исходные изображения (1, 2) и код, который я использую:

РЕДАКТИРОВАТЬ: вот результаты переменной trans

array([[  1.00768049e+00,  -3.76690353e-17,  -3.13824885e+00],
       [  4.84461775e-03,   1.30769231e+00,   9.61912797e+02]])

А вот очки, переданные cv2.getAffineTransform: unified_pair1

array([[  671.,  1024.],
       [   15.,   979.],
       [   15.,   962.]], dtype=float32)

unified_pair2

array([[ 669.,   45.],
       [  18.,   13.],
       [  18.,    0.]], dtype=float32)

import cv2
import numpy as np


def showimage(image, name="No name given"):
    cv2.imshow(name, image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    return

image_a = cv2.imread('image_a.png')
image_b = cv2.imread('image_b.png')


def get_roi(image):
    roi = cv2.selectROI(image) # spacebar to confirm selection
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    crop = image_a[int(roi[1]):int(roi[1]+roi[3]), int(roi[0]):int(roi[0]+roi[2])]
    return crop
temp_1 = get_roi(image_a)
temp_2 = get_roi(image_a)
temp_3 = get_roi(image_a)

def find_template(template, search_image_a, search_image_b):
    ccnorm_im_a = cv2.matchTemplate(search_image_a, template, cv2.TM_CCORR_NORMED)
    template_loc_a = np.where(ccnorm_im_a == ccnorm_im_a.max())

    ccnorm_im_b = cv2.matchTemplate(search_image_b, template, cv2.TM_CCORR_NORMED)
    template_loc_b = np.where(ccnorm_im_b == ccnorm_im_b.max())
    return template_loc_a, template_loc_b


coord_a1, coord_b1 = find_template(temp_1, image_a, image_b)
coord_a2, coord_b2 = find_template(temp_2, image_a, image_b)
coord_a3, coord_b3 = find_template(temp_3, image_a, image_b)

def unnest_list(coords_list):
    coords_list = [a[0] for a in coords_list]
    return coords_list

coord_a1 = unnest_list(coord_a1)
coord_b1 = unnest_list(coord_b1)
coord_a2 = unnest_list(coord_a2)
coord_b2 = unnest_list(coord_b2)
coord_a3 = unnest_list(coord_a3)
coord_b3 = unnest_list(coord_b3)

def unify_coords(coords1,coords2,coords3):
    unified = []
    unified.extend([coords1, coords2, coords3])
    return unified

# Create a 2 lists containing 3 pairs of coordinates
unified_pair1 = unify_coords(coord_a1, coord_a2, coord_a3)
unified_pair2 = unify_coords(coord_b1, coord_b2, coord_b3)

# Convert elements of lists to numpy arrays with data type float32
unified_pair1 = np.asarray(unified_pair1, dtype=np.float32)
unified_pair2 = np.asarray(unified_pair2, dtype=np.float32)

# Get result of the affine transformation
trans = cv2.getAffineTransform(unified_pair1, unified_pair2)

# Apply the affine transformation to original image
result = cv2.warpAffine(image_a, trans, (image_a.shape[1] + image_b.shape[1], image_a.shape[0]))
result[0:image_b.shape[0], image_b.shape[1]:] = image_b

showimage(result)
cv2.imwrite('result.png', result)

Источники: подход, основанный на рекомендациях, полученных здесь, это руководство и этот пример из документации.


person Jason    schedule 09.06.2017    source источник
comment
Отличная работа по реализации решений из последнего вопроса! В качестве предложения, чтобы людям не приходилось просеивать весь ваш код, вы можете включить преобразование, которое вы создали, и отображать только код, пытающийся получить это преобразование, чтобы получить правильные координаты. Хотя я не знаю, нужно ли это, так как скоро я смогу получить вам ответ. Основная проблема заключается в том, что в учебнике предполагается, что вы идете слева направо - на самом деле он не знает, на сколько пикселей нужно сместить ширину. Однако вы действительно можете это вычислить.   -  person alkasm    schedule 09.06.2017
comment
@AlexanderReynolds Спасибо за ваш комментарий, я добавил, какие переменные использовались для создания warpMatrix, в исходный вопрос, а также матрицу преобразования trans. Я предположил (возможно, ошибочно), что матрица преобразования, сгенерированная cv2.getAffineTransformation, также сместит изображение. Вы знаете, как я могу рассчитать сдвиг?   -  person Jason    schedule 09.06.2017
comment
Да. Пишу ответ сейчас. :)   -  person alkasm    schedule 09.06.2017


Ответы (1)


12 июля Редактировать:

Этот пост вдохновил репозиторий GitHub, предоставляющий функции для выполнения этой задачи; один для набивного warpAffine() и другой для набивного warpPerspective(). Разветвите версию Python или Версия C ++.


Преобразования сдвигают расположение пикселей

Любое преобразование берет координаты вашей точки (x, y) и сопоставляет их с новыми местоположениями (x', y'):

s*x'    h1 h2 h3     x
s*y' =  h4 h5 h6  *  y
s       h7 h8  1     1

где s - некоторый коэффициент масштабирования. Вы должны разделить новые координаты на коэффициент масштабирования, чтобы вернуть правильное расположение пикселей (x', y'). Технически это верно только для гомографий ---_ 8_ матриц преобразований --- вам не нужно масштабировать для аффинных преобразований (вам даже не нужно использовать однородные координаты ... но лучше оставить это обсуждение в целом) .

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

Предположим, ваша матрица преобразования просто сдвигает пиксели влево на десять пикселей. Переводом занимается последний столбец; первая строка - это перевод в x, а вторая строка - это перевод в y. Таким образом, у нас будет единичная матрица, но с -10 в первой строке третьего столбца. Где бы отображался пиксель (0,0)? Надеюсь, (-10,0) если логика имеет смысл. На самом деле это так:

transf = np.array([[1.,0.,-10.],[0.,1.,0.],[0.,0.,1.]])
homg_pt = np.array([0,0,1])
new_homg_pt = transf.dot(homg_pt))
new_homg_pt /= new_homg_pt[2]
# new_homg_pt = [-10.  0.  1.]

Идеально! Итак, мы можем выяснить, где отображаются все точки, с помощью небольшой линейной алгебры. Нам нужно будет получить все (x,y) точек и поместить их в огромный массив, чтобы каждая точка находилась в своем собственном столбце. Давайте представим, что наше изображение только 4x4.

h, w = src.shape[:2] # 4, 4
indY, indX = np.indices((h,w))  # similar to meshgrid/mgrid
lin_homg_pts = np.stack((indX.ravel(), indY.ravel(), np.ones(indY.size)))

Эти lin_homg_pts теперь имеют все однородные точки:

[[ 0.  1.  2.  3.  0.  1.  2.  3.  0.  1.  2.  3.  0.  1.  2.  3.]
 [ 0.  0.  0.  0.  1.  1.  1.  1.  2.  2.  2.  2.  3.  3.  3.  3.]
 [ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.]]

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

trans_lin_homg_pts = transf.dot(lin_homg_pts)
trans_lin_homg_pts /= trans_lin_homg_pts[2,:]

И теперь у нас есть преобразованные точки:

[[-10. -9. -8. -7. -10. -9. -8. -7. -10. -9. -8. -7. -10. -9. -8. -7.]
 [  0.  0.  0.  0.   1.  1.  1.  1.   2.  2.  2.  2.   3.  3.  3.  3.]
 [  1.  1.  1.  1.   1.  1.  1.  1.   1.  1.  1.  1.   1.  1.  1.  1.]]

Как мы видим, все работает как положено: мы сместили только x-значения на -10.

Пиксели могут быть смещены за пределы вашего изображения

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

theta = 45*np.pi/180
transf = np.array([
    [ np.cos(theta),np.sin(theta),0],
    [-np.sin(theta),np.cos(theta),0],
    [0.,0.,1.]])
print(transf)
trans_lin_homg_pts = transf.dot(lin_homg_pts)
minX = np.min(trans_lin_homg_pts[0,:])
minY = np.min(trans_lin_homg_pts[1,:])
maxX = np.max(trans_lin_homg_pts[0,:])
maxY = np.max(trans_lin_homg_pts[1,:])
# minX: 0.0, minY: -2.12132034356, maxX: 4.24264068712, maxY: 2.12132034356,

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

Заполнение целевого изображения

Обратите внимание, что когда вы звоните cv2.warpAffine(), вы должны ввести размер адресата. Эти преобразованные значения пикселей ссылаются на этот размер. Таким образом, если пиксель сопоставлен с (-10,0), он не будет отображаться в конечном изображении. Это означает, что нам придется создать еще одну гомографию с переводами, которые сдвигают все пиксели, будут положительными, а затем мы можем дополнить матрицу изображения, чтобы компенсировать наш сдвиг. Нам также нужно будет добавить исходное изображение снизу и справа, если гомография также перемещает точки на позиции больше, чем изображение.

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

pad_sz = list(src.shape) # in case three channel
pad_sz[0] = np.round(np.maximum(pad_sz[0], maxY) - np.minimum(0, minY)).astype(int)
pad_sz[1] = np.round(np.maximum(pad_sz[1], maxX) - np.minimum(0, minX)).astype(int)
dst_pad = np.zeros(pad_sz, dtype=np.uint8)
# pad_sz = [6, 4, 3]

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

Добавьте перевод к преобразованию, чтобы сдвинуть положение всех пикселей в положительное

Теперь нам нужно создать новую матрицу гомографии, чтобы преобразовать искаженное изображение на ту же величину, на которую мы сместились. И для применения обоих преобразований - исходного и нового сдвига - мы должны скомпоновать две омографии (для аффинного преобразования вы можете просто добавить перевод, но не для омографии) . Кроме того, нам нужно разделить на последнюю запись, чтобы убедиться, что шкала по-прежнему правильная (опять же, только для омографий):

anchorX, anchorY = 0, 0
transl_transf = np.eye(3,3)
if minX < 0: 
    anchorX = np.round(-minX).astype(int)
    transl_transf[0,2] -= anchorX
if minY < 0:
    anchorY = np.round(-minY).astype(int)
    transl_transf[1,2] -= anchorY
new_transf = transl_transf.dot(transf)
new_transf /= new_transf[2,2]

Я также создал здесь точки привязки, в которые мы поместим целевое изображение в матрицу с дополнениями; он сдвинут на ту же величину, на которую гомография сдвинет изображение. Итак, давайте поместим изображение назначения внутри матрицы с дополнениями:

dst_pad[anchorY:anchorY+dst_sz[0], anchorX:anchorX+dst_sz[1]] = dst

Деформация с новым преобразованием в заполненное изображение

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

warped = cv2.warpPerspective(src, new_transf, (pad_sz[1],pad_sz[0]))

alpha = 0.3
beta = 1 - alpha
blended = cv2.addWeighted(warped, alpha, dst_pad, beta, 1.0)

Собираем все вместе

Давайте создадим для этого функцию, поскольку в конце мы создавали довольно много переменных, которые нам не нужны. Для входных данных нам нужны исходное изображение, целевое изображение и исходная гомография. А для выходных данных нам просто нужно заполненное целевое изображение и деформированное изображение. Обратите внимание, что в примерах мы использовали 3x3 гомографию, поэтому лучше убедиться, что мы отправляем 3x3 преобразований вместо 2x3 аффинных или евклидовых искажений. Вы можете просто добавить строку [0,0,1] к любой аффинной деформации внизу, и все будет в порядке.

def warpPerspectivePadded(img, dst, transf):

    src_h, src_w = src.shape[:2]
    lin_homg_pts = np.array([[0, src_w, src_w, 0], [0, 0, src_h, src_h], [1, 1, 1, 1]])

    trans_lin_homg_pts = transf.dot(lin_homg_pts)
    trans_lin_homg_pts /= trans_lin_homg_pts[2,:]

    minX = np.min(trans_lin_homg_pts[0,:])
    minY = np.min(trans_lin_homg_pts[1,:])
    maxX = np.max(trans_lin_homg_pts[0,:])
    maxY = np.max(trans_lin_homg_pts[1,:])

    # calculate the needed padding and create a blank image to place dst within
    dst_sz = list(dst.shape)
    pad_sz = dst_sz.copy() # to get the same number of channels
    pad_sz[0] = np.round(np.maximum(dst_sz[0], maxY) - np.minimum(0, minY)).astype(int)
    pad_sz[1] = np.round(np.maximum(dst_sz[1], maxX) - np.minimum(0, minX)).astype(int)
    dst_pad = np.zeros(pad_sz, dtype=np.uint8)

    # add translation to the transformation matrix to shift to positive values
    anchorX, anchorY = 0, 0
    transl_transf = np.eye(3,3)
    if minX < 0: 
        anchorX = np.round(-minX).astype(int)
        transl_transf[0,2] += anchorX
    if minY < 0:
        anchorY = np.round(-minY).astype(int)
        transl_transf[1,2] += anchorY
    new_transf = transl_transf.dot(transf)
    new_transf /= new_transf[2,2]

    dst_pad[anchorY:anchorY+dst_sz[0], anchorX:anchorX+dst_sz[1]] = dst

    warped = cv2.warpPerspective(src, new_transf, (pad_sz[1],pad_sz[0]))

    return dst_pad, warped

Пример запуска функции

Наконец, мы можем вызвать эту функцию с некоторыми реальными изображениями и аналогами и посмотреть, что из этого получится. Я позаимствовал пример из LearnOpenCV:

src = cv2.imread('book2.jpg')
pts_src = np.array([[141, 131], [480, 159], [493, 630],[64, 601]], dtype=np.float32)
dst = cv2.imread('book1.jpg')
pts_dst = np.array([[318, 256],[534, 372],[316, 670],[73, 473]], dtype=np.float32)

transf = cv2.getPerspectiveTransform(pts_src, pts_dst)

dst_pad, warped = warpPerspectivePadded(src, dst, transf)

alpha = 0.5
beta = 1 - alpha
blended = cv2.addWeighted(warped, alpha, dst_pad, beta, 1.0)
cv2.imshow("Blended Warped Image", blended)
cv2.waitKey(0)

И в итоге мы получаем это искаженное изображение с мягкой подкладкой:

[Мягкие и деформированные [1]

в отличие от типичной обрезанной деформации, которую вы обычно получаете.

person alkasm    schedule 09.06.2017
comment
Ух ты, спасибо за такой исчерпывающий и хорошо организованный ответ. Я прорабатываю свой путь и останавливаюсь, чтобы прочитать об определенных концепциях. Я представлю свое окончательное решение, когда закончу. Еще раз спасибо! - person Jason; 10.06.2017
comment
Привет спасибо за это Должна ли эта функция работать для любого исходного, конечного изображения и правильной гомографии? Я могу воспроизвести ваши результаты с изображениями из книг, но не со своими собственными. Не могли бы вы дать мне совет? - person rich; 31.08.2019
comment
@wich да, единственная проблема должна заключаться в том, если ваша гомография отправляет один из углов в бесконечность (или просто взрывается больше, чем изображение), см. проблему, которую кто-то поднял на GitHub с этой проблемой здесь: github.com/alkasm/padded-transformations/issues/4. В противном случае открывайте новый выпуск с примерами. Желательно в новом репо, содержащем это: github.com/alkasm/cvtools/issues - person alkasm; 01.09.2019