一、前言

最近在做视频融合项目,视频融合的第一步就是将来自不同拍摄视角的视频进行拼接,这两段视频存在画面重合的部分,因此将两这段视频拼接后可以获得更宽阔的视野。视频拼接的基础是图像拼接,所以在此记录下我学习图像拼接的笔记。

二、图像拼接流程

这里我在网上查阅了很多资料,最后总结出来如下流程:

opencv视频拼接图像_图像处理

 三、特征提取

常用的特征提取算法有SIFT算法、SURF算法和ORB算法,这里记录下我学习到的这些算法原理(没有完全理解,只记录下自己理解到的部分)。

1、SIFT算法

① 特点

SIFT的全称是尺度不变特征变换,它的特点是对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换和噪声保持一定的稳定性。

② 原理

它的原理就是找到图像的特征点,然后为每一个特征点生成能够描述它们的特征符,这样计算机就能从两张图片中找到相似的特征符并根据这些相似特征符匹配特征点。

③ 步骤

i、构建尺度空间,检测极值获取潜在特征点

这里分为三个步骤,第一是构建高斯金字塔,构建过程是将原图像进行不同程度的高斯核卷积,这里称这样一组图像为oracle,该过程是模拟人眼在不同距离下观察到的图像情况,然后再取中间的图像进行下采样,再对下采样的图像进行高斯核卷积的重复操作,这样重复几次后,可以得到一个高斯金字塔。下图来自哔站中一位博主的讲解示意图。

opencv视频拼接图像_opencv_02

第二步是构建高斯差分金字塔,就是将每个oracle中的相邻两张图像进行相减操作,以此得到高斯差分金字塔。

opencv视频拼接图像_opencv_03

最后一步是寻找极值点:从每个oracle的第二层搜索,每个像素点与它的26邻域内(包括本像素点的同层周围8邻域和上下两层的9*2邻域)的像素点相比,如果最大或最小,则记录为潜在关键点。

opencv视频拼接图像_学习_04

 ii、关键点精确定位

这里主要分为两个步骤,由于之前得到的极值点都是在离散空间下得到的,这一步是利用离散空间的极值点插值得到连续空间下的极值点。

opencv视频拼接图像_笔记_05

我理解的是,将这些离散的极值点画在一个坐标轴下,然后根据这些离散点进行曲线拟合,接着根据这个曲线函数写出它的泰勒展开式,求导并让方程f'(x)等于0,得到极值点的偏移量,当偏移量大于0.5是,需要重新迭代, 但是要注意迭代次数。

第二步是过滤噪声和边缘点,这里的原理没有太懂,先把原理搬到这里供参考。

过滤噪声:舍去低对比度的点,若|f(x)| < T/n,则舍去x;

去除边缘:利用海森矩阵,二阶海森矩阵为:

opencv视频拼接图像_学习_06

H的迹:Tr(H) = Dxx + Dyy = α + β

H的行列式:Det(H) = Dxx * Dyy -(Dxy)^2 = αβ

过滤1:若Det(H)  < 0,舍去x点

过滤2:过滤掉不满足

opencv视频拼接图像_opencv视频拼接图像_07

的值,T = 

opencv视频拼接图像_学习_08

iii、关键点主方向分配

它的作用是使关键点的描述符具有旋转不变性。

步骤:

1、把360°分为36个柱,每柱10°;

2、在高斯金字塔中找到关键点对应的位置,以它为圆心,尺度σ的1.5倍为半径画个圈;

3、统计圆中所有像素的梯度方向和梯度幅值,并用1.5σ进行高斯滤波

4、最后统计出数值最高的为主方向,并保留数值大于主方向的80%的方向作为辅方向

在获取主方向之前,为防止某个梯度方向角度收到噪声干扰等原因突变,还需要对梯度方向直方图进行平滑处理。直方图的一个柱状表示一个角度范围,这样得到的主方向或者是辅方向是一个角度区间,需要进行抛物线插值来求出主方向和辅方向的角度。

opencv视频拼接图像_opencv_09

iv、构建关键点的描述符

描述符是一组向量,用于描述特征点及其邻域点的特征,以便能更好和其他图片进行匹配。

1、将特征点附近的邻域划分问d*d个子区域,每个子区域划分为8个方向,每个子区域大小为mσ*mσ个像素

2、图像旋转θ°,让该关键点的主方向和平面直角坐标系的x轴重合

opencv视频拼接图像_学习_10

3、计算旋转后邻域范围内像素的梯度幅值和幅角,再用σ = d/2进行高斯加权,就得到了该关键点的描述符,取值d=4时,描述符是一个4*4*8=128维的向量。

opencv视频拼接图像_opencv_11


 2、代码实现(BF特征匹配算法)

① 特征点检测:

import cv2 as cv
img = cv.imread('lake1.jpg')
gray= cv.cvtColor(img,cv.COLOR_BGR2GRAY)
sift = cv.xfeatures2d.SIFT_create()
kp = sift.detect(gray,None)
img=cv.drawKeypoints(gray,kp,img)

cv.namedWindow('SIFT', cv.WINDOW_NORMAL)
cv.imshow("SIFT", img)
cv.waitKey(0)
cv.destroyAllWindows()

这里使用了一个cv.namedWindow()的方法,目的是使显示的图像与原图大小一致,因为我发现不使用这个方法显示出来的图像总是不全。

得到的效果如下:

opencv视频拼接图像_图像处理_12

原图:

opencv视频拼接图像_opencv视频拼接图像_13

 ② 特征点匹配:

这里使用SIFT算法对特征点进行检测,然后利用蛮力特征匹配的方法对特征点进行匹配:

import numpy as np
import cv2
img1_gray = cv2.imread("lake1.jpg")  # 左图
img2_gray = cv2.imread("lake2.jpg")  # 右图
# 使用SIFT算法检测特征点
sift = cv2.xfeatures2d.SIFT_create()
# 计算特征点
kp1, des1 = sift.detectAndCompute(img1_gray, None)
kp2, des2 = sift.detectAndCompute(img2_gray, None)
# 利用暴力匹配算法进行特征点匹配
bf = cv2.BFMatcher(cv2.NORM_L2)
matches = bf.knnMatch(des1, des2, k=2)  # 最相似的特征点

goodMatch = []  # 最佳匹配点集合
'''基于距离阈值选择优质匹配点对,如果最近邻m的距离小于0.65倍的次近邻n的距离,
则认为这个匹配点对是优质的,将它存储在good列表中。'''
for m, n in matches:
    if m.distance < 0.65 * n.distance:
        goodMatch.append(m)

goodMatch = np.expand_dims(goodMatch, 1)
# 绘制匹配特征点
res = cv2.drawMatchesKnn(img1_gray, kp1, img2_gray, kp2, goodMatch[:50], None, flags=2)

cv2.namedWindow('res', cv2.WINDOW_NORMAL)
cv2.resizeWindow('res', 1080, 720)
cv2.imshow('res', res)

cv2.waitKey(0)
cv2.destroyAllWindows()

代码解析:

  • BF,Brute-Force,暴力特征匹配方法,通过枚举的方式进行特征匹配,效率低,但准确率很高。
  • 匹配原理
    图像特征匹配,分别计算每张图像的特征点和描述子,然后使用第一组中的每个特征的描述子与第二组中的所有特征描述子进行匹配。
    匹配的时候是计算两个描述子之间的相似度,然后返回相似度最高的一对儿匹配点,也就是最终的匹配结果。
  • 计算相似度
    opencv提供了好几种计算方法,比如NORM_L1, NORM_L2, HAMMING1, HAMMING2等方法。其中,NORM_L1, NORM_L2主要用于SIFT、SURF的描述子的计算;HAMMING1, HAMMING2是专门用ORB算法获取的描述子的计算。
    HAMMING方法是通过二进制位来判断两个二进制串在第几位出现差异,出现差异的前面的位数越多,HANNING值就越高,就越相似。
  • BF匹配步骤
    (1)创建匹配器:bf = cv2.BFMatcher(normType[, corssCheck])
    参数normType表示相似度计算的方法,默认值是NORM_L2;
    参数crossCheck表示交叉检查,意思就是用第一组中的每个描述子和第二组的所有描述子进行匹配完后,再用第二组中的每个描述子和第一组的所有描述子进行匹配,这两步做完后,两步同时找到的相同的匹配对儿就是有效的匹配。但这个参数默认值是False,就是不开启这个功能,如果开启计算量就会增大。

(2)进行特征匹配:

        方法一:用匹配器的match方法,进行特征匹配:match = bf.match(des1, des2),就是          对两幅图的描述子进行计算,返回匹配的结果。
        方法二:调用knnMatch方法进行匹配:match = bf.knnMatch(des1, des2, k)
        参数des1,des2是描述子,就是通过SIFT\SURF\ORB等特征提取算法计算出来的描述            子;参数k表示取欧式距离最近的前k个关键点,就是计算第一组每个描述子和第二组所          有描述子之间的欧式距离,然后取距离最小的前k对儿。当k=1就和match方法的结果一          样。返回结果是一个match对象,这个对象包含了:distance, 描述子之间的距离,值越          低越好,越低表示近似度越高,或者说匹配度越高;queryIdx,表示第一个图像的描述子          的索引值,query就是查找,就是用哪副图去查找;trainIdx,表示第二幅图像的描述子的          索引值。

      (3)绘制匹配点:
        方法一对应的绘制方法:img_match = cv2.drawMatches(img1, kp1, img2, kp2, match,          outImg),就是将匹配的点用线连接到一起,这样通过人眼就能观察到哪些点进行匹配了。
        参数img1,kp1是指第一组搜索的图和其特征点,就是我们提供的、要搜索的图;参数            img2,kp2是指匹配的图及其特征点,就是比如搜索引擎从图片库中拿出来的图;参数            match就是匹配器的匹配结果;outImg是图像的输出,这里不用输出,设置位None,            我们用img_match这个对象来接受返回值。
        方法二对应的绘制方法:img_match = cv2.drawMatchesKnn(img1,kp1, img2, kp2, match)

结果:

当m.distance < 0.3 * n.distance

opencv视频拼接图像_笔记_14

当 m.distance < 0.65 * n.distance

opencv视频拼接图像_笔记_15

结果分析:使用SIFT算法检测到的特征点比较全面,但是对于相似度高的特征点匹配效果不好,容易出错,通过改变匹配对儿之间的阈值大小可以改变匹配特征点的数量,阈值越小,特征点数量越少,匹配的效果越好,越准确。

3、代码实现(FLANN特征匹配算法)

特征点匹配:

# FLANN匹配算法对特征点进行匹配
import cv2 as cv

# 读取待拼接图像
img1 = cv.imread('lake1.jpg')
img2 = cv.imread('lake2.jpg')

# 创建SIFT特征点检测
sift = cv.xfeatures2d.SIFT_create()

# 检测兴趣点并计算描述子
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)

# 使用OpenCV中的FLANN匹配算法进行特征匹配,并返回最近邻和次近邻匹配的结果
FLANN_INDEX_KDTREE = 0
indexParams = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
searchParams = dict(checks=50)
flann = cv.FlannBasedMatcher(indexParams, searchParams)
matches = flann.knnMatch(des1, des2, k=2)

good = []
for m, n in matches:
    if m.distance < 0.30 * n.distance:
        good.append(m)

# 可视化特征匹配结果,并保存
res = cv.drawMatches(img1=img1, keypoints1=kp1, img2=img2, keypoints2=kp2, matches1to2=good[:100], outImg=None,flags=2)
cv.namedWindow('FLANN', cv.WINDOW_NORMAL)
cv.resizeWindow('FLANN', 1080, 720)
cv.imshow('FLANN', res)

cv.waitKey(0)
cv.destroyAllWindows()

代码解析:

  • FLANN,最快邻近区特征匹配方法,是一种快速匹配方法,在进行批量特征匹配时,FLANN速度更快。但是FLANN使用的是邻近近似值,所以精度较差。如果我们想要精确匹配就用BF匹配方法,如果我们想要速度就用FLANN匹配方法。
  • FLANN匹配步骤:
    (1)创建FLANN匹配器:flann = cv2.FlannBasedMatcher(index_params[, search_params])
    参数index_params是一个字典,我们主要是传入要匹配的算法,有KDTREE和LSH两种算法,通常如果我们使用的是SIFT或者是SURF,就选择KDTREE匹配算法;如果使用的是ORB就选择LSH算法。如果搭配错误会报错。
    如果我们使用的匹配算法是KDTREE,就需要传入第二个参数search_params, 这个参数也是一个字典,用来指定KDTREE算法中遍历树的次数,一般情况下经验是:KDTREE的层级设置为5,搜索值设置为50,就是10倍,这样计算量相对比较少,速度比较快,准确率也相对比较高。比如:index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5), search_params = dict(checks=50),其中FLANN_INDEX_KDTREE的默认值是1,即参数填:dict(algorithm=1, trees=5), dict(checks=50)即可。
    (2)进行特征匹配:
    方法一:调用match方法进行匹配:Dmatch = flann.match(des1, des2)
    方法二:调用knnMatch方法进行匹配:Dmatch = flann.knnMatch(des1, des2, k)
    (3)绘制匹配点:
    方法一对应的绘制方法:cv2.drawMatches(img1, kp1, img2, kp2, Dmatch, outImg)
    方法二对应的绘制方法:cv2.drawMatchesKnn(img1,kp1, img2, kp2, Dmatch)

结果:

 当m.distance < 0.3 * n.distance

opencv视频拼接图像_图像处理_16

当 m.distance < 0.65 * n.distance

opencv视频拼接图像_图像处理_17