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]](https://i.stack.imgur.com/0jKcp.jpg)
в отличие от типичной обрезанной деформации, которую вы обычно получаете.
person
alkasm
schedule
09.06.2017
trans
. Я предположил (возможно, ошибочно), что матрица преобразования, сгенерированнаяcv2.getAffineTransformation
, также сместит изображение. Вы знаете, как я могу рассчитать сдвиг? - person Jason   schedule 09.06.2017