目录
1.基本介绍
2.RANSAC
3.单应性矩阵估计
4.全景图像
引言
众所周知,在我们拍摄风光摄影时,广角镜头是使用频率最高的镜头,特别是拍摄那些波澜壮阔的大场景风光。而且镜头可谓是越广越好,恨不得将眼前看到的所有美景都纳入到画面中去。这个时候,常规的超广角16mm焦段就有点不够用了,所以催生了14mm、12mm甚至更广的超广角镜头产生。但是这类超广角镜头,通常都是“灯泡头”的设计,为风光滤镜的使用增加了难度和成本负担。而且,即使是如此之广的镜头,也不完全能够满足我们的需求。因此,我们需要“曲线救国”,使用一个名为 “全景拼接” 的技术来达到我们的目的。
图像拼接技术就是将数张有重叠部分的图像(可能是不同时间、不同视角或者不同传感器获得的)拼成一幅无缝的全景图或高分辨率图像的技术。图像拼接在医学成像、计算机视觉、卫星数据、军事目标自动识别等领域具有重要意义。图像拼接的输出是两个输入图像的并集。图像配准(image alignment)和图像融合是图像拼接的两个关键技术。图像配准是图像融合的基础,而且图像配准算法的计算量一般非常大,因此图像拼接技术的发展很大程度上取决于图像配准技术的创新。早期的图像配准技术主要采用点匹配法,这类方法速度慢、精度低,而且常常需要人工选取初始匹配点,无法适应大数据量图像的融合。图像拼接的方法很多,不同的算法步骤会有一定差异,但大致的过程是相同的。
基础流程
① 针对某个场景拍摄多张/序列图像
② 计算第二张图像与第一张图像之间的变换关 系
③ 将第二张图像叠加到第一张图像的坐标系中
④ 变换后的融合/合成
⑤ 在多图场景中,重复上述过程
RANSAC 是“RANdom SAmple Consensus”(随机一致性采样)的缩写。该方法是用来找到正确模型来拟合带有噪声数据的迭代方法。给定一个模型,例如点集之间的单应性矩阵,RANSAC 基本的思想是,数据中包含正确的点和噪声点,合理的模型应该能够在描述正确数据点的同时摒弃噪声点。
举个简单的例子,假设观测数据中包含局内点和局外点,其中局内点近似的被直线所通过,而局外点远离于直线。简单的最小二乘法不能找到适应于局内点的直线,原因是最小二乘法尽量去适应包括局外点在内的所有点。相反,RANSAC能得出一个仅仅用局内点计算出模型,并且概率还足够高
算法基本思想和流程
RANSAC是通过反复选择数据集去估计出模型,一直迭代到估计出认为比较好的模型。
具体的实现步骤可以分为以下几步:
RANSAC的优点是它能鲁棒的估计模型参数。例如,它能从包含大量局外点的数据集中估计出高精度的参数。RANSAC的缺点是它计算参数的迭代次数没有上限;如果设置迭代次数的上限,得到的结果可能不是最优的结果,甚至可能得到错误的结果
运行结果
单应性(Homography)变换。可以简单的理解为它用来描述物体在世界坐标系和像素坐标系之间的位置映射关系。对应的变换矩阵称为单应性矩阵。
单应性在计算机视觉领域是一个非常重要的概念,它在图像校正、图像拼接、相机位姿估计、视觉SLAM等领域有非常重要的作用。
什么是齐次坐标系?为什么要用齐次坐标系? - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/373969867
单应性Homograph估计:从传统算法到深度学习 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/74597564
单应矩阵描述的就是同一个平面的点在不同图像之间的映射关系
代码实践---更换广告牌
import cv2 as cv
import numpy as np
# 鼠标操作,鼠标选中源图像中需要替换的位置信息
def mouse_action(event, x, y, flags, replace_coordinate_array):
cv.imshow('collect coordinate', img_dest_copy)
if event == cv.EVENT_LBUTTONUP:
# 画圆函数,参数分别表示原图、坐标、半径、颜色、线宽(若为-1表示填充)
# 这个是为了圈出鼠标点击的点
cv.circle(img_dest_copy, (x, y), 2, (0, 255, 255), -1)
# 用鼠标单击事件来选择坐标
# 将选中的四个点存放在集合中,在收集四个点时,四个点的点击顺序需要按照 img_src_coordinate 中的点的相对位置的前后顺序保持一致
print(f'{x}, {y}')
replace_coordinate_array.append([x, y])
if __name__ == '__main__':
# 首先,加载待替换的源图像,并获得该图像的长度等信息,cv.IMREAD_COLOR 表示加载原图
img_src = cv.imread('../images/JMU.jpg', cv.IMREAD_COLOR)
h, w, c = img_src.shape
# 获得图像的四个边缘点的坐标
img_src_coordinate = np.array([[x, y] for x in (0, w - 1) for y in (0, h - 1)])
print(img_src_coordinate)
# cv.imshow('replace', replace)
print("===========================")
# 加载目标图像
img_dest = cv.imread('../images/img_dest.webp', cv.IMREAD_COLOR)
# 将源数据复制一份,避免后来对该数据的操作会对结果有影响
img_dest_copy = np.tile(img_dest, 1)
# 源图像中的数据
# 定义一个数组,用来存放要源图像中要替换的坐标点,该坐标点由鼠标采集得到
replace_coordinate = []
cv.namedWindow('collect coordinate')
cv.setMouseCallback('collect coordinate', mouse_action, replace_coordinate)
while True:
# 当采集到四个点后,可以按esc退出鼠标采集行为
if cv.waitKey(20) == 27:
break
print(replace_coordinate)
replace_coordinate = np.array(replace_coordinate)
# 根据选中的四个点坐标和代替换的图像信息完成单应矩阵
matrix, mask = cv.findHomography(img_src_coordinate, replace_coordinate, 0)
print(f'matrix: {matrix}')
perspective_img = cv.warpPerspective(img_src, matrix, (img_dest.shape[1], img_dest.shape[0]))
cv.imshow('img', perspective_img)
# cv.imshow('threshold', threshold_img)
# 降噪,去掉最大或最小的像素点
retval, threshold_img = cv.threshold(perspective_img, 0, 255, cv.THRESH_BINARY)
# 将降噪后的图像与之前的图像进行拼接
cv.copyTo(src=threshold_img, mask=np.tile(threshold_img, 1), dst=img_dest)
cv.copyTo(src=perspective_img, mask=np.tile(perspective_img, 1), dst=img_dest)
cv.imshow('result', img_dest)
cv.waitKey()
cv.destroyAllWindows()
运行结果
书本上样例代码
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
featname = ['../images/chp3/sift/JMU' + str(i + 1) + '.sift' for i in range(5)]
imname = ['../images/chp3/JMU' + str(i + 1) + '.jpg' for i in range(5)]
# 显示5张校园照片
plt.figure(figsize=(15, 8))
for i in range(5):
im = np.array(Image.open(imname[i]))
plt.subplot(2, 3, i + 1)
plt.imshow(im)
plt.title('Univ' + str(i + 1) + '.jpg')
plt.axis('off')
plt.show()
class RansacModel(object):
""" Class for testing homography fit with ransac.py from
http://www.scipy.org/Cookbook/RANSAC"""
def __init__(self,debug=False):
self.debug = debug
def fit(self, data):
""" Fit homography to four selected correspondences. """
# transpose to fit H_from_points()
data = data.T
# from points
fp = data[:3,:4]
# target points
tp = data[3:,:4]
# fit homography and return
return H_from_points(fp,tp)
def get_error( self, data, H):
""" Apply homography to all correspondences,
return error for each transformed point. """
data = data.T
# from points
fp = data[:3]
# target points
tp = data[3:]
# transform fp
fp_transformed = dot(H,fp)
# normalize hom. coordinates
fp_transformed = normalize(fp_transformed)
# return error per point
return sqrt( sum((tp-fp_transformed)**2,axis=0) )
def H_from_ransac(fp,tp,model,maxiter=1000,match_theshold=10):
""" Robust estimation of homography H from point
correspondences using RANSAC (ransac.py from
http://www.scipy.org/Cookbook/RANSAC).
input: fp,tp (3*n arrays) points in hom. coordinates. """
import ransac
# group corresponding points
data = vstack((fp,tp))
# compute H and return
H,ransac_data = ransac.ransac(data.T,model,4,maxiter,match_theshold,10,return_all=True)
return H,ransac_data['inliers']
def convert_points(j):
ndx = matches[j].nonzero()[0]
fp = homography.make_homog(l[j+1][ndx,:2].T)
ndx2 = [int(matches[j][i]) for i in ndx]
tp = homography.make_homog(l[j][ndx2,:2].T)
return fp,tp
model = homography.RansacModel()
fp,tp = convert_points(1)
H_12 = homography.H_from_ransac(fp,tp,model)[0]
fp,tp = convert_points(0)
H_01 = homography.H_from_ransac(fp,tp,model)[0]
tp,fp = convert_points(2)
H_32 = homography.H_from_ransac(fp,tp,model)[0]
tp,fp = convert_points(3)
H_43 = homography.H_from_ransac(fp,tp,model,match_threshold=3000)[0]
估计出图像间的单应性矩阵(使用 RANSAC 算法),现在我们需要将所有的图像扭 曲到一个公共的图像平面上。通常,这里的公共平面为中心图像平面(否则,需要 进行大量变形)。一种方法是创建一个很大的图像,比如图像中全部填充 0,使其和 中心图像平行,然后将所有的图像扭曲到上面。由于我们所有的图像是由照相机水平 旋转拍摄的,因此我们可以使用一个较简单的步骤:将中心图像左边或者右边的区域填充0,以便为扭曲的图像腾出空间。
def panorama(H,fromim,toim,padding=2400,delta=2400):
""" Create horizontal panorama by blending two images
using a homography H (preferably estimated using RANSAC).
The result is an image with the same height as toim. 'padding'
specifies number of fill pixels and 'delta' additional translation. """
# check if images are grayscale or color
is_color = len(fromim.shape) == 3
# homography transformation for geometric_transform()
def transf(p):
p2 = dot(H,[p[0],p[1],1])
return (p2[0]/p2[2],p2[1]/p2[2])
if H[1,2]<0: # fromim is to the right
print('warp - right')
# transform fromim
if is_color:
# pad the destination image with zeros to the right
toim_t = hstack((toim,zeros((toim.shape[0],padding,3))))
fromim_t = zeros((toim.shape[0],toim.shape[1]+padding,toim.shape[2]))
for col in range(3):
fromim_t[:,:,col] = ndimage.geometric_transform(fromim[:,:,col],
transf,(toim.shape[0],toim.shape[1]+padding))
else:
# pad the destination image with zeros to the right
toim_t = hstack((toim,zeros((toim.shape[0],padding))))
fromim_t = ndimage.geometric_transform(fromim,transf,
(toim.shape[0],toim.shape[1]+padding))
else:
print('warp - left')
# add translation to compensate for padding to the left
H_delta = array([[1,0,0],[0,1,-delta],[0,0,1]])
H = dot(H,H_delta)
# transform fromim
if is_color:
# pad the destination image with zeros to the left
toim_t = hstack((zeros((toim.shape[0],padding,3)),toim))
fromim_t = zeros((toim.shape[0],toim.shape[1]+padding,toim.shape[2]))
for col in range(3):
fromim_t[:,:,col] = ndimage.geometric_transform(fromim[:,:,col],
transf,(toim.shape[0],toim.shape[1]+padding))
else:
# pad the destination image with zeros to the left
toim_t = hstack((zeros((toim.shape[0],padding)),toim))
fromim_t = ndimage.geometric_transform(fromim,
transf,(toim.shape[0],toim.shape[1]+padding))
# blend and return (put fromim above toim)
if is_color:
# all non black pixels
alpha = ((fromim_t[:,:,0] * fromim_t[:,:,1] * fromim_t[:,:,2] ) > 0)
for col in range(3):
toim_t[:,:,col] = fromim_t[:,:,col]*alpha + toim_t[:,:,col]*(1-alpha)
else:
alpha = (fromim_t > 0)
toim_t = fromim_t*alpha + toim_t*(1-alpha)
return toim_t
im1 = np.array(Image.open(imname[1]))
delta = im1.shape[1] # 領域追加と水平移動量
im2 = np.array(Image.open(imname[2]))
im_12 = warp.panorama(H_12,im1,im2,delta,delta)
im1 = np.array(Image.open(imname[0]))
im_02 = warp.panorama(np.dot(H_12,H_01),im1,im_12,delta,delta)
im1 = np.array(Image.open(imname[3]))
im_32 = warp.panorama(H_32,im1,im_02,delta,delta)
im1 = np.array(Image.open(imname[4]))
im_42 = warp.panorama(np.dot(H_32,H_43),im1,im_32,delta,2*delta)
不懂啥意思 但是最后结果还是出来了
运行结果
我们还是可以看到明显的拼接缝,这是由于拍摄图片的曝光程度有所不同而造成的 ,同时,由于 这组照片,角度的方位变化有些大,所以效果还不是很好。
后续多拍几组照片进行测试,暂时先这样吧!
RANSAC算法详解(附Python拟合直线模型代码) - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/62238520