简介:这份资源围绕双目视觉系统下的三维人脸重建展开,面向计算机视觉、图像处理方向的学习者与研究人员,尤其适合正在接触立体匹配、点云生成与特征配准的读者。内容以《现代电子技术》2015年刊载的论文为基础,系统梳理了从双目图像采集、GrabCut人脸分割、NCC区域匹配求视差图,到三维点云构建及SIFT特征粗配准的完整技术链路,可帮助读者理解双目视觉获取深度信息并重建人脸几何模型的核心思路。资源包共1个docx文件,约20KB,为论文全文文档,便于直接阅读与引用。目前已有172人学习下载,适合作为三维重建入门参考或相关课题的文献资料,在虚拟现实、人脸识别、医学成像等应用场景中具备一定借鉴价值。
1. 双目视觉做三维人脸重建:从左右图到点云粗配准的完整链路
用两个摄像头拍下人脸左右视图,经过极线校正、GrabCut 分割、NCC 区域匹配拿到视差图,再换算成三维点云,最后用 SIFT 特征把不同角度的点云粗配准到一起——这套流程出自《现代电子技术》2015 年的一篇论文,作者是江南大学物联网工程学院的隋巧燕、董洪伟和刘蕾。它解决的核心问题是:在低成本双目硬件上,如何把一张二维人脸照片变成带深度信息的三维点云,并且让正面和侧面两个角度扫出来的点云能对齐。适合做人脸识别、虚拟试妆、医学面部扫描、动作捕捉预研的工程师,也适合想搞懂双目三角测距和点云配准之间怎么衔接的学生。整套方案依赖 OpenCV 2.1,硬件就是两台罗技 Pro9000 加一块黑白棋盘格标定板,门槛不高,但每一步都有容易翻车的地方。
2. 双目系统搭建与极线校正:标定误差 0.377542 是怎么来的
2.1 为什么光轴距离要尽量短
双目视觉的几何基础是三角测距。两个相机从不同位置拍同一个点,该点在左右图像上的投影位置存在水平差异,这个差异就是视差。视差越大,说明物体越近;视差越小,物体越远。深度计算公式是 d = b·f / (I_L - I_R),其中 b 是基线长度(两相机光轴之间的距离),f 是焦距,I_L - I_R 就是视差。
论文里特别强调了一点:两个相机的光轴距离越短越好。原因在于,当基线很短时,左右图像中对应点的位置差异主要表现为水平方向的平移,垂直方向的偏移可以忽略不计。这样一来,后续做立体匹配时,只需要在水平方向上搜索对应点,搜索空间从二维降到了一维。如果基线太长,左右视图的透视差异会变大,同一个空间点在两幅图中的邻域外观可能完全不同,NCC 这类基于灰度相似度的匹配算法就会大面积失效。
实际操作中,我一般会把两个相机固定在一条铝型材导轨上,间距控制在 60~80 mm,模拟人眼瞳距。相机要选同型号、同焦距的,最好固件版本也一致,否则内参差异太大会让标定变得很痛苦。
2.2 立体标定与极线校正的实操步骤
标定是整个流程的地基。论文中使用的标定误差是 0.377542 像素,这个精度在 2015 年的低成本设备上算是可用水平。标定板是黑白棋盘格,常见规格是 9×6 格、方格边长 25 mm 或 30 mm。
import cv2 import numpy as np # 棋盘格参数:内角点数量(列数-1, 行数-1) pattern_size = (8, 5) square_size = 25.0 # 毫米 # 生成棋盘格三维坐标 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp *= square_size # 分别采集左右相机的棋盘格角点 # left_imgs, right_imgs 是两组同步拍摄的标定图路径列表 obj_points = [] # 三维点 img_points_l = [] # 左图角点 img_points_r = [] # 右图角点 for fname_l, fname_r in zip(left_imgs, right_imgs): img_l = cv2.imread(fname_l, cv2.IMREAD_GRAYSCALE) img_r = cv2.imread(fname_r, cv2.IMREAD_GRAYSCALE) ret_l, corners_l = cv2.findChessboardCorners(img_l, pattern_size) ret_r, corners_r = cv2.findChessboardCorners(img_r, pattern_size) if ret_l and ret_r: # 亚像素精化 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_l = cv2.cornerSubPix(img_l, corners_l, (11, 11), (-1, -1), criteria) corners_r = cv2.cornerSubPix(img_r, corners_r, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points_l.append(corners_l) img_points_r.append(corners_r) # 单目标定 ret_l, K_l, D_l, _, _ = cv2.calibrateCamera(obj_points, img_points_l, img_l.shape[::-1], None, None) ret_r, K_r, D_r, _, _ = cv2.calibrateCamera(obj_points, img_points_r, img_r.shape[::-1], None, None) # 双目标定 ret, K_l, D_l, K_r, D_r, R, T, E, F = cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, K_l, D_l, K_r, D_r, img_l.shape[::-1], flags=cv2.CALIB_FIX_INTRINSIC ) # 立体校正 R_l, R_r, P_l, P_r, Q, roi_l, roi_r = cv2.stereoRectify( K_l, D_l, K_r, D_r, img_l.shape[::-1], R, T, alpha=0 ) # 生成映射表 map_lx, map_ly = cv2.initUndistortRectifyMap(K_l, D_l, R_l, P_l, img_l.shape[::-1], cv2.CV_32FC1) map_rx, map_ry = cv2.initUndistortRectifyMap(K_r, D_r, R_r, P_r, img_r.shape[::-1], cv2.CV_32FC1)这段代码的逻辑是:先分别对左右相机做单目标定,拿到各自的内参矩阵 K 和畸变系数 D;再用 stereoCalibrate 求两个相机之间的旋转矩阵 R 和平移向量 T;最后用 stereoRectify 计算校正变换,让左右图像的极线变成水平对齐的。校正之后,同一个空间点在左右图中的 y 坐标应该基本一致,搜索对应点时只需要沿水平方向找。
参数方面,alpha=0表示校正后图像不做额外裁剪,保留所有像素;如果边缘畸变严重,可以改成alpha=1让 OpenCV 自动裁剪。CALIB_FIX_INTRINSIC标志表示双目标定时不优化已经标定好的内参,适合单目标定结果已经比较可靠的情况。
提示:标定图至少采集 15~20 组,棋盘格要覆盖画面各个区域,包括四个角和中心。标定误差超过 0.5 像素时,后续视差图的噪点会明显增多。
3. GrabCut 分割与 NCC 视差计算:把搜索范围压下来
3.1 GrabCut 为什么能加速立体匹配
人脸只占整幅图像的一部分,背景区域做立体匹配纯属浪费。论文的思路是先用 GrabCut 把人脸分割出来,得到一个掩码图,掩码值为 1 的区域才做匹配,值为 0 的直接跳过。这样搜索范围大幅缩小,匹配时间自然就降下来了。
GrabCut 的核心是把分割问题转化成图割问题。图像被建模成一个无向图 G=(V,E),每个像素是一个顶点,相邻像素之间连边。再加两个特殊顶点:源点 S 代表前景,汇点 T 代表背景。分割就是给每个像素分配前景或背景标签,使得能量函数最小。能量函数由两部分组成:区域项 R(f) 衡量像素颜色和前景/背景模型的相似度,边界项 B(f) 惩罚相邻像素标签不一致的情况。最终通过最大流最小割定理求解。
相比早期的 Graph Cut,GrabCut 用高斯混合模型(GMM)代替直方图来估计颜色分布,只需要画一个矩形框就能完成初始化,交互成本低了很多。
import cv2 import numpy as np img = cv2.imread('face_left.jpg') mask = np.zeros(img.shape[:2], np.uint8) # 定义矩形框:左上角坐标和宽高,框内是可能的前景 rect = (80, 50, 400, 450) # 根据实际人脸位置调整 bgd_model = np.zeros((1, 65), np.float64) fgd_model = np.zeros((1, 65), np.float64) # 迭代 5 次 cv2.grabCut(img, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT) # mask 中 0 和 2 是背景,1 和 3 是前景 mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8') face = img * mask2[:, :, np.newaxis] cv2.imwrite('face_segmented.png', face)矩形框的位置直接决定分割质量。框太小人脸会被切掉一部分,框太大背景混进来太多。我一般先用 Haar 级联或 DNN 人脸检测器定位人脸,再把人脸框向外扩 20~30 像素作为 GrabCut 的输入矩形。迭代次数设为 5 是论文中的取值,实际用的时候 3~5 次基本收敛,再多收益不大。
3.2 NCC 区域匹配与三个约束条件
拿到分割掩码之后,在掩码为 1 的区域里做 NCC 匹配。NCC 即归一化互相关,衡量的是两个窗口内灰度分布的相似程度,对光照变化有一定鲁棒性。公式本身不复杂:两个窗口内像素灰度减去各自均值后做相关运算,结果归一化到 [-1, 1],值越接近 1 说明越相似。
论文给了 NCC 匹配的三个约束条件,这三个条件在实际编码时非常关键:
平滑约束:一个像素的视差值和它周围邻居的视差值应该是连续的。具体做法是在 5×5 窗口内,超过一半的邻居视差值和中心点差异不超过 1 个像素。不满足这个条件的匹配点直接丢弃。
唯一性约束:双向搜索。先从左图角点在右图中找最相关的点,再从右图角点在左图中找最相关的点,只有双向都互相匹配上的才作为候选。这一步能过滤掉大量误匹配。
有序性约束:左图中两个点的左右顺序,在右图中应该保持一致。公式表达为 d(p) ≤ d(q) + 1,其中 p 在 q 的左边。违反这个约束的匹配对说明出现了交叉,不可靠。
def ncc_match(left_patch, right_patch): """计算两个窗口的 NCC 值""" left_mean = np.mean(left_patch) right_mean = np.mean(right_patch) left_centered = left_patch - left_mean right_centered = right_patch - right_mean numerator = np.sum(left_centered * right_centered) denominator = np.sqrt(np.sum(left_centered**2) * np.sum(right_centered**2)) if denominator < 1e-6: return 0 return numerator / denominator def compute_disparity(left_img, right_img, mask, win_size=15, max_disparity=64): """在掩码区域内计算视差图""" h, w = left_img.shape half = win_size // 2 disparity = np.zeros((h, w), np.float32) for y in range(half, h - half): for x in range(half, w - half): if mask[y, x] == 0: continue # 背景区域跳过 left_patch = left_img[y-half:y+half+1, x-half:x+half+1] best_ncc = -1 best_d = 0 for d in range(0, min(max_disparity, x - half)): right_patch = right_img[y-half:y+half+1, x-half-d:x+half+1-d] score = ncc_match(left_patch.astype(np.float32), right_patch.astype(np.float32)) if score > best_ncc: best_ncc = score best_d = d disparity[y, x] = best_d return disparity窗口大小win_size是个需要权衡的参数。窗口太小,NCC 对噪声敏感,视差图会出现大量散斑;窗口太大,人脸边缘和鼻梁两侧的视差会被平滑掉,深度不连续的地方出现模糊。论文没有明确给出窗口尺寸,但根据人脸区域的特征密度,15×15 到 21×21 是比较常见的取值。max_disparity根据基线和最近拍摄距离来定,人脸场景一般 64 足够了。
3.3 金字塔模型:从低分辨率到高分辨率的逐层细化
直接在 640×480 的原图上做全搜索匹配,计算量很大。论文用了三层金字塔:第 2 层 160×120,第 1 层 320×240,第 0 层 640×480。匹配从最顶层开始,得到的视差图作为下一层的初值,下一层只需要在初值附近一个小范围内搜索,搜索范围从max_disparity缩小到±t(t 是预设值,通常取 2~4)。
具体映射关系是:第 M-1 层某个位置 (x, y) 的视差为 d,映射到第 M 层时对应 2×2 的区域,每个位置的初始视差为 2d,搜索范围为 (2d-t, 2d+t)。这样逐层传递,既降低了计算量,又因为低分辨率层的匹配结果相对稳定,减少了高分辨率层误匹配的概率。
def build_pyramid(img, levels=3): """构建图像金字塔""" pyramid = [img] for i in range(1, levels): img = cv2.pyrDown(img) pyramid.append(img) return pyramid[::-1] # 从最小到最大排列 def pyramid_disparity(left_img, right_img, mask, levels=3, t=3): """金字塔逐层匹配""" left_pyr = build_pyramid(left_img, levels) right_pyr = build_pyramid(right_img, levels) mask_pyr = build_pyramid(mask, levels) disparity = None for level in range(levels): if disparity is None: # 最顶层:全搜索 disparity = compute_disparity(left_pyr[level], right_pyr[level], mask_pyr[level], max_disparity=16) else: # 非顶层:在上层视差指导下搜索 h, w = left_pyr[level].shape upsampled = cv2.resize(disparity, (w, h), interpolation=cv2.INTER_NEAREST) * 2 disparity = guided_disparity(left_pyr[level], right_pyr[level], mask_pyr[level], upsampled, t) return disparity金字塔层数不是越多越好。论文取 N=3,对应 640×480 的输入,最顶层 160×120 已经能看出人脸的大致轮廓了。如果输入分辨率更高,比如 1280×960,可以加到 4 层。层数太多的话,最顶层的人脸信息太少,初始视差就不准,反而会拖累后续层。
4. 从视差图到三维点云:深度换算与 SIFT 粗配准
4.1 视差转深度与点云生成
有了视差图,深度值通过 d = b·f / (I_L - I_R) 就能算出来。b 是标定得到的基线长度,f 是校正后的焦距(从 P_l 矩阵中取),I_L - I_R 就是视差值。每个像素的深度知道后,结合像素坐标和相机内参,就能反投影出三维坐标。
def disparity_to_pointcloud(disparity, Q): """利用重投影矩阵 Q 将视差图转为三维点云""" points_3d = cv2.reprojectImageTo3D(disparity, Q) # 过滤无效点(视差为 0 或深度异常) mask = disparity > 0 mask &= points_3d[:, :, 2] < 2000 # 深度超过 2 米视为噪声 mask &= points_3d[:, :, 2] > 100 # 太近的也过滤 points = points_3d[mask] colors = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)[mask] return points, colorsQ矩阵是 stereoRectify 的输出,包含了基线、焦距和主点信息。reprojectImageTo3D内部做的就是三角测距。过滤条件里的深度范围要根据实际拍摄距离调整,人脸拍摄一般在 400~800 mm 之间,超出这个范围的深度值基本是匹配错误产生的噪点。
4.2 SIFT 特征提取与 RANSAC 去误匹配
不同角度拍到的两组点云要配准,论文的做法是在二维图像上用 SIFT 提取特征点并匹配,然后把匹配关系映射到三维点云上。这样做的好处是 SIFT 在二维图像上的匹配技术非常成熟,比直接在三维点云上找对应点要稳定。
SIFT 特征点的提取包括四个步骤:尺度空间极值检测、关键点定位、方向分配、描述子生成。最终每个特征点得到一个 128 维的描述向量,对尺度、旋转、亮度变化都不变。匹配时用欧氏距离衡量两个描述子的相似度,距离越小越可能是同一个点。
def sift_match(img1, img2, ratio=0.75): """SIFT 特征提取与匹配""" sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(img1, None) kp2, des2 = sift.detectAndCompute(img2, None) # FLANN 匹配器 index_params = dict(algorithm=1, trees=5) # KDTree search_params = dict(checks=50) flann = cv2.FlannBasedMatcher(index_params, search_params) matches = flann.knnMatch(des1, des2, k=2) # Lowe's ratio test good = [] for m, n in matches: if m.distance < ratio * n.distance: good.append(m) # RANSAC 进一步去除误匹配 if len(good) > 10: src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) M, mask_ransac = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) good = [g for g, flag in zip(good, mask_ransac.ravel()) if flag == 1] return kp1, kp2, goodLowe's ratio test 中的ratio=0.75是经验值。比值越小,保留的匹配越可靠但数量越少;比值越大,匹配数量多但误匹配也多。人脸场景纹理相对丰富,0.75 通常能保留足够的匹配点。RANSAC 的重投影阈值设为 5.0 像素,这个值太大会放过误匹配,太小会把正确匹配也滤掉。
论文在实验中发现,即使经过 RANSAC 过滤,仍然存在少量误匹配。作者的建议是后续用匹配点之间的欧氏距离中值或高斯分布来进一步筛选参考点。这个思路在实际工程中很实用:先算所有匹配点对的距离,取中值,然后只保留距离在中值 1.5 倍以内的匹配对。
4.3 四元数法求旋转平移矩阵
拿到三维对应点之后,需要求一个旋转矩阵 R 和一个平移向量 T,使得源点云经过变换后和参考点云对齐。论文用的是四元数法,相比 SVD 分解,四元数法在数值稳定性上更好,而且不会出现万向节锁的问题。
四元数法的核心步骤是:先计算两个点云的重心,然后求协方差矩阵,由协方差矩阵构造一个 4×4 的对称矩阵 Q,求 Q 的最大特征值对应的特征向量,这个特征向量就是最佳旋转四元数。平移向量由两个重心和旋转矩阵直接算出。
def quaternion_registration(src_points, dst_points): """四元数法粗配准""" # 计算重心 centroid_src = np.mean(src_points, axis=0) centroid_dst = np.mean(dst_points, axis=0) # 去中心化 src_centered = src_points - centroid_src dst_centered = dst_points - centroid_dst # 协方差矩阵 Sigma = src_centered.T @ dst_centered / len(src_points) # 构造对称矩阵 trace = np.trace(Sigma) delta = np.array([Sigma[1,2] - Sigma[2,1], Sigma[2,0] - Sigma[0,2], Sigma[0,1] - Sigma[1,0]]) Q = np.zeros((4, 4)) Q[0, 0] = trace Q[0, 1:] = delta Q[1:, 0] = delta Q[1:, 1:] = Sigma + Sigma.T - trace * np.eye(3) # 特征分解 eigenvalues, eigenvectors = np.linalg.eigh(Q) q = eigenvectors[:, np.argmax(eigenvalues)] # 最大特征值对应的特征向量 # 四元数转旋转矩阵 q0, q1, q2, q3 = q R = np.array([ [q0**2 + q1**2 - q2**2 - q3**2, 2*(q1*q2 - q0*q3), 2*(q1*q3 + q0*q2)], [2*(q1*q2 + q0*q3), q0**2 - q1**2 + q2**2 - q3**2, 2*(q2*q3 - q0*q1)], [2*(q1*q3 - q0*q2), 2*(q2*q3 + q0*q1), q0**2 - q1**2 - q2**2 + q3**2] ]) # 平移向量 T = centroid_dst - R @ centroid_src return R, T这个函数返回的 R 和 T 可以直接作用于源点云:aligned = (R @ src_points.T).T + T。论文中这套粗配准的结果是为后续 ICP 精细配准做铺垫,粗配准把两个点云拉到一个大致对齐的位置,ICP 再在这个基础上做迭代优化,收敛速度和最终精度都会好很多。
5. 避坑与排查:双目人脸重建中最容易翻车的五个地方
5.1 视差图出现大面积黑色空洞
现象:生成的视差图在额头、脸颊等平滑区域出现大片零值,点云稀疏不完整。
原因:NCC 匹配依赖窗口内的灰度变化,人脸额头和脸颊的纹理非常少,窗口内像素灰度几乎一致,NCC 分母趋近于零,匹配得分不稳定,被唯一性或平滑约束过滤掉了。
解决:在平滑区域适当放大匹配窗口,或者改用 Census 变换等基于相对灰度排序的匹配代价,对无纹理区域更鲁棒。另一个办法是在人脸表面投影散斑图案,人为增加纹理,但这需要额外的投影设备。
5.2 SIFT 匹配点集中在眉毛和嘴唇,鼻梁区域没有对应点
现象:粗配准后鼻梁和面颊区域明显错位,但眉毛和嘴唇附近对齐得很好。
原因:SIFT 特征点天然倾向于出现在角点、边缘等纹理丰富的地方。人脸图像中眉毛、眼睛、嘴唇的纹理最强,鼻梁和面颊的曲率变化平缓,SIFT 检测不到足够的特征点。
解决:论文在结尾也提到了这个问题。可以在 SIFT 之外补充其他特征,比如在人脸关键点检测的基础上手动指定鼻尖、眼角等语义对应点。或者用稠密光流代替稀疏特征匹配,获取像素级的对应关系。
5.3 标定误差偏大导致点云整体扭曲
现象:重建出的人脸点云看起来是歪的,左右不对称,深度方向有明显的系统性偏移。
原因:双目标定的旋转矩阵 R 和平移向量 T 不够准确,极线校正后左右图像的对应点不在同一水平线上,视差计算出现系统性误差。
解决:重新标定,增加标定图数量,确保棋盘格在画面中各个位置都有出现。标定完成后用cv2.stereoRectify的返回值检查校正效果:在左右校正图上画水平线,同一个角点应该落在同一条线上。如果偏差超过 1 个像素,标定结果不可用。
5.4 GrabCut 分割把人脸边缘也切掉了
现象:分割后的掩码图在脸颊边缘、下巴处收缩了 5~10 个像素,导致点云边缘缺失。
原因:GrabCut 的能量函数中边界项会惩罚前景和背景的突变,当人脸边缘和背景颜色接近时,算法倾向于把边缘像素判为背景以降低边界项代价。
解决:初始化矩形框时向外多扩 20~30 像素,给 GrabCut 留出判断余地。分割完成后对掩码做一次形态学膨胀,把边缘补回来。膨胀核大小取 3×3 或 5×5,太大又会把背景带进来。
5.5 金字塔匹配在低分辨率层丢失了鼻尖的视差
现象:鼻尖区域深度值偏小,点云上鼻子看起来是塌的。
原因:鼻尖是人脸深度变化最剧烈的地方,在 160×120 的低分辨率层,鼻尖可能只占 2~3 个像素,视差被周围平滑区域平均掉了。传递到高分辨率层时,搜索范围被限制在错误初值附近,无法纠正。
解决:在金字塔匹配的每一层都保留一个最小搜索范围,不要完全依赖上层初值。具体做法是把搜索范围设为max(2d-t, min_search),min_search取 4~8 个像素,给高分辨率层留出纠正空间。
6. 进阶技巧:用中值距离筛选把粗配准精度再提一档
论文在实验部分留了一个尾巴:即使经过 RANSAC 过滤,SIFT 匹配点中仍然存在误匹配,作者建议后续用匹配点间的欧氏距离中值或高斯分布来筛选参考点。我在实际项目里把这个思路落地过,效果比单纯用 RANSAC 稳定不少。
具体做法分三步。第一步,对 RANSAC 过滤后的匹配点对,计算每一对在三维空间中的欧氏距离。第二步,取这些距离的中值d_median,然后只保留距离在[0.5 * d_median, 1.5 * d_median]范围内的匹配对。第三步,用筛选后的匹配对重新跑一次四元数配准。
def filter_by_distance(src_3d, dst_3d, low=0.5, high=1.5): """按欧氏距离中值筛选匹配点对""" distances = np.linalg.norm(src_3d - dst_3d, axis=1) d_median = np.median(distances) mask = (distances >= low * d_median) & (distances <= high * d_median) return src_3d[mask], dst_3d[mask], mask这个筛选的逻辑是:正确的匹配点对,其三维距离应该集中在中值附近,因为同一个物理点在两次拍摄中的位置变化是连续的。距离特别小的可能是两个点云中偶然靠近的不同点,距离特别大的基本可以确定是误匹配。low和high这两个参数根据点云噪声水平调整,噪声大的时候把范围放宽到[0.3, 1.8],噪声小的时候收紧到[0.7, 1.3]。
还有一个细节值得注意:筛选之后如果匹配点对少于 6 对,四元数法求解会不稳定。这时候要么放宽筛选范围,要么回到 RANSAC 之前的结果。我一般会设一个下限,少于 10 对匹配点时就不做距离筛选了,直接用 RANSAC 的结果。
验证粗配准效果的方法很简单:把配准后的两个点云用不同颜色渲染到同一个坐标系里,肉眼看重叠程度。更定量一点的做法是计算配准后对应点之间的均方根误差(RMSE),RMSE 小于 3 mm 基本可以认为粗配准到位了,可以交给 ICP 做精细配准。如果 RMSE 大于 5 mm,说明粗配准没做好,ICP 很可能收敛到局部最优。
从那以后我每次做点云粗配准,都会在 RANSAC 之后强制走一遍中值距离筛选,并且把筛选前后的 RMSE 都打出来对比。这个习惯帮我省了很多次返工的时间。希望帮到你。
本文还有配套的精品资源,点击获取