使用warpAffine将拼接图像显示在一起而不会中断

Jas*_*son 3 opencv image-stitching

我试图通过使用模板匹配将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]])
Run Code Online (Sandbox Code Playgroud)

以下是传递给我们的观点cv2.getAffineTransform:unified_pair1

array([[  671.,  1024.],
       [   15.,   979.],
       [   15.,   962.]], dtype=float32)
Run Code Online (Sandbox Code Playgroud)

unified_pair2

array([[ 669.,   45.],
       [  18.,   13.],
       [  18.,    0.]], dtype=float32)
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)

来源:基于此处收到的建议的方法,本教程和文档中的此示例.

alk*_*asm 13

7月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
Run Code Online (Sandbox Code Playgroud)

哪里s是一些缩放因子.您必须将新坐标除以比例因子以返回正确的像素位置(x', y').从技术上讲,这只适用于单应性 - (3, 3)转换矩阵 - 你不需要为仿射变换进行缩放(你甚至不需要使用齐次坐标......但最好保持这种讨论的一般性).

然后将实际像素值移动到那些新位置,并且插值颜色值以适合新的像素网格.因此,在此过程中,这些新位置会在某个时刻被记录下来.我们需要这些位置来查看像素实际移动到的位置,相对于另一个图像.让我们从一个简单的例子开始,看看点的映射位置.

假设您的变换矩阵只是将像素向左移动十个像素.翻译由最后一栏处理; 第一行是翻译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.]
Run Code Online (Sandbox Code Playgroud)

完善!因此,我们可以找出所有点用一点线性代数映射的位置.我们需要获得所有的(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)))
Run Code Online (Sandbox Code Playgroud)

这些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.]]
Run Code Online (Sandbox Code Playgroud)

然后我们可以进行矩阵乘法来得到每个点的映射值.为简单起见,让我们坚持以前的单应性.

trans_lin_homg_pts = transf.dot(lin_homg_pts)
trans_lin_homg_pts /= trans_lin_homg_pts[2,:]
Run Code Online (Sandbox Code Playgroud)

现在我们有了转换点:

[[-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.]]
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,一切都按预期工作:我们已经改变了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,
Run Code Online (Sandbox Code Playgroud)

因此,我们看到我们可以在正面和正面方向上获得远远超出原始图像的像素位置.最小值x不会改变,因为当单应性应用旋转时,它从左上角开始.现在需要注意的一点是,我已将变换应用于图像中的所有像素.但这实际上是不必要的,你可以简单地扭曲四个角点,看看它们落在哪里.

填充目标图像

请注意,当您打电话时,cv2.warpAffine()您必须输入目的地大小.这些变换的像素值参考该大小.因此,如果像素被映射到(-10,0),它将不会显示在目标图像中.这意味着我们必须制作另一个带有平移的单应性,将所有像素位置移动为正,然后我们可以填充图像矩阵以补偿我们的移位.如果单应性移动指向大于图像的位置,我们还必须在底部和右侧填充原始图像.

在最近的例子中,最小值x是相同的,所以我们不需要水平移位.但是,最小值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]
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,高度从原来增加了两个像素,以说明这种转变.

向转换添加转换以将所有像素位置转换为正

现在,我们需要创建一个新的单应矩阵来将扭曲的图像转换为我们移动的相同数量.并且要应用两种变换 - 原始变换和新变换 - 我们必须编写两个单应性(对于仿射变换,您可以简单地添加翻译,但不能用于单应性).另外,我们需要除以最后一个条目以确保比例仍然正确(再次,仅针对单应性):

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]
Run Code Online (Sandbox Code Playgroud)

我还在这里创建了锚点,我们将目标图像放在填充矩阵中; 它的移动量与单应性移动图像的数量相同.所以让我们将目标图像放在填充矩阵中:

dst_pad[anchorY:anchorY+dst_sz[0], anchorX:anchorX+dst_sz[1]] = dst
Run Code Online (Sandbox Code Playgroud)

翘曲与填充图像的新变换

我们剩下要做的就是将新变换应用于源图像(填充目标大小),然后我们可以覆盖这两个图像.

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)
Run Code Online (Sandbox Code Playgroud)

把它们放在一起

让我们为此创建一个函数,因为我们在这里创建了一些我们最不需要的变量.对于输入,我们需要源图像,目标图像和原始单应性.对于输出,我们只需要填充目标图像和扭曲图像.请注意,在示例中我们使用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
Run Code Online (Sandbox Code Playgroud)

运行该函数的示例

最后,我们可以用一些真实的图像和单应性来调用这个函数,看看它是如何实现的.我将借用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)
Run Code Online (Sandbox Code Playgroud)

我们最终得到这个填充的扭曲图像:

[填充和翘曲[1]

而不是你通常会得到的典型切断扭曲.