文章目录
项目介绍
将给定的如下两张图片合并,产生全景拼接的效果。
代码实现过程
1、导入需要的库
import numpy as np
import cv2
2、定义绘图函数
def cv_show(name,img):
cv.imshow(name, img)
cv.waitKey(0)
cv.destroyAllWindows()
3、读取拼接图片
imageA = cv.imread("right_01.png")
imageB = cv.imread("left_01.png")
4、检测A、B图片的SIFT关键特征点,并计算特征描述子
grayA = cv.cvtColor(imageA, cv.COLOR_BGR2GRAY)
grayB = cv.cvtColor(imageB, cv.COLOR_BGR2GRAY)
# 建立SIFT生成器
descriptor = cv.xfeatures2d.SIFT_create()
# 检测SIFT特征点,并计算描述子
kpsA, featuresA = descriptor.detectAndCompute(grayA, None)
print(np.array(kpsA).shape)
print(np.array(featuresA).shape)
kpsB, featuresB = descriptor.detectAndCompute(grayB, None)
# 将结果转换成NumPy数组
kpsA = np.float32([kpA.pt for kpA in kpsA])
kpsB = np.float32([kpB.pt for kpB in kpsB])
print(kpsA.shape)
得到898个特征点,每个特征点都有128个特征。
【注】上面提到的函数的作用是检测SIFT特征点并计算特征点的特征。对其中函数有兴趣了解的,可以参考我的另一篇文章:Opencv之特征匹配。
另外,在这个函数中的最后一步中,要将检测到的特征点的坐标转换成numpy数组的形式。即:
kps = np.float32([kp.pt for kp in kps])
注意:这条语句不可以写成:
for i, kp in enumerate(kps):
kps[i] = np.float32([kp.pt])
否则得到的是一个列表而不是数组。
5、匹配两张图片的所有特征点,返回匹配结果
暴力匹配部分代码的详细讲解同样可以在Opencv之特征匹配中找到。
值得一提的是,存储两个点的索引值时我们使用了m[0].trainIdx和m[0].queryIdx,前者表示在第一张图片中所提取出来的特征点中匹配点m所对应的索引值,后者表示在第二张图片中所提取出来的特征点中匹配点m所对应的索引值。
# 建立暴力匹配器
matcher = cv.BFMatcher()
# 使用KNN检测来自A、B图的SIFT特征匹配对,K=2
rawMatches = matcher.knnMatch(featuresA, featuresB, k=2)
print(np.array(rawMatches).shape)
matches = []
for m, n in rawMatches:
if m.distance < 0.75 * n.distance:
matches.append((m.trainIdx, m.queryIdx))
print(np.array(matches).shape)
最终得到符合条件的匹配点有148对。
6、当筛选后的匹配对大于4时,计算视角变换矩阵
对于视角变换矩阵,我将在文章末尾介绍。
if len(matches) > 4:
# 获取匹配对的点坐标
ptsA = np.float32([kpsA[i] for (_, i) in matches])
print(ptsA.shape)
ptsB = np.float32([kpsB[i] for (i, _) in matches])
# 计算视角变换矩阵
H, status = cv.findHomography(ptsA, ptsB, cv.RANSAC, 4.0)
print(H.shape)
print(status.shape)
7、将图片A进行视角变换并与B图结合
# 将图片A进行视角变换,result是变换后图片
hA, wA = imageA.shape[:2]
print((hA, wA))
hB, wB = imageB.shape[:2]
print((hB, wB))
result = cv.warpPerspective(imageA, H, (wA+wB, hA))
cv_show('result', result)
# 将图片B传入result图片最左端
result[:hB, :wB] = imageB
cv_show('result', result)
图片A进行视角变换后:
最终得到图片:
8、可视化
# 可视化
vis = np.zeros((hA, wA+wB, 3), dtype=np.uint8)
vis[0:hA, 0:wA] = imageA
vis[0:hB, wA:] = imageB
# 联合遍历,画出匹配对
for ((trainIdx, queryIdx), s) in zip(matches, status):
# 当点对匹配成功时,画到可视化图上
if s == 1:
# 画出匹配对
ptA = (int(kpsA[queryIdx][0]), int(kpsA[queryIdx][1]))
ptB = (int(kpsB[trainIdx][0]) + wA, int(kpsB[trainIdx][1]))
cv.line(vis, ptA, ptB, (0, 255, 0), 1)
cv_show("Keypoint Matches", vis)
可视化后可以得到:
计算视角变换矩阵
此视角变换矩阵也被称为单应性矩阵(H)。如下图所示,需要至少8个方程才能解出八个未知数,从而得到H矩阵。而8个方程可以通过找到4个点(每个点都有x和y两个维度)得到。
在上面的代码中,我们使用函数cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh)来得到单应性矩阵H。
函数介绍
H, status = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh)
输入参数:
- ptsA表示图像A关键点的坐标
- ptsB图像B关键点的坐标
- cv2.RANSAC指使用随机抽样一致性算法来进行迭代
- reproThresh表示每次抽取样本的个数
输出参数: - H:单应性矩阵。
- status:是布尔值,如果某点经过随机抽样一致性算法后符合要求则为1,否则为0。
PS:RANSAC算法(随机抽样一致性算法):对于左边的图,可以看到使用最小二乘法尽可能多的满足点可以分布在拟合曲线周围,减小均分根误差,因此拟合的曲线在一定程度上容易发生偏离,而RANSAC却不会出现这种情况。
RANSCA原理:因为拟合一条直线只需要两个点,因此我们每次随机选取两个点,做出直线,划定一个距离,判断落在直线周围距离范围点的个数,不断的迭代,直到找出拟合的直线,使得点落在上面最多的拟合曲线。