【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)

文章目录

  • 图像到图像的映射
    • 一、实验目的
    • 二、单应性变换
      • 1、直接线性变换
      • 2、仿射变换
    • 三、图像扭曲
      • 1、图像中的图像
      • 2、分段仿射扭曲
    • 四、问题及解决方案

图像到图像的映射

一、实验目的

1、学习了解本篇博客中 H_from_points(fp,tp)和affine_from_points函数;
2、根据匹配点,通过最小二乘法分别求解单应性变换矩阵和仿射变换矩阵;
3、【图像变换】根据仿射或单应性变换实现图像的扭曲、映射、融合。

二、单应性变换

【原理及数据】
单应性变换也称为“透视变换”

  1. 是将一个平面内的点映射到另一个平面内的二维投影变换。(平面是指图像或者三维中的平面表示)。
  2. 具有很强的实用性,比如图像配准,图像纠正和纹理扭曲,以及创建全景图像。
  3. 本质上,单应性变换H,按照下面的方程映射二维中的点(齐次坐标意义下):
    【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第1张图片
  4. 点的齐次坐标是依赖于其尺度定义的,所以,x=[x,y,w]=[ax,ay,aw]=[x/w,y/w,1]都表示同一个二维点。因此,单应性矩阵H也仅依赖尺度定义,所以,单应性矩阵具有8个独立的自由度。我们通常使用w=1来归一化点,这样,点具有唯一的图像坐标x和y。这个额外的坐标是的我们可以简单地使用一个矩阵来表示变换。

【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第2张图片
【代码】
创建homography.py文件,使用下边的函数实现对点进行归一化和转换齐次坐标的功能:

def normallize(points):
    """在齐次坐标意义下,对点集进行归一化,是最后一行为1"""
    for row in points:
        row /= points[-1]
    return points

def make_homog(points):
    """将点集(dim×n的数组)转换为齐次坐标表示"""

    return vstack((points,ones((1, points.shape[1]))))

【分析】
行点和变换处理时,我们会按照列优先的原则存储这些点。因此,n个二维点集将会存储为齐次坐标意义下的一个3 *n数组。这种格式使得矩阵乘法和点的变换操作更加容易。

1、直接线性变换

【原理及数据】
单应性矩阵可以由两幅图像中对应点对计算出来。一个完全射影变换具有8个自由度,根据对应点约束,每个对应点对可以写出两个方程,分别对应于x和y坐标:
【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第3张图片
【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第4张图片
因此,计算单应性矩阵H需要4个对应点对。
【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第5张图片

DLT(Direct Linear Transformation,直接线性变换)是给定4个点或者更多对应点对矩阵,来计算单应性矩阵H的算法。将单应性矩阵H作用在对应点上,重新写出该方程,我们可以得到下面的方程:
【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第6张图片
又因为上式即:
在这里插入图片描述
所以Ah=0,其中A是一个具有对应点对二倍数量行数的矩阵。将这些对应点对方程的系数堆叠到一个矩阵红,我们可以使用SVD算法找到H的最小二乘解。

【代码】

  • 下面是算法代码:(将其添加至homography.py文件中)
def H_from_points(fp, tp):
    """使用线性DLT方法,计算单应性矩阵H,使fp映射到tp。点自动进行归一化"""

    if fp.shape != tp.shape:
        raise RuntimeError('number of points do not match')

    # 对点进行归一化(对数值计算很重要)
    # --- 映射起始点 ---
    m = mean(fp[:2], axis=1)
    maxstd = max(std(fp[:2], axis=1)) + 1e-9
    C1 = diag([1/maxstd, 1/maxstd, 1])
    C1[0][2] = -m[0]/maxstd
    C1[1][2] = -m[1]/maxstd
    fp = dot(C1,fp)
    
    # --- 映射对应点 ---
    m = mean(tp[:2], axis=1)
    maxstd = max(std(tp[:2], axis=1)) + 1e-9
    C2 = diag([1 / maxstd, 1 / maxstd, 1])
    C2[0][2] = -m[0] / maxstd
    C2[1][2] = -m[1] / maxstd
    tp = dot(C2, tp)
    
    # 创建用于线性方法的矩阵,对于每个对应对,在矩阵中会出现两行数值
    nbr_correspondences = fp.shape[1]
    A = zeros((2 * nbr_correspondences, 9))
    for i in range(nbr_correspondences):
        A[2*i] = [-fp[0][i], -fp[1][i],-1,0,0,0,
                  tp[0][i]*fp[0][i],tp[0][i]*fp[1][i],tp[0][i]]
        A[2*i+1] = [0,0,0,-fp[0][i],-fp[1][i],-1,
                    tp[1][i]*fp[0][i],tp[1][i]*fp[1][i],tp[1][i]]
        
    U,S,V = linalg.svd(A)
    H = V[8].reshape((3,3))
    
    #反归一化
    H = dot(linalg.inv(C2),dot(H,C1))
    
    #归一化,然后返回
    return H / H[2,2]

【分析】
上面函数的第一步操作是检查点对的两个数组中点的数目是否相同。如果不相同,函数抛出异常信息。
对这些点进行归一化操作,使其均值为0,方差为1。然后使用对应点对构造矩阵A。最小二乘解即为矩阵SVD分解后所得矩阵V的最后一行。该行经过变形后得到矩阵H。然后对着矩阵进行处理和归一化,返回输出。

2、仿射变换

【原理及数据】
仿射变换(Affine Transformation或 Affine Map)是一种二维坐标到二维坐标之间的线性变换,它保持了二维图形的“平直性”(即:直线经过变换之后依然是直线)和“平行性”(即:二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。

  1. 简单来说,仿射变换就是允许图像任意倾斜,而且允许图形在两个方向上任意伸缩变换,但是,不能保持原来的线段长度不变,也不能保持原来的夹角角度不变。
  2. 由于仿射变换具有 6 个自由度,因此我们需要三个对应点对来估计矩阵 H。通过将最后两个元素设置为 0,即h20=h21=0,仿射变换可以用上面的 DLT 算法估计得出。
  3. 仿射变换的矩阵表现形式:
    【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第7张图片
    它可以通过一系列原子变换的复合来实现,其中包括平移,缩放,旋转,翻转和错切。不共线的三对对应点决定了一个唯一的仿射变换。

【代码】

  • 下面的函数使用“对应点对”来计算仿射变换矩阵,将其添加到homograph.py 文件中:
def Haffine_from_points(fp, tp):
    """计算H仿射变换,使得tp是fp经过仿射变换H得到的"""

    if fp.shape != tp.shape:
        raise RuntimeError('number of points do not match')

    # 对点进行归一化(对数值计算很重要)
    # --- 映射起始点 ---
    m = mean(fp[:2], axis=1)
    maxstd = max(std(fp[:2], axis=1)) + 1e-9
    C1 = diag([1 / maxstd, 1 / maxstd, 1])
    C1[0][2] = -m[0] / maxstd
    C1[1][2] = -m[1] / maxstd
    fp_cond = dot(C1, fp)

    # --- 映射对应点 ---
    m = mean(tp[:2], axis=1)
    C2 = C1.copy()  # 两个点集,必须都进行相同的缩放
    C2[0][2] = -m[0] / maxstd
    C2[1][2] = -m[1] / maxstd
    tp_cond = dot(C2, tp)

    # 因为归一化后点的均值为0,所以平移量为0
    A = concatenate((fp_cond[:2], tp_cond[:2]), axis=0)
    U, S, V = linalg.svd(A.T)

    # 如Hartley和Zisserman著的Multiplr View Geometry In Computer,Scond Edition所示,
    # 创建矩阵B和C
    tmp = V[:2].T
    B = tmp[:2]
    C = tmp[2:4]

    tmp2 = concatenate((dot(C, linalg.pinv(B)), zeros((2, 1))), axis=1)
    H = vstack((tmp2, [0, 0, 1]))

    # 反归一化
    H = dot(linalg.inv(C2), dot(H, C1))

    return H / H[2, 2]

同样地,类似于 DLT 算法,这些点需要经过预处理和去处理化操作。

三、图像扭曲

【原理】
对图像块应用仿射变换,我们将其称为图像扭曲(或者仿射扭曲)。该操作不仅常应用在计算机图形学中,而且经常出现在计算机视觉算法中。扭曲操作可以使用SciPy 工具包中的 ndimage 包来简单完成。

  • 命令:
transformed_im = ndimage.affine_transform(im,A,b,size)

使用如上所示的一个线性变换 A 和一个平移向量 b 来对图像块应用仿射变换。选项参数 size 可以用来指定输出图像的大小。默认输出图像设置为和原始图像同样大小。
【代码】

  • 如下面扭曲操作的代码:
from numpy import *
from matplotlib.pyplot import *
from scipy import ndimage
from PIL import Image

im = array(Image.open('xx.jpg').convert('L'))
H = array([[1.4,0.05,-100],[0.05,1.5,-100],[0,0,1]])
im2 = ndimage.affine_transform(im, H[:2,:2],(H[0,2],H[1,2]))

gray()
subplot(121)
imshow(im)
axis('off')
subplot(122)
imshow(im2)
axis('off')
show()
  • 得到的结果:
    【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第8张图片
    【分析】
    可以清楚的看见右图输出图像结果中丢失的像素用零来填充。

1、图像中的图像

【原理】
仿射扭曲的一个简单例子是,将图像或者图像的一部分放置在另一幅图像中,使得他们能够和指定的区域或者标记物对齐。
【代码】

  • 将函数 image_in_image()添加到warp.py文件中。该函数的输入参数为两幅图像和一个坐标。该坐标为将第一幅图像放置到第二幅图像中的角点坐标:
import homography
from numpy import array
def image_in_image(im1, im2, tp):
    """使用仿射变换将im1放置在im2上,使im1图像的角和tp尽可能的靠近
        tp是齐次表示的,并且是按照从左上角逆时针计算的"""

    # 扭曲的点
    m, n = im1.shape[:2]
    fp = array([[0, m, m, 0], [0, 0, n, n], [1, 1, 1, 1]])

    # 计算仿射变换,并且将其应用于图像im1中
    H = homography.Haffine_from_points(tp, fp)
    im1_t = homography.ndimage.affine_transform(im1, H[:2, :2],
                                     (H[0, 2], H[1, 2]), im2.shape[:2])
    alpha = (im1_t > 0)

    return (1 - alpha) * im2 + alpha * im1_t

【分析】
该函数没有很多繁杂的操作,将扭曲的图像和第二幅图像融合,创建了 alpha 图像,该图像定义了每个像素从各个图像中获取的像素值成分多少。扭曲的图像是在扭曲区域边界之外以 0 来填充的图像,来创建一个二值的 alpha 图像。严格意义上说,需要在第一幅图像中的潜在 0 像素上加上一个小的数值,或者合理地处理这些 0 像素。
【注意】这里使用的图像坐标是齐次坐标意义下的。

【代码】

  • 以下代码实现将第一幅图插入到第二张图:
import warp
from numpy import *
from matplotlib.pyplot import *
from scipy import ndimage
from numpy import array
from PIL import Image
im1 = array(Image.open('2.jpg').convert('L'))
im2 = array(Image.open('1.jpg').convert('L'))

gray()
subplot(131)
imshow(im1)
axis('equal')
axis('off')
subplot(132)
imshow(im2)
axis('equal')
axis('off')

# 选定一些目标点
tp = array([[264, 538, 540, 264], [40, 36, 605, 605], [1, 1, 1, 1]])

im3 = warp.image_in_image(im1, im2, tp)
subplot(133)
imshow(im3)
axis('equal')
axis('off')
show()
  • 得到的结果:
    【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第9张图片

【分析】
上面的代码将图像放置在小猫的脸部。需要注意,标记物的坐标 tp 是用齐次坐标意义下的坐标表示的。将这些坐标换成:

tp = array([[274, 562, 540, 164],[500, 500, 1005, 975],[1,1,1,1]])

则图像会放置在小狗的脸部:
【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第10张图片
函数 Haffine_from_points() 会返回给定对应点对的最优仿射变换。如果透视效应比较弱,那么这种方法会返回很好的结果。

2、分段仿射扭曲

【原理】
分段仿射扭曲(Piecewise affine warping )是对应点集合之间最常用的扭曲方式,给定任意图像的标记点,通过将这些点进行三角剖分,然后使用仿射扭曲来扭曲每个三角形,可以将图像和另一幅图像的对应标记点扭曲对应。为了三角化这些点,可以使用狄洛克三角剖分方法。
狄洛克三角剖分选择一些三角形,使三角剖分中所有三角形的最小角度最大 1。函数 delaunay() 有 4 个输出,其中我们仅需要三角形列表信息(第三个输出)。
【代码】

  • 在 warp.py 文件中创建用于三角剖分的函数:
# 三角剖分的函数
def triangulate_points(x,y):
    """ Delaunay triangulation of 2D points. """
    
    # centers,edges,tri,neighbors = md.delaunay(x,y)
    tri = Delaunay(np.c_[x, y]).simplices

    return tri

  • 然后写出一个用于分段仿射图像扭曲的通用扭曲函数,将其放入warp.py文件中:
def pw_affine(fromim, toim, fp, tp, tri):
    """ 从一幅图像中扭曲矩形图像块
    fromim= 将要扭曲的图像
    toim= 目标图像
    fp= 齐次坐标表示下,扭曲前的点
    tp= 齐次坐标表示下,扭曲后的点
    tri= 三角剖分 """
    im = toim.copy()
    # 检查图像是灰度图像还是彩色图象
    is_color = len(fromim.shape) == 3
    # 创建扭曲后的图像(如果需要对彩色图像的每个颜色通道进行迭代操作,那么有必要这样做)
    im_t = zeros(im.shape, 'uint8')
    for t in tri:
    # 计算仿射变换
    H = homography.Haffine_from_points(tp[:, t], fp[:, t])
    if is_color:
        for col in range(fromim.shape[2]): 图像到图像的映射 | 69
    im_t[:, :, col] = ndimage.affine_transform(
        fromim[:, :, col], H[:2, :2], (H[0, 2], H[1, 2]), im.shape[:2])
    else:
    im_t = ndimage.affine_transform(
        fromim, H[:2, :2], (H[0, 2], H[1, 2]), im.shape[:2])
    # 三角形的 alpha
    alpha = alpha_for_triangle(tp[:, t], im.shape[0], im.shape[1])

    # 将三角形加入到图像中
    im[alpha > 0] = im_t[alpha > 0]
    return im

【分析】
我们首先检查该图像是灰度图像还是彩色图像。如果图像为彩色图像,则对每个颜色通道进行扭曲处理。因为对于每个三角形来说,仿射变换是唯一确定的,所以我们这里使用Haffine_from_points() 函数来处理。
【代码】

  • 我们通过下面的辅助函数(将其添加到 warp.py 文件中)来绘制出图像中的这些三角形:
# 绘制三角形
def plot_mesh(x, y, tri):
    """ 绘制三角形 """
    for t in tri:
        t_ext = [t[0], t[1], t[2], t[0]]  # 将第一个点加入到最后
        plot(x[t_ext], y[t_ext], 'r')

  • 最后撰写一个主程序调用:
from numpy import *
from matplotlib.pyplot import *
from PIL import Image
from matplotlib import pyplot as plt
import warp
from PCV.geometry import warp
from PIL import Image
from pylab import *

# 打开图像,并将其扭曲
fromim = array(Image.open(r'xx.jpg').convert('L'))
x, y = meshgrid(range(5), range(6))
x = (fromim.shape[1] / 4) * x.flatten()
y = (fromim.shape[0] / 5) * y.flatten()
# 三角剖分
tri = warp.triangulate_points(x, y)
# 打开图像
im = array(Image.open(r'2.jpg').convert('L'))
gray()
imshow(im)
# 手工选取目标点
tp = plt.ginput(30)
for i in range(0, len(tp)):
    tp[i] = list(tp[i])
    tp[i][0] = int(tp[i][0])
    tp[i][1] = int(tp[i][1])
tp = array(tp)

# 将点转换成齐次坐标
fp = vstack((y, x, ones((1, len(x)))))
tp = vstack((tp[:, 1], tp[:, 0], ones((1, len(tp)))))
# 扭曲三角形
im = warp.pw_affine(fromim, im, fp, tp, tri)
# 绘制图像
figure()
imshow(im)
# 绘制三角形
warp.plot_mesh(tp[1], tp[0], tri)
axis('off')
show()


【结果】
【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第11张图片
【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第12张图片
【Python计算机视觉】图像到图像的映射(单应性变换、图像扭曲)_第13张图片
【分析】

  • 所有目标点需要通过ginput函数去手动选取,然后将这些点写到turningtorso1_points.txt文件中,至少选择30个点(因为triangulate_points函数得到的三角形的点最大下标是29)
  • 手动取点的时候,按照左向右、从上到下的顺序进行采点方可得出理想的效果(如第二张图),若胡乱取点则拼接出来的三角形就会发生很大的错位(如第三张图)。

四、问题及解决方案

1.运行代码时遇到:ModuleNotFoundError: No module named ‘matplotlib.delaunay’

  • 解决方案:
    将import matplotlib.delaunay as md改为:

    python from scipy.spatial import Delaunay
    再将wrap.py中的triangulate_points函数中的语句替换为:

    python tri = Delaunay(np.c_[x,y]).simplices

2.运行代码时遇到:TypeError: ‘numpy.float64’ object cannot be interpreted as an integer

  • 解决方案:alpha_for_triangle函数下添加:

    python points = np.array(points,dtype=np.int)

3.运行代码时遇到:IndexError: index 0 is out of bounds for axis 1 with size 0

  • 解决方案:使用ginput() 函数进行手工选取目标点

你可能感兴趣的:(实验,计算机视觉,计算机视觉,python,pycharm,深度学习,图像处理)