多视图几何三维重建

基础知识

相机标定
相机成像原理:世界坐标系、相机坐标系、图像坐标系、像素坐标系之间的转换
基于单目视觉的三维重建算法综述
SIFT图像匹配算法
增量式SFM估计相机运动和三角测量进行重建
增量式SFM后的非线性优化算法Bundle Adjustment
三角化—深度滤波器—单目稠密重建
双目立体匹配,SFM和MVS三维点云重建的区别和联系

欧拉角一般都是动态定义,很多人习惯 动态定义里的外旋=主动旋转=惯性参考系=左乘 的定义方式。
roll Z, pitch X, yaw Y。顺序按是什么顺规来(习惯ZXY),左乘右乘看外旋还是内旋。动态定义里的内旋会出现万象锁现象。

我们首先介绍使用十分广泛的几种图像处理算法,然后转而介绍器像处理的机器学习实现。主要内容如下

  1. 使用SIFT(scale-invariant feature transform,尺度不变特征转换)算法的特征映射
  2. 使用RANSAC(random sample consensus,随机样本共识算法的图像配准
  3. 使用人工神经网络的图像分类
  4. 使用卷积神经网络的图像分类
  5. 使用机器学习的图像分类
  6. 重要术语

使用SIFT算法的特征映射

假设我们有两张图像。一张图像是公园里的长凳,另一张图像是包含那张长凳的整个公园。现在假设我们想要编写代码以找出那张公园图像里的长凳。你可能认为这是件容易的事情,现在让我来说点儿复杂的事情。如果那张长凳的图像放大过会怎样?或者那张图像旋转过某个角度呢?或者两种情况都有E?现在你要如何进行处理?

答案就是使用SIFT算法。顾名思义,无论如何放大(或缩小。图像都不会影响我们找到图像间的相似性.SIFT算法另一个特点是旋转不变性。无论转动多少角度,性能依然很好。唯一的问题是SIFT算法已经获得专利,也就是说,用于学术目的是没有问题的,但是用于商业就可能牵涉很多法律问题。不过,这暂时不会让我们放弃学习和应用SIFT算法。

我们首先必须理解SIT算法的基本知识,接着可以在Python下求两张图像的相似性,最后逐行查看代码。

下面介绍SIFT算法在图像处理过程中尝试剔除的图像特征:
缩放(放大或缩小图像)
旋转
光照
透视

可以看到,SIFT算法不仅考虑了缩放和旋转,还兼顾了图像中的光照和我们观察的视角。但这是如何做到的呢?下面逐步介绍使用SIFT算法的过程:
(1)构造尺度不变的空间。
(2)求两个高斯之差。
(3)找出图像中的关键点。
(4)为了高效地比较,移除非关键点。
(5)提供步骤3中找到的关键点的方向。
(6)确定唯一关键特征。

步骤1:构造尺度不变的空间
首先我们对原始图像做高斯模糊,去掉图像中的部分非关键点和额外噪音。成之后,重新调整图像的大小,并重复这一过程。调整大小和高斯模糊依赖因素,我们这里不深入讨论数学细节。
步骤2:求两个高斯之差
求步骤1得到的图像之间的差值。这使得图像具有尺度不变的性质。
步骤3:找出图像中的关键点
找出重要的点也称为关键点,我们在步骤2求得的两个高斯之间的差用于确定局部极大值和极小值。我们查看每个像素的邻点:如果一个像素的值在邻域中最大极大值或最小极小值),就将它标记为关键点。
接下来找到子像素(subpixel)的极大值和/或极小值。我们用泰勒展开(Taylor expansion)求子像素,在找到子像素之后,我们使用同样的过程继续求极大值和极小值。另外,为了只找到角点(corner)作为关键点,我们应用海赛矩阵(Hessian matrix)。角点总是被当成最佳关键点。
步骤4:为了高效地比较,移除非关键点
在这一步,我们首先确定一个阈值,在由关键点组成的图像和子像素图像中,我们将像素的强度与阈值做比较。如果像素的强度小于阈值,就将像素当成不重要的关键点丢弃。
步骤5:提供关键点的方向
我们为每个关键点及其邻点找到梯度的方向和大小,然后把关键点周围的最高频的方向分配给它。我们用直方图找出这些方向并得到最终结果。
步骤6:确定唯一关键特征
为了让这些关键点具有唯一性,我们从关键点里提取关键特征,另外,我们确保在拿这些关键点与第二张图像做比较时,它们不完全相似,但是几乎相似。

在了解了SIFT算法的基本知识之后,下面介绍将SIFT算法应用到两张图像上的代码。

import cv2
import numpy as np
from matplotlib import pyplot as plt
from sift_Operations import *

Image1 = cv2.imread('./images/1.png')
Image2 = cv2.imread('./images/2.png')

Image1_gray = cv2.cvtColor(Image1, cv2.COLOR_BGR2GRAY)
Image2_gray = cv2.cvtColor(Image2, cv2.COLOR_BGR2GRAY)

Image1_key_points, Image1_descriptors = extract_sift_features(Image1_gray)
Image2_key_points, Image2_descriptors = extract_sift_features(Image2_gray)

print('Displaying SIFT Features')
showing_sift_features(Image1_gray, Image1, Image1_key_points)

norm = cv2.NORM_L2
bruteForce = cv2.BFMatcher(norm)

matches = bruteForce.match(Image1_descriptors, Image2_descriptors)

matches = sorted(matches, key = lambda match:match.distance)

matched_img = cv2.drawMatches(
    Image1, Image1_key_points,
    Image2, Image2_key_points,
    matches[:100], Image2.copy())

plt.figure(figsize=(100,300))
plt.imshow(matched_img)

以上代码将整个SIFT算法应用到两张图片上。但是,SIFT算法保存在与这段代码位于同一个目录的Sift_Operations.py文件中。我们来看下代码:

## Sift_Operations.py
import cv2
from matplotlib import pyplot as plt
import numpy as np

def extract_sift_features(img):
    sift_initialize = cv2.xfeatures2d.SIFT_create()
    key_points, descriptors = sift_initialize.detectAndCompute(img, None)
    return key_points, descriptors

def showing_sift_features(img1, img2, key_points):
    return plt.imshow(cv2.drawKeypoints(img1, key_points, img2.copy()))

现在我们来研究这段代码,必要的时候我们会在这两个文件之间来回切换:

  1. 在主代码中,导入重要的程序库:OpenCV、NumPy、matplotlib,还有自定义模块Sift_Operations,import* 的意思是导入Python文件中的所有内容。
  2. 然后,读入要应用SIFT算法的两张图像。
  3. 接下来,将图像转换成灰度图.SIFT算法需要在灰度图上操作。便用OpenCV的cv2.cvtColor()函数转换色彩格式:
    Image1_gray = cv2.cvtColor(Image1, cv2.COLOR_BGR2GRAY) Image2_gray = cv2.cvtColor(Image2, cv2.COLOR_BGR2GRAY)

  4. 现在将这两张图像传给extract_sift_features()函数,它位于Sift_Operations.py文件中。这个函数返回在图像中找到的关键点和具有描述子(descriptor)名字的那些点的特征。我们来看下这个函数的内部:
    sift_initialize = cv2.xfeatures2d.SIFT_create()


    a. 上面这代码将整个SIFT算法保存在变量shift_initialize中。
    b. detectAndCompute()函数将SIFT算法应用于图像并返回关键点和描述子:
    key_points, descriptors = sift_initialize.detectAndCompute(img, None)
    c. 然后返回这些值:
    return key_points,descriptors
    d. 在调用代码中,这些值被保存在与具体图像相关的不同变量中:
    Image1_key_points, Image1_descriptors = extract_sift_features(Image1_gray) Image2_key_points, Image2_descriptors = extract_sift_features(Image2_gray)

  5. 然后显示特征,以便我们能够看到关键点和相似性。这里使用了showing_sift_features方法。
  6. 我们来看看这个方法的内部。这里使用cv2.drawKeypoints()函数绘制在两张图像中找到的关键点。
  7. 然后初始化norm变量,并用它求出关键点之间的距离,这里使用cv2.Norm_L2()函数计算曼哈顿距离,也就是两个点之间沿垂直坐标轴的距离。它不是直线距离,而是用来了一种网格的思路。
    多视图几何三维重建_第1张图片
  8. 接下来,初始化cv2.BFMatcher()函数。它用于匹配关键点的描述子。nom变量作为参数传递,它告诉cv2.BFMatcher()函数在匹配时使用曼哈顿距离。初始化完毕后的算法保存在bruteForce变量中。

  9. bruteForce.match匹配了两个描述子。将匹配结果matches根据曼哈顿距离排序:
    matches = bruteForce.match(Image1_descriptors, Image2_descriptors)
    matches = sorted(matches, key = lambda match:match.distance)

  10. 根据排完序的匹配结果的前100条,连接两张图像的关键点:
    matched_img = cv2.drawMatches(Image1, Image1_key_points, Image2, Image2_key_points, matches[:100], Image2.copy())
  11. 最后,显示匹配的图像。

Python中的sort和lambda函数
单应性矩阵的理解及求解
仿射变换
变换矩阵
旋转变换矩阵

# 另一种代码
import numpy as np
import cv2
import matplotlib.pyplot as plt

print("debug")
# 加载图片
img1 = cv2.imread('images/1.jpg',0)          # queryImage
# img3是和img1一样的照片
img3 = cv2.imread('images/3.jpg',0)
# 对这张照片进行高斯模糊
blurred = cv2.GaussianBlur(img3, (9, 9), 0)
# 再旋转
(height, width) = blurred.shape[:2]
center = (width//2, height//2)
# 定义旋转矩阵,3个参数分别为:旋转中心,旋转角度,缩放比率
M = cv2.getRotationMatrix2D(center, 45, 1.0)
# 正式旋转,这样就得到了和原始图片img1不太一样的照片
rotated = cv2.warpAffine(blurred, M, (width,height))
img2 = rotated

cv2.imshow("original image",img1)
cv2.waitKey(0)
cv2.imshow("after blurred and rotated",img2)
cv2.waitKey(0)

# 初始化SIFT算子,如果不能用,使用pip安装如下版本的库并重启conda
# pip install opencv-python == 3.4.2.16
# pip install opencv-contrib-python == 3.4.2.16
sift = cv2.xfeatures2d.SIFT_create()

# 使用SIFT算子计算特征点和特征点周围的特征向量
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)

# BFMatcher中设置knn的k值
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1,des2, k=2)

# Apply ratio test
good = []
for m,n in matches:
    if m.distance < 0.75*n.distance:
        good.append([m])

# cv.drawMatchesKnn expects list of lists as matches.
imgNew = cv2.drawMatchesKnn(img1,kp1,img2,kp2,good,None,flags=2)
# 保存图片
cv2.imwrite("imgNew.jpg",imgNew)
# 使用plt绘制
plt.imshow(imgNew),plt.show()

cv2.destroyAllWindows(

使用RANSAC算法的图像配准

假设我们有同一个地方的两张航拍图像。一张用卫星拍摄,另一张用无人机拍摄。卫星图像按年更新,无人机图像的拍摄频率要高得多。因此,存在一种情况:无人机图像能捕捉到卫星图像里没有的变化。在这种场景下,我们可能想把无人机图像准确地放在对应的卫星图像的相同地方,但是同时展示最新的变化。
这种将一张图像准确叠加在另一张图像的相同地方的过程称为图像配准(image registration).
RANSAC是进行图像配准的最佳算法,由如下四个步骤组成:
(1)特征检测与抽取。
(2)特征匹配。
(3)转换函数拟合。
(4)图像转换和图像重采样。

RANSAC算法用于在上述步骤(3)中找到转换函数。我们使用RANSAC算法找到两张图像之间的单应性(相似性)。下面简单介绍一下这种算法:
(1)随机找到两张图像之间的共同特征点,然后求单应性矩阵。
(2)重复多次,直到找到具有最多内点(inlier)的单应性矩阵。

我们在Python下应用这种算法。示例代码由三个自定义模块构成:Ransac.py、Afine.py 和 Align.py。Ransac.py包含整个RANSAC算法,Affine.py用于执行图像的旋转、平移和缩放操作,Align.py用于把图像对齐到跟原始图像完美配准
我们逐行看一下代码:
(1)首先,导入重要的程序库和刚刚提到的自定义模块。

import numpy as np
import cv2
from Ransac import *
from Affine import *
from Align import *

(2)然后,加载要童另一张图像(目标图像)配准的图像。接下来,加载目标图像。

img_source = cv2.imread("source. jpg")
img_target = cv2.imread("target. jpg")

(3)现在,我们使用Align模块里的extract_SIFT()函数提取关键点和关联的描述子(在前面解释过)。

keypoint_source, descriptor_source = extract SIFT(img _source)
keypoint_target, descriptor_target = extract SIFT(img _target)

(4)接着,使用match_SIFT()函数获取上一步找到的所有关键点的位置:

pos =  match_SIFT(descriptor_source, descriptor_target)

(5)在match_SIFT()函数中,我们试图在所有匹配的描述子中获取最佳的两个匹配。为此,使用函数BFMatcher()knnMatch()。我们来看下Align模块中的这段代码:

bf = cv2.BFMatcher()
matches = bf.knnMatch(descriptor_source, descriptor_target, k=2)

(6)必须创建一个空的NumPy数组来保存关键点的位置。我们将它命名为pos。我们只把D.Lowe的比值判别法中比值小于或等于0.8的点放到pos中。

(7)trainldx 返回来源图像中的描述子下标,而 queryIdx返回目标图像中的描述子下标。我们把这些实际位置竖直堆放在pos变量中,然后返回。

for i in range(matches_num): 
    if matches[1][0].distance <= 0.8 * matches[i][1].distance: 
        temp = np.array([matches[i][0].queryIdx, matches[1][0].trainIdx])
        pos = np.vstack((pos, temp))
return pos

(8)在取得描述子的位置之后,使用Align模块中的affine_matrix()数求得图像配准的单应性矩阵。

H = affine_matrix(keypoints_source, keypoint_target, pos)

(9)我们来看下这个函数的内部。

a.首先,根据保存在pos变量中的最佳描述子位置,将所有的关键点保存在变量s和t中。

s = s[:, pos[:, 0]]
t = t[:, pos[:, 1]]

b.然后,我们需要求出内点。内点在两张图像中呈现出最大相似性,因此能多用于选取RANSAC模型。使用ransac模块中的ransac_fit()函数获取这些关键点。

_, _, inliers = ransac_fit(s, t)

在ransac_fit()函数中,我们初始化几个变量:它们分别代表内点的数量、用于仿射变换的矩阵,还有存放内点位置的变量:

inliers_nums = 0
A = None
t = None
inliers = None

接下来,需要求出用于帮我们做仿射变换的临时矩阵,为此,我们随机生成下标,然后由这些下标抽取点作为estimate_affne()函数的参数。

idx = np.random.randint(0, pts_s.shape[1], (K, 1))
A_tmp, t_tmp = estimate_affine(pts_s[:, idx], pts_t[:, idx])

在得到这些临时矩阵之后,将它们传给residual_lenghs()函数,计算误差。误差能帮助我们确定最终的矩阵。

residual = residual_lengths(A_tmp, t_tmp, pts_s, pts_t)

有关函数residual_lengths()和estimate_affine()的解释将在下节中给出。在知道残差(residualy)误差之后,可以跟阈值做比较,我们假设阈值为1,如果残差小于阔值,则将它算到(内点的)实例里面。

inliers_tmp = np.where(residual < threshold)

然后,比较当前求得的内点和全局维护的内点。如果当前内点个数大于全局内点个数,则使用当前内点替换全局内点,并更新仿射矩阵A和t。另外,我们保存这些内点的下标。

inliers_tmps = np.where(residual < threshold)
inliers_nums_tmps = len(inliers_tmps[0])
if inliers_nums_tmps > inliers_nums:
    inliers_num = inliers_num_tmp
    inliers = inliers_tmp
    A = A_tmp
    t = t_tmp

重复这一过程2000次,得到最佳的矩阵并返回。
c.在求得内点下标之后,可以使用它们提取最佳的来源图像关键点和目标图像关键点。

s = s[:, inliers[0]]
t = t[:, inliers[0]]

d.我们把这些关键点传给estimate_affine()函数,得到最终的变换矩阵。

A, t = estimate_affine(s ,t)

e.最终,我们将A和t水平并置并将整体作为单应性矩阵返回。

M = np.hstack((A, t)
return M

(10)在求得单应性矩阵之后,我们只剩下图像配准的工作要做。为此,首先获取目标图像的行数与列数:

rows, cols, _ = img_target.shape

(11)然后在来源图像上应用单应性矩阵,并缩放到目标图像的大小:

warp = cv2.warpAffine(img_source, H, (cols, rows))

(12)现在,只需要把两张图像混合。为此,给目标图像设置50%权重,给变形后的图像(要与另一张图像混合的图像)设置50%权重:

merge = np.unit8(img_target * 0.5 + wrap * 0.5)

(13)现在,只剩下显示配准结果了。我们也可以根据需求保存图像。

cv2.imshow('img', merge)
cv2.waitkey(0)
cv2.destroyAllWindows()

estimate_affine()函数

estimate_affine()函数以来源图像和目标图像的关键点(矩阵)作为输入,返回仿射变换矩阵作为输出。首先,基于来源关键点矩阵的维度,我们初始化一个临时矩阵,往其中填充来源关键点。然后把目标关键点矩阵的维度在转置之后变形成2000x1。最后,在两个矩阵上做线性回归,并求得直线的斜率和截距,据此算出最终的矩阵X和Y。代码如下:

def estimate_affine(s, t):
    num = s.shape [1]
    M = np. zeros((2 * num, 6))
    
    for i in range(num):
        temp= [[s[0, i], s[1, i], 0, 0, 1, 0],
               [0, 0, s[0, i], s[1, i], 0, 1]
        M[2 * i: 2 * i+ 2, :] = np. array(temp)
        
    b = t.T.reshape((2 * num, 1))
    theta = np.linalg.lstsq(M, b)[0]
    X = theta[:4].reshape((2, 2))
    Y = theta[4:]
return X, Y

residual_lengths()函数

residual_lengths()函数用于确定模型中存在的误差,并确保生成的仿射矩阵和匹配的描述子的误差尽可能小。首先,我们建立仿射矩阵和来源图像关键点(矩阵)的线性模型,给出目标图像的点估计。我们通过将它们与实际的目标点做比较求得最终的误差。然后,我们将目标点与目标点的估计相减,求平方和后再求平方根以消除负值的影响。这就是均方根误差估计或残差估计。最后,我们返回这个值。这个操作的代码如下:

def residual_lengths(x, Y, s, t): 
    e = np.dot(X, s) +Y
    diff_square = np.power(e - t, 2)
    residual = np.sqrt(np.sum(diff_square, axis-0))
return residual

输出图像

多视图几何三维重建_第2张图片

全部代码

以下是图像配准的全部代码。
主代码:

# main.py
import numpy as np
import cv2
from Ransac import *
from Affine import *
from Align import *

img_source = cv2.imread("source. jpg")
img_target = cv2.imread("target. jpg")
keypoint_source, descriptor_source = extract SIFT(img _source)
keypoint_target, descriptor_target = extract SIFT(img _target)
pos =  match_SIFT(descriptor_source, descriptor_target)
H = affine_matrix(keypoints_source, keypoint_target, pos)

rows, cols, _ = img_target.shape
warp = cv2.warpAffine(img_source, H, (cols, rows))
merge = np.unit8(img_target * 0.5 + wrap * 0.5)
cv2.imshow('img', merge)
cv2.waitkey(0)
cv2.destroyAllWindows()

Ransac.py

# Ransac.py
import numpy as np
from Affine import *
K=3
threshold=1
ITER_NUM = 2000
def residual_lengths(x, Y, s, t): 
    e = np.dot(X, s) +Y
    diff_square = np.power(e - t, 2)
    residual = np.sqrt(np.sum(diff_square, axis-0))
    return residual

def ransac_fit(pts_s, pts_t):
    inliers_nums = 0
    A = None
    t = None
    inliers = None
    for i in range(ITER_NUM):
        idx = np.random.randint(0, pts_s.shape[1], (K, 1))
        A_tmp, t_tmp = estimate_affine(pts_s[:, idx], pts_t[:, idx])
        residual = residual_lengths(A_tmp, t_tmp, pts_s, pts_t)
        if not(residual is None):
            inliers_tmps = np.where(residual < threshold)
            inliers_nums_tmps = len(inliers_tmps[0])
            if inliers_nums_tmps > inliers_nums:
                inliers_num = inliers_num_tmp
                inliers = inliers_tmp
                A = A_tmp
                t = t_tmp
                else:
                    pass
    return A, t, inliers

Affine.py

# Affine.py
import numpy as np

def estimate_affine(s, t):
    num = s.shape [1]
    M = np. zeros((2 * num, 6))
    
    for i in range(num):
        temp= [[s[0, i], s[1, i], 0, 0, 1, 0],
               [0, 0, s[0, i], s[1, i], 0, 1]
        M[2 * i: 2 * i+ 2, :] = np. array(temp)
        
    b = t.T.reshape((2 * num, 1))
    theta = np.linalg.lstsq(M, b)[0]
    X = theta[:4].reshape((2, 2))
    Y = theta[4:]
    return X, Y

Align.py

# Align.py
import numpy as np
import cv2
from Ransac import *
from Affine import *

def extract_SIFT(img):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    sift = cv2.xfeatures2d.SIFT_create()
    kp, desc = sift.detectAndCompute(img_gray, None)
    kp = np.array([p.pt for p in kp]).T
    return kp, desc
def match_SIFT(descriptor_source, descriptor_target):
    bf = cv2.BFMatcher()
    matches = bf.knnMatch(descriptor_source, descriptor_target, k=2)
    pos = np.array([], dtypr=np.int32).reshape((0, 2))
    matches_num = len(matches)
    for i in range(matches_num):
        if matches[i][0].distance <= 0.8 * matches[i][1].distance:
            temp = np.array([matches[i][0].queryIdx, matches[i][0].trainIdx])
            pos = np.vstack((pos,temp))
    return pos

def affine_matrix(s, t, pose):
    s = s[:, pos[:, 0]]
    t = t[:, pos[:, 1]]
    _, _, inliers = ransac_fit(s, t)
    s = s[:, inliers[0]]
    t = t[:, inliers[0]]
    A, t = estimate_affine(s ,t)
    M = np.hstack((A, t))
    return M

你可能感兴趣的:(图像处理)