单应性变换是将一个平面内的点映射到另一个平面内的二维投影变换。在这里,平 面是指图像或者三维中的平面表面。单应性变换具有很强的实用性,比如图像配准、 图像纠正和纹理扭曲,以及创建全景图像。本质上,单应性变换H,按照下面的方程映射二维中的点(齐次坐标意义下):
[ x ′ y ′ w ′ ] = [ h 1 h 2 h 3 h 4 h 5 h 6 h 7 h 8 h 9 ] [ x y w ] 或 x ′ = H x \left[\begin{array}{l} x^{\prime} \\ y^{\prime} \\ w^{\prime} \end{array}\right]=\left[\begin{array}{lll} h_{1} & h_{2} & h_{3} \\ h_{4} & h_{5} & h_{6} \\ h_{7} & h_{8} & h_{9} \end{array}\right]\left[\begin{array}{l} x \\ y \\ w \end{array}\right] \text { 或 } \quad \mathbf{x}^{\prime}=\boldsymbol{H} \mathbf{x} ⎣⎡x′y′w′⎦⎤=⎣⎡h1h4h7h2h5h8h3h6h9⎦⎤⎣⎡xyw⎦⎤ 或 x′=Hx
也就是原图中的点为x,经过经过矩阵H的变换,就能变换为 x ′ x^{\prime} x′等。点的齐次坐标是依赖于其尺度定义的,所以, x=[x,y,w]=[αx,αy,αw]=[x/w,y/w,1] 都表示同一个二维点。因此,单应性矩阵 H也仅 依赖尺度定义,所以,单应性矩阵具有 8 个独立的自由度。我们通常使用 w=1 来归 一化点,这样,点具有唯一的图像坐标 x 和 y。这个额外的坐标使得我们可以简单地使用一个矩阵来表示变换。
单应性矩阵可以由两幅图像中对应点对计算出来。且一个完全射影变换具有 8 个自由度。根据对应点约束,每个对应点对可以写出两个 方程,分别对应于 x 和 y 坐标。因此,计算单应性矩阵H需要4个对应点对。所谓的DLT(Direct Linear Transformation,直接线性变换)是给定4个或者更多对应点对 矩阵,来计算单应性矩阵H的算法。参数求解过程如下:
[ w x ′ w y ′ w ] = [ h 00 h 01 h 02 h 10 h 11 h 12 h 20 h 21 h 22 ] [ x y 1 ] \left[\begin{array}{c} w x^{\prime} \\ w y^{\prime} \\ w \end{array}\right]=\left[\begin{array}{lll} h_{00} & h_{01} & h_{02} \\ h_{10} & h_{11} & h_{12} \\ h_{20} & h_{21} & h_{22} \end{array}\right]\left[\begin{array}{l} x \\ y \\ 1\\ \end{array}\right] \quad ⎣⎡wx′wy′w⎦⎤=⎣⎡h00h10h20h01h11h21h02h12h22⎦⎤⎣⎡xy1⎦⎤
其中 w w w=1,且
x i ′ = h 00 x i + h 01 y i + h 02 h 20 x i + h 21 y i + h 22 y i ′ = h 10 x i + h 11 y i + h 12 h 20 x i + h 21 y i + h 22 \begin{array}{r} x_{i}^{\prime}=\frac{h_{00} x_{i}+h_{01} y_{i}+h_{02}}{h_{20} x_{i}+h_{21} y_{i}+h_{22}} \\ y_{i}^{\prime}=\frac{h_{10} x_{i}+h_{11} y_{i}+h_{12}}{h_{20} x_{i}+h_{21} y_{i}+h_{22}} \end{array} xi′=h20xi+h21yi+h22h00xi+h01yi+h02yi′=h20xi+h21yi+h22h10xi+h11yi+h12
得到:
x i ′ ( h 20 x i + h 21 y i + h 22 ) = h 00 x i + h 01 y i + h 02 y i ′ ( h 20 x i + h 21 y i + h 22 ) = h 10 x i + h 11 y i + h 12 \begin{aligned} &x_{i}^{\prime}\left(h_{20} x_{i}+h_{21} y_{i}+h_{22}\right)=h_{00} x_{i}+h_{01} y_{i}+h_{02} \\ &y_{i}^{\prime}\left(h_{20} x_{i}+h_{21} y_{i}+h_{22}\right)=h_{10} x_{i}+h_{11} y_{i}+h_{12} \end{aligned} xi′(h20xi+h21yi+h22)=h00xi+h01yi+h02yi′(h20xi+h21yi+h22)=h10xi+h11yi+h12
[ x i y i 1 0 0 0 − x i ′ x i − x i ′ y i − x i ′ 0 0 0 x i y i 1 − y i ′ x i − y i ′ y i − y i ′ ] [ h 00 h 01 h 02 h 10 h 11 h 12 h 20 h 21 h 22 ] = [ 0 0 ] \left[\begin{array}{ccccccccc} x_{i} & y_{i} & 1 & 0 & 0 & 0 & -x_{i}^{\prime} x_{i} & -x_{i}^{\prime} y_{i} & -x_{i}^{\prime} \\ 0 & 0 & 0 & x_{i} & y_{i} & 1 & -y_{i}^{\prime} x_{i} & -y_{i}^{\prime} y_{i} & -y_{i}^{\prime} \end{array}\right]\left[\begin{array}{l} h_{00} \\ h_{01} \\ h_{02} \\ h_{10} \\ h_{11} \\ h_{12} \\ h_{20} \\ h_{21} \\ h_{22} \end{array}\right]=\left[\begin{array}{l} 0 \\ 0 \end{array}\right] [xi0yi0100xi0yi01−xi′xi−yi′xi−xi′yi−yi′yi−xi′−yi′]⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡h00h01h02h10h11h12h20h21h22⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤=[00]
即:
[ x 1 y 1 1 0 0 0 − x 1 ′ x 1 − x 1 ′ y 1 − x 1 ′ 0 0 0 x 1 y 1 1 − y 1 ′ x 1 − y 1 ′ y 1 − y 1 ′ … … x n y n 1 0 0 0 − x n ′ x n − x n ′ y n − x n ′ 0 0 0 x n y n 1 − y n ′ x n − y n ′ y n − y n ′ ] [ h 00 h 01 h 02 h 10 h 11 h 12 h 20 h 21 h 22 ] = [ 0 0 ⋮ 0 0 ] \begin{gathered}{\left[\begin{array}{ccccccccc}x_{1} & y_{1} & 1 & 0 & 0 & 0 & -x_{1}^{\prime} x_{1} & -x_{1}^{\prime} y_{1} & -x_{1}^{\prime} \\0 & 0 & 0 & x_{1} & y_{1} & 1 & -y_{1}^{\prime} x_{1} & -y_{1}^{\prime} y_{1} & -y_{1}^{\prime} \\……\\x_{n} & y_{n} & 1 & 0 & 0 & 0 & -x_{n}^{\prime} x_{n} & -x_{n}^{\prime} y_{n} & -x_{n}^{\prime} \\0 & 0 & 0 & x_{n} & y_{n} & 1 & -y_{n}^{\prime} x_{n} & -y_{n}^{\prime} y_{n} & -y_{n}^{\prime}\end{array}\right]\left[\begin{array}{l}h_{00} \\h_{01} \\h_{02} \\h_{10} \\h_{11} \\h_{12} \\h_{20} \\h_{21} \\h_{22}\end{array}\right]=\left[\begin{array}{c}0 \\0 \\\vdots \\0 \\0\end{array}\right]} \\\end{gathered} ⎣⎢⎢⎢⎢⎡x10……xn0y10yn010100x10xn0y10yn0101−x1′x1−y1′x1−xn′xn−yn′xn−x1′y1−y1′y1−xn′yn−yn′yn−x1′−y1′−xn′−yn′⎦⎥⎥⎥⎥⎤⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡h00h01h02h10h11h12h20h21h22⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎢⎢⎡00⋮00⎦⎥⎥⎥⎥⎥⎤
可记为Ah = 0 ,其中A是一个具有对应点对二倍数量行数的矩阵,且A的维度为2n×9,h的维度为9,0的维度为2n。根据最小二乘解, h ^ = A T A \hat{h}=A^{T}A h^=ATA最小特征值对应的特征向量。代码如下:
def H_from_points(fp,tp):
""" Find homography H, such that fp is mapped to tp
using the linear DLT method. Points are conditioned
automatically. """
if fp.shape != tp.shape:
raise RuntimeError('number of points do not match')
# condition points (important for numerical reasons)
# --from points--
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)
# --to points--
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)
# create matrix for linear method, 2 rows for each correspondence pair
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))
# decondition
H = dot(linalg.inv(C2),dot(H,C1))
# normalize and return
return H / H[2,2]
其实可以伪造一些数据来调用此函数,直观感受下输入输出是什么。首先伪造输入,即两个随机矩阵,大小为3×3,然后调用上述函数:
fp = np.random.randint(0, 500, size=(3, 3))
tp = np.random.randint(0, 500, size=(3, 3))
result = H_from_points(fp,tp)
然后result结果如下:
array([[ 2.96402671e+00, -1.27728682e+00, -1.22660693e+01],
[ 1.44447830e+00, -3.97930961e-01, -3.30726226e+01],
[ 4.88783355e-03, -3.40812578e-03, 1.00000000e+00]])
可以看到数组的最后一个值就是1。
仿射变换有 6 个自由度,因此需要三个对应点对来估计矩阵H。通过将 最后两个元素设置为 0,即 h7=h8=0,仿射变换可以用上面的 DLT 算法估计得出。代码如下:
def Haffine_from_points(fp,tp):
""" Find H, affine transformation, such that
tp is affine transf of fp. """
if fp.shape != tp.shape:
raise RuntimeError('number of points do not match')
# condition points
# --from points--
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)
# --to points--
m = mean(tp[:2], axis=1)
C2 = C1.copy() #must use same scaling for both point sets
C2[0][2] = -m[0]/maxstd
C2[1][2] = -m[1]/maxstd
tp_cond = dot(C2,tp)
# conditioned points have mean zero, so translation is zero
A = concatenate((fp_cond[:2],tp_cond[:2]), axis=0)
U,S,V = linalg.svd(A.T)
# create B and C matrices as Hartley-Zisserman (2:nd ed) p 130.
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]))
# decondition
H = dot(linalg.inv(C2),dot(H,C1))
return H / H[2,2]
同样按照上述可看下函数的输入输出:
array([[ 860.17452003, -599.25709744, -111.19311744],
[ 658.73707606, -458.91440696, -85.53262921],
[ 0. , 0. , 1. ]])
能看到最后一行的数据为0,0,1,因此也就对应了6个自由度。
所谓图像扭曲,就是仿射扭曲,进行的操作即是改变物体位置和形状,但是保持平直性。可先简单看下效果:
图像扭曲的一个简单例子是,将图像或者图像的一部分放置在另一幅图像中,使得它们能够和指定的区域或者标记物对齐。也就是把图像放在图像上,图像中的图像。函数如下:
def image_in_image(im1,im2,tp):
""" Put im1 in im2 with an affine transformation
such that corners are as close to tp as possible.
tp are homogeneous and counter-clockwise from top left. """
# points to warp from
m,n = im1.shape[:2]
fp = array([[0,m,m,0],[0,0,n,n],[1,1,1,1]])
# compute affine transform and apply
H = homography.Haffine_from_points(tp,fp)
im1_t = 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
该函数的输入参数为两幅图像和 一个坐标。该坐标为将第一幅图像放置到第二幅图像中的角点坐标:
函数 Haffine_from_points() 会返回给定对应点对的最优仿射变换。
分段仿射扭曲是对应点对集合之间最常用的扭曲方式。给定任意图像的标记 点,通过将这些点进行三角剖分,然后使用仿射扭曲来扭曲每个三角形,我们可以 将图像和另一幅图像的对应标记点扭曲对应。对于任何图形和图像处理库来说,这 些都是最基本的操作。下面我们来演示一下如何使用 Matplotlib 和 SciPy 来完成该操作。使用的是 Matplotlib中的狄洛克三角剖分:
import numpy as np
from scipy.spatial import Delaunay
import matplotlib.pyplot as plt
x,y = array(random.standard_normal((2,100)))
tri = Delaunay(np.c_[x,y]).simplices
plt.figure(dpi = 140)
plt.subplot(121)
plt.plot(x,y,'*')
plt.axis('off')
for t in tri:
t_ext = [t[0], t[1], t[2], t[0]] # 将第一个点加入到最后 plot(x[t_ext],y[t_ext],'r')
plt.subplot(122)
plt.plot(x[t_ext],y[t_ext],'r')
plt.plot(x,y,'*')
plt.axis('off')
plt.show()
上图显示了一些实例点和三角剖分的结果。狄洛克三角剖分选择一些三角形, 使三角剖分中所有三角形的最小角度最大。
图像配准是对图像进行变换,使变换后的图像能够在常见的坐标系中对齐。配准可以是严格配准,也可以是非严格配准。为了能够进行图像对比和更精细的图像分析,图像配准是一步非常重要的操作。图像配准具有广泛的应用,适用于同一个场景中有多张图像需要进行匹配或叠加。在医学图像领域以及卫星图像分析和光流(optical flow)方面非常普遍。
在同一位置(即图像的照相机位置相同)拍摄的两幅或者多幅图像是单应性相关的,我们可以使用该约束将很多图像缝补起来,拼成一个大的图像来创建全景图。
RANSAC(RAndom SAmple Consensus,随机采样一致)算法是从一组含有“外点”(outliers)的数据中正确估计数学模型参数的迭代算法。“外点”一般指的的数据中的噪声,比如说匹配中的误匹配和估计曲线中的离群点。所以,RANSAC也是一种“外点”检测算法。对于RANSAC算法来说一个基本的假设就是数据是由“内点”和“外点”组成的。“内点”就是组成模型参数的数据,“外点”就是不适合模型的数据。同时RANSAC假设:在给定一组含有少部分“内点”的数据,存在一个程序可以估计出符合“内点”的模型。可用代码实现:
import numpy as np
import matplotlib.pyplot as plt
import random
import math
# 数据量。
SIZE = 50
# 产生数据。np.linspace 返回一个一维数组,SIZE指定数组长度。
# 数组最小值是0,最大值是10。所有元素间隔相等。
X = np.linspace(0, 10, SIZE)
Y = 3 * X + 10
fig = plt.figure()
# 画图区域分成1行1列。选择第一块区域。
ax1 = fig.add_subplot(1,1, 1)
# 标题
ax1.set_title("RANSAC")
# 让散点图的数据更加随机并且添加一些噪声。
random_x = []
random_y = []
# 添加直线随机噪声
for i in range(SIZE):
random_x.append(X[i] + random.uniform(-0.5, 0.5))
random_y.append(Y[i] + random.uniform(-0.5, 0.5))
# 添加随机噪声
for i in range(SIZE):
random_x.append(random.uniform(0,10))
random_y.append(random.uniform(10,40))
RANDOM_X = np.array(random_x) # 散点图的横轴。
RANDOM_Y = np.array(random_y) # 散点图的纵轴。
# 画散点图。
ax1.scatter(RANDOM_X, RANDOM_Y)
# 横轴名称。
ax1.set_xlabel("x")
# 纵轴名称。
ax1.set_ylabel("y")
# 使用RANSAC算法估算模型
# 迭代最大次数,每次得到更好的估计会优化iters的数值
iters = 100000
# 数据和模型之间可接受的差值
sigma = 0.25
# 最好模型的参数估计和内点数目
best_a = 0
best_b = 0
pretotal = 0
# 希望的得到正确模型的概率
P = 0.99
for i in range(iters):
# 随机在数据中红选出两个点去求解模型
sample_index = random.sample(range(SIZE * 2),2)
x_1 = RANDOM_X[sample_index[0]]
x_2 = RANDOM_X[sample_index[1]]
y_1 = RANDOM_Y[sample_index[0]]
y_2 = RANDOM_Y[sample_index[1]]
# y = ax + b 求解出a,b
a = (y_2 - y_1) / (x_2 - x_1)
b = y_1 - a * x_1
# 算出内点数目
total_inlier = 0
for index in range(SIZE * 2):
y_estimate = a * RANDOM_X[index] + b
if abs(y_estimate - RANDOM_Y[index]) < sigma:
total_inlier = total_inlier + 1
# 判断当前的模型是否比之前估算的模型好
if total_inlier > pretotal:
iters = math.log(1 - P) / math.log(1 - pow(total_inlier / (SIZE * 2), 2))
pretotal = total_inlier
best_a = a
best_b = b
# 判断是否当前模型已经符合超过一半的点
if total_inlier > SIZE:
break
# 用我们得到的最佳估计画图
Y = best_a * RANDOM_X + best_b
# 直线图
ax1.plot(RANDOM_X, Y)
plt.show()
普通最小二乘是保守派:在现有数据下,如何实现最优。是从一个整体误差最小的角度去考虑,尽量谁也不得罪。
RANSAC是改革派:首先假设数据具有某种特性(目的),为了达到目的,适当割舍一些现有的数据。
实际情况中,创建全景图的步骤大致分为三步:图像获取、图像配准、图像融合。两张待配准的图像处于不同的坐标系中,配准的目的就是将他们投影到拼接平面(即同一坐标系下)对齐。拼接的前提是,我们的图片集中图片之间要有共视区域,这样我们才能在各图像之间提取特征并寻找匹配点,然后在尽量去除错配点对的情况下,根据匹配点计算图像间的映射关系,最后将图像根据映射关系拼接以获得更宽广的可视面积,多次进行配准拼接融合步骤,就得到了我们所需的全景图。