
简介本资源面向计算机视觉、立体视觉与三维重建方向的学习者与开发者聚焦RGB与IR多模态图像配准、深度图生成及三维人脸模型重建这一完整技术链路可用于课程设计、科研复现或项目原型搭建。压缩包共205个文件约166.95MB以167张png图像数据与结果图为主辅以7个Python脚本、3个MATLAB的m文件、2个mat数据文件及md说明文档覆盖图像预处理、特征提取、配准融合与模型构建等环节另含少量xlsx、docx等辅助资料。目前已有104人学习下载。资源提供了从多模态数据到深度图、再到三维人脸模型的实践素材与代码参考读者可据此理解配准算法流程、复现深度估计与重建效果并借鉴其目录组织与脚本结构为后续在虚拟现实、人脸识别等场景中的深度感知应用打下基础。1. 从两张图到一张脸RGB-IR 配准到底在解决什么问题很多人第一次接触三维人脸重建脑子里想的都是「上一套结构光或者 ToF 相机不就完了」。真到项目里才发现结构光在室外强光下基本废掉ToF 的分辨率又撑不起人脸细节而单目 RGB 估深度的方案尺度是漂的、边缘是糊的。于是绕一圈回来最稳的组合还是 RGB 加 IR 这一对RGB 负责纹理和颜色IR 负责在暗光、逆光下依然能拿到稳定的结构信息两者配准之后生成深度图再往上叠三维人脸模型。这个方向的核心难点从来不是「有没有深度图」而是「深度图准不准、配准稳不稳、重建出来的脸能不能用」。标题里说的 RGB 和 IR 图像配准本质是把两个不同模态、不同视角、不同成像特性的传感器对齐到同一个坐标系里让每个像素的深度值都能对应到正确的空间点。这件事做不好后面所有重建都是空中楼阁。适合谁看做多模态图像处理、做人脸建模、做深度感知落地的工程师以及想从单目深度估计升级到多模态融合的开发者。2. RGB-IR 配准与深度图生成从标定到稠密深度的完整链路2.1 为什么不能直接拿 RGB 做深度模态互补的选型逻辑单目深度估计这几年进步很大但它的本质是从语义和纹理里「猜」深度遇到无纹理区域、反光表面、细长结构就翻车。IR 图像不一样它记录的是主动或被动红外辐射在暗光下依然有稳定的灰度梯度而且很多设备会投射红外点阵或结构光图案这些图案的形变直接编码了深度信息。RGB 和 IR 的互补性体现在三个层面第一光谱互补RGB 在可见光波段IR 在近红外波段两者对材质和光照的响应不同融合后能覆盖更多场景第二分辨率互补RGB 通常分辨率更高IR 分辨率低但深度线索更直接第三失效场景互补RGB 在暗光下噪声大IR 在强光下可能过曝但两者同时失效的概率低。常见做法是先用标定把两个相机的内参和外参求出来再把 IR 的深度线索投影到 RGB 视角下生成一张和 RGB 对齐的稠密深度图。这样既保留了 RGB 的高分辨率纹理又用上了 IR 的深度信息。2.2 双目标定把两个相机塞进同一个坐标系标定的质量直接决定配准的上限。我一般会先用棋盘格或者圆点标定板分别对 RGB 和 IR 相机做单目标定再做双目标定。IR 相机标定时要注意如果 IR 发射器一直开着标定板上的图案会被红外点阵污染导致角点检测失败。解决办法是标定期间临时关闭 IR 发射器或者用能挡住红外但透可见光的滤片。下面是一个用 OpenCV 做双目标定的最小代码示例import cv2 import numpy as np import glob # 棋盘格参数内角点数量比如 9x6 chessboard_size (9, 6) square_size 25.0 # 毫米 # 生成棋盘格三维坐标 objp np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp * square_size objpoints [] # 三维点 imgpoints_rgb [] # RGB 图像点 imgpoints_ir [] # IR 图像点 rgb_images sorted(glob.glob(calib/rgb/*.png)) ir_images sorted(glob.glob(calib/ir/*.png)) for rgb_path, ir_path in zip(rgb_images, ir_images): img_rgb cv2.imread(rgb_path) img_ir cv2.imread(ir_path, cv2.IMREAD_GRAYSCALE) gray_rgb cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY) ret_rgb, corners_rgb cv2.findChessboardCorners(gray_rgb, chessboard_size, None) ret_ir, corners_ir cv2.findChessboardCorners(img_ir, chessboard_size, None) if ret_rgb and ret_ir: # 亚像素优化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_rgb cv2.cornerSubPix(gray_rgb, corners_rgb, (11, 11), (-1, -1), criteria) corners_ir cv2.cornerSubPix(img_ir, corners_ir, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints_rgb.append(corners_rgb) imgpoints_ir.append(corners_ir) # 单目标定 ret_rgb, mtx_rgb, dist_rgb, _, _ cv2.calibrateCamera(objpoints, imgpoints_rgb, gray_rgb.shape[::-1], None, None) ret_ir, mtx_ir, dist_ir, _, _ cv2.calibrateCamera(objpoints, imgpoints_ir, img_ir.shape[::-1], None, None) # 双目标定 ret, mtx_rgb, dist_rgb, mtx_ir, dist_ir, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_rgb, imgpoints_ir, mtx_rgb, dist_rgb, mtx_ir, dist_ir, gray_rgb.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC ) print(旋转矩阵 R:\n, R) print(平移向量 T:\n, T)这段代码的逻辑是先分别求两个相机的内参和畸变系数再用双目标定求它们之间的旋转和平移关系。CALIB_FIX_INTRINSIC表示固定单目标定得到的内参只优化外参适合单目标定已经比较准的情况。如果标定板图像少于 15 组外参容易不稳建议拍到 20 组以上且标定板要覆盖画面各个区域。2.3 立体校正与视差计算让对应点落在同一行双目标定之后下一步是立体校正。校正的目的是让两个相机的图像平面共面且极线水平这样同一个空间点在两幅图上的投影只差一个水平位移也就是视差。视差和深度成反比视差越准深度越准。# 立体校正 img_size gray_rgb.shape[::-1] R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_rgb, dist_rgb, mtx_ir, dist_ir, img_size, R, T, alpha0 ) # 生成映射表 map1_rgb, map2_rgb cv2.initUndistortRectifyMap(mtx_rgb, dist_rgb, R1, P1, img_size, cv2.CV_32FC1) map1_ir, map2_ir cv2.initUndistortRectifyMap(mtx_ir, dist_ir, R2, P2, img_size, cv2.CV_32FC1) # 应用映射 rectified_rgb cv2.remap(img_rgb, map1_rgb, map2_rgb, cv2.INTER_LINEAR) rectified_ir cv2.remap(img_ir, map1_ir, map2_ir, cv2.INTER_LINEAR)alpha0表示校正后只保留有效像素画面会裁掉一部分边缘但不会引入黑色无效区域。如果做全图配准可以设alpha1但边缘会有拉伸。视差计算用 SGBM 是常见选择# SGBM 视差计算 window_size 5 min_disp 0 num_disp 16 * 5 # 必须是 16 的倍数 stereo cv2.StereoSGBM_create( minDisparitymin_disp, numDisparitiesnum_disp, blockSizewindow_size, P18 * 3 * window_size ** 2, P232 * 3 * window_size ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32 ) disparity stereo.compute(rectified_ir, rectified_rgb).astype(np.float32) / 16.0这里rectified_ir作为左图rectified_rgb作为右图是因为 IR 图像纹理更稳定作为参考图效果更好。numDisparities决定最大可测深度范围设小了远处会丢设大了计算量上升且噪声增加。uniquenessRatio是唯一性检查值越大越严格能过滤掉误匹配但太大会导致弱纹理区域视差缺失。2.4 从视差到深度图Q 矩阵与空洞填充视差算完用Q矩阵重投影到三维# 视差转深度 points_3d cv2.reprojectImageTo3D(disparity, Q) depth_map points_3d[:, :, 2] # Z 轴就是深度 # 无效视差置零 mask disparity disparity.min() depth_map[~mask] 0Q矩阵是stereoRectify输出的 4x4 重投影矩阵它把视差和像素坐标映射到三维空间。points_3d的每个通道分别是 X、Y、ZZ 就是深度。实际用的时候视差图里会有大量空洞尤其是人脸边缘、鼻孔、眼睛这些地方。常见做法是用联合双边滤波或者导向滤波做空洞填充以 RGB 图作为引导因为 RGB 的边缘和深度边缘通常是对齐的。# 用 RGB 引导滤波填充深度空洞 import cv2.ximgproc as ximgproc depth_filled ximgproc.guidedFilter( guiderectified_rgb, srcdepth_map.astype(np.float32), radius8, eps1e-3 )radius控制滤波窗口大小eps控制边缘保持程度。eps太小噪声压不住太大边缘会被模糊。我一般从1e-3开始调看深度图边缘是否贴合人脸轮廓。3. 三维人脸模型重建从深度图到可用的网格3.1 点云生成与预处理别急着上泊松重建拿到深度图之后最直接的做法是反投影成点云。但原始点云通常有几类问题离群点、噪声、密度不均、人脸区域和背景混在一起。直接上泊松重建结果往往是一坨带刺的球。# 深度图转点云 h, w depth_map.shape fx P1[0, 0] fy P1[1, 1] cx P1[0, 2] cy P1[1, 2] points [] colors [] for v in range(h): for u in range(w): z depth_map[v, u] if z 0 or z 2000: # 过滤无效深度和过远点 continue x (u - cx) * z / fx y (v - cy) * z / fy points.append([x, y, z]) colors.append(rectified_rgb[v, u] / 255.0) points np.array(points) colors np.array(colors)这段代码把每个有效深度像素反投影到三维空间。fx、fy、cx、cy从P1里取P1是校正后的投影矩阵。深度阈值2000是毫米根据实际场景调整人脸重建一般 300 到 800 毫米就够了。预处理我一般会做三步统计滤波去离群点、体素下采样统一密度、直通滤波裁掉背景。import open3d as o3d pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points) pcd.colors o3d.utility.Vector3dVector(colors) # 统计滤波 pcd, _ pcd.remove_statistical_outlier(nb_neighbors20, std_ratio2.0) # 体素下采样 pcd pcd.voxel_down_sample(voxel_size1.0) # 直通滤波保留人脸区域 bbox o3d.geometry.AxisAlignedBoundingBox( min_bound(-200, -200, 300), max_bound(200, 200, 800) ) pcd pcd.crop(bbox)nb_neighbors和std_ratio控制离群点判定voxel_size是体素边长单位毫米。1.0表示 1 毫米的体素对人脸来说细节保留得比较好。直通滤波的边界要根据实际相机距离调别把人脸裁掉了。3.2 网格重建泊松、滚球与 Marching Cubes 的取舍点云预处理完接下来是网格化。常见方法有三种泊松重建、滚球法、Marching Cubes。泊松重建对噪声容忍度高生成的网格光滑但容易把背景和头发也连成一片滚球法适合有法向量的点云细节好但参数敏感Marching Cubes 需要先把点云转成体素网格适合规则化数据。我一般先用泊松重建出粗网格再裁掉非人脸区域# 估计法向量 pcd.estimate_normals( search_paramo3d.geometry.KDTreeSearchParamHybrid(radius5.0, max_nn30) ) pcd.orient_normals_consistent_tangent_plane(k30) # 泊松重建 mesh, densities o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( pcd, depth9 ) # 根据密度裁剪低置信区域 densities np.asarray(densities) density_threshold np.quantile(densities, 0.05) vertices_to_remove densities density_threshold mesh.remove_vertices_by_mask(vertices_to_remove)depth9控制八叉树深度值越大细节越多但内存和计算量也越大。人脸重建一般 8 到 10 够用。density_threshold取 5% 分位数意思是去掉密度最低的 5% 顶点这些通常是噪声或者背景边缘。3.3 纹理映射把 RGB 贴回三维脸网格有了纹理映射就是把 RGB 图像的颜色对应到网格顶点上。因为深度图和 RGB 已经配准每个三维点都能找到对应的 RGB 像素。# 纹理映射 mesh.compute_vertex_normals() mesh.textures [o3d.geometry.Image(rectified_rgb)] # 为每个顶点找对应的 UV 坐标 # 这里用简单投影把三维点投影回图像平面 vertices np.asarray(mesh.vertices) uvs [] for v in vertices: x, y, z v u fx * x / z cx v_coord fy * y / z cy u_norm u / w v_norm v_coord / h uvs.append([u_norm, v_norm]) mesh.triangle_uvs o3d.utility.Vector2dVector(np.array(uvs))这段代码用针孔相机模型把三维顶点投影回图像平面得到归一化 UV 坐标。实际用的时候要注意遮挡鼻子后面的点不应该映射到鼻子的纹理。简单做法是用 Z-buffer 判断可见性只保留每个像素最近的点。4. 避坑与排查RGB-IR 配准和重建里最容易翻车的 5 个点4.1 标定板在 IR 下看不清角点检测失败现象双目标定时 IR 图像上找不到棋盘格角点或者找到的角点位置偏移。原因IR 相机对材质反射特性敏感普通打印的棋盘格在 IR 下对比度可能很低另外 IR 发射器如果开着点阵图案会覆盖标定板。解决用专门的红外标定板或者在普通标定板后面加一层能反射红外的材料标定时关闭 IR 发射器如果必须开着用能挡红外的滤片盖住发射器。4.2 视差图大面积空洞纹理太弱或曝光不一致现象SGBM 输出的视差图里人脸区域大片黑色深度图几乎不可用。原因IR 图像在暗光下噪声大或者 RGB 和 IR 的曝光差异太大导致匹配代价不稳定。解决先做直方图均衡化或者 CLAHE 提升 IR 对比度检查两个相机的曝光时间尽量让同一场景的亮度接近调大uniquenessRatio和speckleWindowSize过滤误匹配如果还是不行考虑用主动 IR 投射器增加纹理。4.3 深度图边缘锯齿视差精度不够现象重建出来的人脸边缘像台阶一样鼻子和脸颊过渡不自然。原因视差是整数像素精度reprojectImageTo3D直接量化到深度边缘就会跳变。解决用亚像素视差优化比如cv2.StereoSGBM的mode设为MODE_HH或者用ximgproc.createRightMatcher做左右一致性检查后处理用导向滤波或者联合双边滤波平滑深度图同时保持边缘。4.4 泊松重建把背景连成一片裁剪没做干净现象重建出来的网格里人脸和背景、头发连在一起像个头盔。原因点云里还有背景点泊松重建会把所有点都包进去。解决在点云阶段就用直通滤波或者 RANSAC 平面分割去掉背景泊松重建后根据密度裁剪去掉低密度区域如果头发干扰严重可以先做一次人脸检测只保留人脸框内的点。4.5 纹理映射错位UV 计算没考虑畸变现象贴图后人脸纹理和几何对不上眼睛跑到额头上。原因UV 计算用的是理想针孔模型但实际图像有畸变校正后的图像虽然去掉了畸变但投影矩阵P1和实际像素坐标可能有偏差。解决确保 UV 计算用的是校正后的投影矩阵P1而不是原始内参如果还有偏差检查stereoRectify的alpha参数alpha0会裁掉边缘UV 坐标要对应裁剪后的图像。5. 进阶技巧用多帧融合和法向优化把深度图精度再提一档单帧 RGB-IR 配准的深度图精度受限于视差量化和噪声。如果场景允许多帧融合是最直接的提升手段。我一般会让人脸在相机前缓慢转动采集 10 到 20 帧然后用 ICP 把多帧点云配准到一起再做泊松重建。这样不仅能填上单帧的空洞还能把噪声平均掉。# 多帧点云配准 def pairwise_registration(source, target, init_transform): # 先用粗配准 icp_coarse o3d.pipelines.registration.registration_icp( source, target, max_correspondence_distance50, initinit_transform, estimation_methodo3d.pipelines.registration.TransformationEstimationPointToPoint() ) # 再用精配准 icp_fine o3d.pipelines.registration.registration_icp( source, target, max_correspondence_distance5, initicp_coarse.transformation, estimation_methodo3d.pipelines.registration.TransformationEstimationPointToPlane() ) return icp_fine.transformation # 假设 pcds 是多个视角的点云列表 # 以第一帧为基准依次配准 transform np.identity(4) for i in range(1, len(pcds)): trans pairwise_registration(pcds[i], pcds[0], transform) pcds[i].transform(trans) transform transmax_correspondence_distance粗配准设大一点比如 50 毫米精配准设小一点5 毫米。TransformationEstimationPointToPlane需要点云有法向量精度比点到点高但对初始位置更敏感。另一个技巧是法向优化。深度图的法向可以从相邻像素的深度梯度算出来然后用这个法向约束泊松重建能让网格更贴合真实表面。Open3D 的泊松重建本身会估计法向但如果点云法向估计不准可以先做一次法向平滑# 法向平滑 pcd.estimate_normals( search_paramo3d.geometry.KDTreeSearchParamHybrid(radius3.0, max_nn30) ) pcd.orient_normals_consistent_tangent_plane(k20) pcd.normals o3d.utility.Vector3dVector( np.array(pcd.normals) * 0.5 np.array(pcd.normals) * 0.5 )这段代码看起来是重复赋值实际是示意法向平滑可以用邻域法向加权平均权重根据距离或者曲率来定。radius越小法向对细节越敏感但噪声也越大。验证深度图精度我一般用两个指标一是和已知尺寸的标定物对比比如放一个已知直径的球看重建出来的直径误差二是看人脸关键点的三维距离比如两眼间距、鼻尖到下巴的距离和真实值比。误差在 1 到 2 毫米以内基本就够用了。最后说个血泪经验RGB-IR 配准最怕的是「看起来对齐了实际深度错了」。因为 RGB 和 IR 的纹理差异大有时候视差图在视觉上很平滑但深度值整体偏移。我现在的习惯是每次标定完先拿一个已知距离的平面板验证深度值确认无误再上人脸。这个后悔药希望你别吃。希望帮到你。本文还有配套的精品资源点击获取