朗伯光度立体法 Python 3.11 实现:从 3 光源图像到法线贴图的完整流程

发布时间:2026/7/7 1:54:42
朗伯光度立体法 Python 3.11 实现:从 3 光源图像到法线贴图的完整流程 朗伯光度立体法 Python 3.11 实现从 3 光源图像到法线贴图的完整流程在计算机视觉领域光度立体法Photometric Stereo是一种通过分析物体在不同光照条件下的图像变化来重建物体表面三维形状的技术。本文将详细介绍如何使用 Python 3.11 和现代计算机视觉库如 NumPy 和 OpenCV实现朗伯光度立体法从三张不同光源方向的图像生成法线贴图和反照率图。1. 光度立体法基础原理光度立体法的核心思想基于朗伯反射模型Lambertian Reflectance该模型假设物体表面是理想漫反射表面即光线在所有方向上均匀散射。根据这一模型图像中某一点的亮度可以表示为I ρ * (L · N)其中I观察到的像素亮度ρ表面反照率albedoL光源方向向量N表面法线向量当使用多个至少三个不同方向的光源照射同一物体时我们可以建立方程组来求解每个像素点的法线和反照率。1.1 数学推导对于 n 个光源我们可以将方程组表示为矩阵形式[I₁, I₂, ..., In]ᵀ ρ * L * N其中 L 是一个 3×n 的矩阵每一列代表一个光源方向。通过最小二乘法我们可以解出 G ρNG (LᵀL)⁻¹LᵀI然后分别计算反照率和归一化的法线ρ ||G|| N G / ρ2. 环境准备与依赖安装在开始编码前需要确保 Python 3.11 环境和必要的库已安装。以下是推荐的环境配置# 创建并激活虚拟环境 python3.11 -m venv photometric_env source photometric_env/bin/activate # Linux/Mac photometric_env\Scripts\activate # Windows # 安装核心依赖 pip install numpy opencv-python matplotlib scipy关键库的作用NumPy用于矩阵运算和线性代数求解OpenCV图像加载和预处理Matplotlib结果可视化SciPy可选用于高级数学运算3. 数据准备与预处理3.1 输入图像要求理想的光度立体法输入应满足以下条件完全静态的场景和相机至少三个不同方向的光源已知精确的光源方向向量无环境光干扰典型的输入图像集可能如下input/ ├── light1.jpg # 光源方向1 ├── light2.jpg # 光源方向2 └── light3.jpg # 光源方向33.2 图像加载与对齐即使相机固定微小的位移也可能影响结果。我们可以使用特征匹配来确保图像对齐import cv2 import numpy as np def align_images(images): # 使用第一张图像作为参考 ref_image images[0] aligned [ref_image] # 初始化ORB检测器 orb cv2.ORB_create() kp1, des1 orb.detectAndCompute(ref_image, None) # 创建BFMatcher对象 bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) for img in images[1:]: kp2, des2 orb.detectAndCompute(img, None) # 匹配描述符 matches bf.match(des1, des2) matches sorted(matches, keylambda x: x.distance) # 提取匹配点位置 src_pts np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1,1,2) dst_pts np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1,1,2) # 计算单应性矩阵 M, _ cv2.findHomography(dst_pts, src_pts, cv2.RANSAC, 5.0) # 应用变换 aligned_img cv2.warpPerspective(img, M, (ref_image.shape[1], ref_image.shape[0])) aligned.append(aligned_img) return aligned4. 核心算法实现4.1 光源方向矩阵构建光源方向矩阵 L 是算法的关键输入。假设我们已经通过校准知道三个光源的方向向量# 示例三个光源方向需根据实际设置调整 L np.array([ [0, 0, 1], # 正上方光源 [1, 0, 0.5], # 右侧光源 [-1, 0, 0.5] # 左侧光源 ]).T # 转置为3×n矩阵注意实际应用中需要通过校准球或其他方法精确测量光源方向。错误的光源方向会导致法线计算不准确。4.2 光度立体法核心计算下面是完整的法线和反照率计算实现def photometric_stereo(images, L): 光度立体法核心计算 :param images: 对齐后的图像列表 (h, w) 或 (h, w, 3) :param L: 光源方向矩阵 (3, n) :return: albedo, normal # 确保L是3×n矩阵 assert L.shape[0] 3 # 将图像堆叠为 (h, w, n) 或 (h, w, 3, n) if images[0].ndim 2: # 灰度图 intensities np.dstack(images) # (h, w, n) else: # 彩色图 intensities np.stack(images, axis-1) # (h, w, 3, n) # 计算伪逆 L_inv np.linalg.pinv(L) # (n, 3) if intensities.ndim 3: # 灰度处理 # 计算G矩阵 (h, w, 3) G np.einsum(ij,klj-kli, L_inv, intensities) # 计算反照率和法线 albedo np.linalg.norm(G, axis2) # (h, w) normal G / (albedo[..., np.newaxis] 1e-10) # 避免除以零 return albedo, normal else: # 彩色处理 h, w intensities.shape[:2] albedo np.zeros((h, w, 3)) normal np.zeros((h, w, 3, 3)) # 对每个颜色通道分别处理 for c in range(3): G np.einsum(ij,klj-kli, L_inv, intensities[..., c, :]) albedo[..., c] np.linalg.norm(G, axis2) normal[..., c, :] G / (albedo[..., c, np.newaxis] 1e-10) # 合并通道取平均法线 final_normal np.mean(normal, axis2) final_normal / np.linalg.norm(final_normal, axis2, keepdimsTrue) return albedo, final_normal4.3 法线可视化计算得到的法线向量各分量在[-1,1]范围内需要映射到[0,1]以便可视化def visualize_normal(normal): 将法线向量可视化为RGB图像 :param normal: (h, w, 3) 法线图分量范围[-1,1] :return: (h, w, 3) uint8图像 # 将法线从[-1,1]映射到[0,1] vis (normal 1) / 2 # 处理可能的NaN值 vis np.nan_to_num(vis) # 转换为8位图像 return (vis * 255).astype(np.uint8)5. 完整流程示例下面是一个完整的从图像加载到结果可视化的示例# 1. 加载图像 image_files [light1.jpg, light2.jpg, light3.jpg] images [cv2.imread(f, cv2.IMREAD_COLOR) for f in image_files] images [cv2.cvtColor(img, cv2.COLOR_BGR2RGB) for img in images] # 2. 图像对齐可选 aligned_images align_images(images) # 3. 转换为灰度图 gray_images [cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(float)/255.0 for img in aligned_images] # 4. 定义光源方向矩阵需根据实际设置调整 L np.array([ [0, 0, 1], [1, 0, 0.5], [-1, 0, 0.5] ]).T # 5. 计算法线和反照率 albedo, normal photometric_stereo(gray_images, L) # 6. 可视化结果 normal_vis visualize_normal(normal) # 显示结果 import matplotlib.pyplot as plt plt.figure(figsize(15,5)) plt.subplot(131); plt.imshow(aligned_images[0]); plt.title(输入图像1) plt.subplot(132); plt.imshow(albedo, cmapgray); plt.title(反照率图) plt.subplot(133); plt.imshow(normal_vis); plt.title(法线图) plt.show()6. 高级优化与注意事项6.1 阴影和高光处理实际图像中可能存在阴影亮度为零和高光饱和区域这些区域会干扰法线计算。我们可以通过以下策略改进阴影检测排除亮度低于阈值的像素高光剔除排除亮度高于阈值的像素鲁棒回归使用RANSAC等鲁棒算法拟合改进后的核心计算部分def robust_photometric_stereo(images, L, shadow_thresh0.05, highlight_thresh0.95): h, w images[0].shape[:2] n len(images) albedo np.zeros((h, w)) normal np.zeros((h, w, 3)) for y in range(h): for x in range(w): # 收集所有光源下的亮度 I np.array([img[y,x] for img in images]) # 排除阴影和高光 valid_mask (I shadow_thresh) (I highlight_thresh) if np.sum(valid_mask) 3: # 至少需要3个有效光源 albedo[y,x] 0 normal[y,x] [0,0,0] continue # 使用有效光源计算 valid_L L[:, valid_mask] valid_I I[valid_mask] # 最小二乘解 G np.linalg.lstsq(valid_L.T, valid_I, rcondNone)[0] # 计算反照率和法线 albedo[y,x] np.linalg.norm(G) if albedo[y,x] 0: normal[y,x] G / albedo[y,x] return albedo, normal6.2 彩色图像处理对于彩色图像我们可以分别处理每个颜色通道然后合并结果def color_photometric_stereo(color_images, L): # 分离通道 channels [ [img[...,0] for img in color_images], # R通道 [img[...,1] for img in color_images], # G通道 [img[...,2] for img in color_images] # B通道 ] # 各通道独立计算 results [photometric_stereo(ch, L) for ch in channels] # 合并反照率 albedo np.stack([res[0] for res in results], axis-1) # 平均法线需重新归一化 combined_normal np.mean([res[1] for res in results], axis0) norm np.linalg.norm(combined_normal, axis2, keepdimsTrue) norm[norm 0] 1 # 避免除以零 combined_normal / norm return albedo, combined_normal7. 结果分析与应用7.1 法线贴图的应用生成的法线贴图可以用于表面缺陷检测通过分析法线异常发现表面缺陷三维重建结合深度积分算法重建三维形状材质分析结合反照率图分析表面材质特性7.2 深度积分从法线图可以进一步计算深度图。基本思路是通过求解泊松方程from scipy import sparse from scipy.sparse.linalg import spsolve def integrate_normal(normal): h, w normal.shape[:2] # 计算梯度场 dzdx -normal[...,0] / (normal[...,2] 1e-10) dzdy -normal[...,1] / (normal[...,2] 1e-10) # 构建稀疏矩阵 indices np.arange(h*w).reshape(h,w) A sparse.lil_matrix((2*h*w, h*w)) b np.zeros(2*h*w) # 填充x方向梯度约束 row 0 for y in range(h): for x in range(w-1): A[row, indices[y,x]] -1 A[row, indices[y,x1]] 1 b[row] dzdx[y,x] row 1 # 填充y方向梯度约束 for y in range(h-1): for x in range(w): A[row, indices[y,x]] -1 A[row, indices[y1,x]] 1 b[row] dzdy[y,x] row 1 # 添加边界条件固定一个点 A sparse.csr_matrix(A) A A[:row] # 移除未使用的行 b b[:row] # 求解线性系统 z spsolve(A.T A, A.T b) return z.reshape(h,w)8. 性能优化技巧对于大图像或实时应用可以考虑以下优化向量化计算使用NumPy的广播机制避免循环GPU加速使用CuPy替代NumPy多分辨率处理先在小尺度计算再上采样细化并行处理对图像分块并行计算示例GPU加速实现import cupy as cp def gpu_photometric_stereo(images, L): # 将数据转移到GPU L_gpu cp.array(L) images_gpu cp.array(images) # 计算伪逆 L_inv cp.linalg.pinv(L_gpu) # 计算G矩阵 G cp.einsum(ij,klj-kli, L_inv, images_gpu) # 计算反照率和法线 albedo cp.linalg.norm(G, axis2) normal G / (albedo[..., cp.newaxis] 1e-10) # 将结果传回CPU return cp.asnumpy(albedo), cp.asnumpy(normal)