这里使用的是手机拍摄的图片进行重建,共拍摄所得8张图片(注意在拍摄时图片间必须要有视角重叠,旋转角度不能太大,拍摄张数根据自己的需求决定,如果要重建结果比较完整,拍摄图片的张数越多越好,当然了这样消耗的时间也会变多),如图为我使用的数据集。
下面分别为SFM和PMVS的重建结果。
多目三维重建可从一系列图片(可有序,可无序,本文为无序)重建出场景的三维结构,其分别需要经历特征提取、特征匹配、点云初始化、点云融合这四个步骤,程序以python-opencv为基础进行编写,其流程如图2所示。
在上面一个小结中,已经完成了多目三维重建,实现了从多张照片中恢复场景结构的功能,但是计算结果和实际坐标之间仍然存在一些误差,随着图片数量的增多,这个误差也就越来越大,BA就是用来优化这个误差的。
重投影误差:上面提到,可利用这些光束计算重投影误差。在重建过程中有两次“投影”过程:第一次投影是拍摄时进行的,其投影结果就是照片本身;第二次投影就是“重投影”,是根据之前计算出的内外参数、空间点坐标计算出的对应点的像素坐标。同一个空间点的两次投影的像素差值就是重投影误差。
算法模型:BA其本质是一个非线性优化算法,其目的是最小重投影误差,算法模型如下所示:
其中xij为需要优化的参数,BA中指的是点云坐标;f(x)为重投影误差;ρ为损失函数。
算法流程:之前实现的三维重建过程无需改动,只需要在生成点云之后加入BA过程即可。这里通过两个过程进行点云优化,一是删除重映射误差大的点,二则是BA优化过程,使用的是scipy.optimize库中的least_squares方法。
这里为SFM主函数实现代码,完整版代码在我的GitHub中,在我的项目中是通过GUI界面调用相关重建接口的,因此在使用时注意文件所在位置。
def sfm_rec():
image_dir = rec_config.image_dir + '/'
image_names = glob.glob(image_dir+'*.jpg') # 读取图片本身的名字
image_names = sorted(image_names)
with open("../project_name.txt", "r") as f:
project_name = f.read()
K = np.load('../calibration/camera_params/' + project_name + '/K.npy')
# 提取特征点、特征匹配
print('提取所有图片特征点')
keypoints_for_all, descriptors_for_all, colors_for_all = extract_feathers(image_names)
# print(colors_for_all)
print('匹配所有图片特征')
matches_for_all = match_all_feather(keypoints_for_all, descriptors_for_all)
for i in range(len(matches_for_all)):
print(len(matches_for_all[i]),end=' ')
# 初始化点云
print('\n初始化点云')
structure, correspond_struct_idx, colors, rotations, motions, projections = init_structure(K, keypoints_for_all, colors_for_all,
matches_for_all)
print("初始化点云数目:",len(structure))
# 增量方式添加剩余点云
print('增量方式添加剩余点云')
for i in tqdm(range(1, len(matches_for_all))):
# 获取第i幅图中匹配点的空间三维坐标,以及第i+1幅图匹配点的像素坐标
obj_points, img_points = get_objpoints_and_imgpoints(matches_for_all[i], correspond_struct_idx[i], structure,
keypoints_for_all[i + 1])
# solvePnPRansac得到第i+1个相机的旋转和平移
# 在python的opencv中solvePnPRansac函数的第一个参数长度需要大于7,否则会报错
# 这里对小于7的点集做一个重复填充操作,
if len(obj_points) < 7:
while len(img_points) < 7:
obj_points = np.append(obj_points, [obj_points[0]], axis=0)
img_points = np.append(img_points, [img_points[0]], axis=0)
# 得到第i+1幅图相机的旋转向量和位移矩阵
_, r, T, _ = cv2.solvePnPRansac(obj_points, img_points, K, np.array([]))
R, _ = cv2.Rodrigues(r) # 将旋转向量转换为旋转矩阵
rotations.append(R)
motions.append(T)
# 根据R T进行重建
p1, p2 = get_matched_points(keypoints_for_all[i], keypoints_for_all[i + 1], matches_for_all[i])
c1, c2 = get_matched_colors(colors_for_all[i], colors_for_all[i + 1], matches_for_all[i])
new_structure, new_proj = reconstruction(K, rotations[i], motions[i], R, T, p1, p2)
projections.append(new_proj)
# 点云融合
structure, colors, correspond_struct_idx[i], correspond_struct_idx[i + 1] = fusion_structure(matches_for_all[i],
correspond_struct_idx[
i],
correspond_struct_idx[
i + 1],
structure,
new_structure,
colors, c1)
print("新生成点云数" ,len(new_structure) , "第" , i , "次融合后点云数" , len(structure))
print(len(structure))
# BA优化
print('删除误差较大的点')
structure = delete_error_point(rotations, motions, K, correspond_struct_idx, keypoints_for_all, structure)
# 由于经过bundle_adjustment的structure,会产生一些空的点(实际代表的意思是已被删除)
# 修改各图片中关键点的索引
# 修改点云中的None为 -1
for i in range(len(structure)):
if math.isnan(structure[i][0]):
structure[i] = -1
# 修改各图片中的索引
for a in range(len(correspond_struct_idx)):
for b in range(len(correspond_struct_idx[a])):
if correspond_struct_idx[a][b] != -1:
if structure[int(correspond_struct_idx[a][b])][0] == -1:
correspond_struct_idx[a][b] = -1
else:
correspond_struct_idx[a][b] -= (np.sum(structure[:int(correspond_struct_idx[a][b])] == -1)/3)
i = 0
# 删除那些为空的点
while i < len(structure):
if structure[i][0] == -1:
structure = np.delete(structure, i, 0)
colors = np.delete(colors, i, 0)
i -= 1
i += 1
reproject_error(rotations, motions, K, correspond_struct_idx, keypoints_for_all, structure)
print('BA优化')
motions = np.array(motions)
rotations = np.array(rotations)
structure = bundle_adjustment(structure, correspond_struct_idx, motions, rotations, keypoints_for_all, K)
reproject_error(rotations, motions, K, correspond_struct_idx, keypoints_for_all, structure)
#保存Bundle.rd.out
print("点云已生成,正在保存.out文件")
# 旋转向量转化为旋转矩阵
Rotations = np.empty((len(rotations), 3, 3))
for i in range(len(rotations)):
R, _ = cv2.Rodrigues(rotations[i])
Rotations[i] = R
save_bundle_rd_out(structure, K, Rotations, motions, colors, correspond_struct_idx, keypoints_for_all)
np.save(image_dir + 'Structure', structure)
np.save(image_dir + 'Colors', colors)
np.save(image_dir + 'Projections', projections)