You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

光度立体最小二乘计算:过滤阴影像素及奇异矩阵报错求助

解决光度立体法中带阴影像素的最小二乘求解问题

问题背景

我正在实现Woodham 1980年提出的朗伯光度立体算法,目前遇到了阴影像素导致的奇异矩阵问题:

  • 现有输入:self.L是8个光照方向向量堆叠的数组,形状为(8, 3);self.M是8张灰度图堆叠重塑后的数组,形状为(8, 9082318),每一列对应一个像素在8种光照下的灰度值。
  • 无阴影场景下,用self.N = np.linalg.lstsq(self.L.T, self.M.T, rcond=None)[0].T可以正常得到形状为(9082318, 3)的法向量数组,归一化后即可使用。
  • 当前痛点:M中存在位置不固定的阴影像素,需要在计算最小二乘时剔除这些无效值(L维度保持不变),但尝试的掩码处理方法触发了numpy.linalg.LinAlgError: Singular matrix错误。

原工作函数

def _solve_l2(self):
    """
    Lambertian Photometric stereo based on least-squares Woodham 1980
    :return: None
    Compute surface normal : numpy array of surface normal (p × 3)
    """
    self.N = np.linalg.lstsq(self.L.T, self.M.T, rcond=None)[0].T
    print(self.N.shape)
    self.N = normalize(self.N, axis=1) # normalize to account for diffuse reflectance

尝试的参考代码

Ma = self.M.copy()
thresh = 300
Ma[self.M <= thresh] = 0
Ma[self.M > thresh] = 1
Ma = Ma.T
self.M = self.M.T
self.L = self.L.T
print(self.L.shape)
print(self.M.shape)
print(Ma.shape)
A = self.L
B = self.M
M = Ma
# 尝试基于掩码的最小二乘计算
rhs = np.dot(A.T, M * B).T[:,:,None] # n x r x 1 tensor
T = np.matmul(A.T[None,:,:], M.T[:,:,None] * A[None,:,:]) # n x r x r tensor
self.N = np.squeeze(np.linalg.solve(T, rhs)).T # transpose to get r x n

解决思路

1. 先修正阴影掩码的合理性

你当前设置的阈值thresh=300明显有问题——8位灰度图的取值范围是0-255,这个阈值会让几乎所有像素都被标记为阴影,导致有效样本数严重不足,直接引发矩阵奇异。

  • 调整阈值:改用符合图像实际灰度范围的阈值,比如基于图像统计的自适应阈值:
    # 取灰度值的5%分位数作为阴影阈值,可根据实际情况调整
    thresh = np.percentile(self.M, 5)
    shadow_mask = self.M > thresh  # True表示有效像素,False表示阴影
    
  • 过滤无效像素:每个像素的有效光照样本数必须≥3(因为要解3个法向量分量的未知数),否则对应的求解矩阵必然奇异。提前过滤这类像素:
    valid_count = np.sum(shadow_mask, axis=0)
    # 只保留有效样本数≥3的像素
    valid_pixel_mask = valid_count >= 3
    

2. 重构带掩码的最小二乘计算逻辑

原参考代码的张量操作逻辑不够清晰,建议针对每个像素独立处理(可用向量化方式优化效率):

def _solve_l2_with_shadow(self):
    # 1. 生成合理的阴影掩码
    thresh = np.percentile(self.M, 5)
    shadow_mask = self.M > thresh
    valid_count = np.sum(shadow_mask, axis=0)
    valid_pixel_mask = valid_count >= 3
    
    # 2. 初始化法向量数组,无效像素先设为0
    self.N = np.zeros((self.M.shape[1], 3))
    
    # 3. 处理有效像素
    for pixel_idx in np.where(valid_pixel_mask)[0]:
        # 提取该像素的有效光照向量和灰度值
        valid_lights = shadow_mask[:, pixel_idx]
        L_sub = self.L[valid_lights, :].T  # 形状(3, k),k是有效光照数
        M_sub = self.M[valid_lights, pixel_idx]  # 形状(k,)
        
        # 用lstsq或伪逆求解,避免奇异矩阵问题
        try:
            normal = np.linalg.lstsq(L_sub.T, M_sub, rcond=None)[0]
        except np.linalg.LinAlgError:
            # 矩阵奇异时用伪逆求解
            normal = np.dot(np.linalg.pinv(L_sub.T), M_sub)
        
        self.N[pixel_idx] = normal
    
    # 4. 归一化有效像素的法向量
    self.N[valid_pixel_mask] = normalize(self.N[valid_pixel_mask], axis=1)

3. 奇异矩阵的通用处理方案

无论用哪种方式,遇到奇异矩阵时,不要直接用np.linalg.solve,改用以下两种方法:

  • np.linalg.lstsq:本身就是最小二乘求解函数,能自动处理接近奇异的矩阵;
  • np.linalg.pinv:计算矩阵的摩尔-彭罗斯伪逆,返回最小二乘意义上的最优解,完全兼容奇异矩阵场景。

4. 中间结果验证

在调试阶段,可以添加一些打印逻辑验证掩码和计算的正确性:

# 查看有效样本数的分布
print(f"有效样本数范围: {np.min(valid_count)} ~ {np.max(valid_count)}")
print(f"有效像素占比: {np.sum(valid_pixel_mask)/self.M.shape[1]:.2%}")
# 查看单个像素的有效数据
test_idx = np.where(valid_pixel_mask)[0][0]
print(f"测试像素有效光照数: {valid_count[test_idx]}")
print(f"测试像素有效光照向量:\n{self.L[shadow_mask[:, test_idx], :]}")

内容的提问来源于stack exchange,提问作者janb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:20:19