SHINE-PPG:非朗伯分解实现光照鲁棒的车载rPPG(ECCV 2026 论文解读)

SHINE-PPG:非朗伯分解实现光照鲁棒的车载rPPG

论文信息

项目 内容
标题 SHINE-PPG: Non-Lambertian Intrinsic Decomposition for Illumination-Robust rPPG
会议 ECCV 2026
链接 Paper Notes
领域 计算机视觉,人脸理解

1. 核心创新

1.1 问题

rPPG 通过捕捉面部皮肤微小颜色变化测量心率,但面临非朗伯反射挑战:

问题 描述 影响
镜面高光 强光在皮肤的镜面反射 掩盖脉搏信号
环境光变化 车辆进出隧道、阳光角度变化 信号基线漂移
非朗伯表面 皮肤不是理想漫反射表面 简单模型失效

1.2 SHINE-PPG 方法

提出非朗伯内在分解框架:

1
2
3
4
5
6
面部视频帧 → 非朗伯分解
├── 漫反射分量 (diffuse reflectance)
├── 环境光照分量 (environmental illumination)
└── 稀疏镜面高光 (specular highlights)
↓
从漫反射分量提取 rPPG 信号

关键创新:

组件 技术 作用
非朗伯分解 显式解耦漫反射/环境光/镜面 消除非脉搏干扰
AdaIN 对抗光照 可学习的对抗光照归一化 消除环境光变化
稀疏高光建模 稀疏约束的镜面分量 去除镜面干扰

2. 方法详解

2.1 非朗伯反射模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""
SHINE-PPG 非朗伯分解模型
"""

import torch
import torch.nn as nn
import torch.nn.functional as F

class NonLambertianDecomposition(nn.Module):
"""
非朗伯内在分解

将面部图像分解为:
1. 漫反射分量 (包含rPPG信号)
2. 环境光照分量
3. 稀疏镜面高光分量
"""

def __init__(self, config: dict):
super().__init__()
in_channels = config.get('in_channels', 3)
feat_dim = config.get('feat_dim', 64)

# 共享编码器
self.encoder = nn.Sequential(
nn.Conv2d(in_channels, feat_dim, 3, padding=1),
nn.BatchNorm2d(feat_dim),
nn.ReLU(inplace=True),
nn.Conv2d(feat_dim, feat_dim*2, 3, stride=2, padding=1),
nn.BatchNorm2d(feat_dim*2),
nn.ReLU(inplace=True),
nn.Conv2d(feat_dim*2, feat_dim*4, 3, stride=2, padding=1),
nn.BatchNorm2d(feat_dim*4),
nn.ReLU(inplace=True),
)

# 漫反射解码器
self.diffuse_head = nn.Sequential(
nn.Conv2d(feat_dim*4, feat_dim*2, 3, padding=1),
nn.Upsample(scale_factor=2),
nn.Conv2d(feat_dim*2, feat_dim, 3, padding=1),
nn.Upsample(scale_factor=2),
nn.Conv2d(feat_dim, in_channels, 3, padding=1),
nn.Sigmoid()
)

# 环境光照解码器(全局向量)
self.illumination_head = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(feat_dim*4, 32),
nn.ReLU(inplace=True),
nn.Linear(32, 3), # RGB 环境光
nn.Sigmoid()
)

# 稀疏镜面高光解码器
self.specular_head = nn.Sequential(
nn.Conv2d(feat_dim*4, feat_dim*2, 3, padding=1),
nn.Upsample(scale_factor=2),
nn.Conv2d(feat_dim*2, feat_dim, 3, padding=1),
nn.Upsample(scale_factor=2),
nn.Conv2d(feat_dim, in_channels, 3, padding=1),
nn.Sigmoid()
)

# AdaIN 参数预测器
self.adain_params = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(feat_dim*4, 64),
nn.ReLU(inplace=True),
nn.Linear(64, 2 * feat_dim * 4) # gamma, beta
)

def forward(self, x: torch.Tensor) -> dict:
"""
前向传播

Args:
x: 面部图像 (B, C, H, W)

Returns:
{
'diffuse': (B, C, H, W), # 漫反射
'illumination': (B, 3), # 环境光
'specular': (B, C, H, W), # 镜面高光
'reconstruction': (B, C, H, W) # 重建 = diffuse + specular
}
"""
# 编码
feat = self.encoder(x)

# AdaIN 参数
adain = self.adain_params(feat)
gamma, beta = adain.chunk(2, dim=1)
gamma = gamma.view(-1, feat.shape[1], 1, 1)
beta = beta.view(-1, feat.shape[1], 1, 1)

# 对抗光照归一化
feat_normalized = gamma * feat + beta

# 分解
diffuse = self.diffuse_head(feat_normalized)
illumination = self.illumination_head(feat)
specular = self.specular_head(feat_normalized)

# 重建
reconstruction = diffuse + specular

return {
'diffuse': diffuse,
'illumination': illumination,
'specular': specular,
'reconstruction': reconstruction
}

def extract_rppg(self, diffuse_frames: torch.Tensor,
skin_mask: torch.Tensor) -> torch.Tensor:
"""
从漫反射分量序列提取 rPPG 信号

Args:
diffuse_frames: (T, C, H, W) 漫反射帧序列
skin_mask: (H, W) 皮肤mask

Returns:
rppg_signal: (T,) 心率信号
"""
T, C, H, W = diffuse_frames.shape

# 只提取皮肤区域的漫反射
mask = skin_mask.unsqueeze(0).unsqueeze(0) # (1, 1, H, W)
masked = diffuse_frames * mask

# 空间平均
spatial_mean = masked.sum(dim=(2, 3)) / (skin_mask.sum() + 1e-10) # (T, C)

# 绿色通道(rPPG最敏感)
green = spatial_mean[:, 1] # (T,)

# 去趋势
green = green - green.mean()

return green


# 测试
if __name__ == "__main__":
model = NonLambertianDecomposition({'in_channels': 3, 'feat_dim': 32})

# 模拟面部图像
x = torch.randn(2, 3, 64, 64)

result = model(x)
print(f"漫反射: {result['diffuse'].shape}")
print(f"环境光: {result['illumination'].shape}")
print(f"镜面: {result['specular'].shape}")
print(f"重建: {result['reconstruction'].shape}")
print(f"参数量: {sum(p.numel() for p in model.parameters()):,}")

2.2 与 PixExpo 的互补

方案 解决的问题 方法
PixExpo 硬件层曝光优化 像素级曝光选择
SHINE-PPG 算法层光照解耦 非朗伯分解
两者结合 硬件+算法协同 PixExpo获取好数据 → SHINE-PPG消除残余干扰

3. 对 IMS 开发的启示

3.1 rPPG 技术栈

1
2
3
4
5
6
完整 rPPG 管线:
1. 像素级曝光(PixExpo)→ 解决过曝/欠曝
2. 非朗伯分解(SHINE-PPG)→ 消除镜面/环境光干扰
3. 皮肤分割 → 定位有效区域
4. 信号滤波 → 提取脉搏波
5. 心率估计 → FFT/深度学习

3.2 落地建议

优先级 建议
🟡 P1 SHINE-PPG 分解模型可在现有 DMS 摄像头上部署
🟡 P1 与 PixExpo 叠加使用,先硬件后算法
🟢 P2 训练数据需要多光照场景(隧道/阳光/夜间)

4. 参考


SHINE-PPG:非朗伯分解实现光照鲁棒的车载rPPG(ECCV 2026 论文解读)
https://dapalm.com/2026/10/06/2026-10-06-014-shine-ppg-non-lambertian-rppg-eccv2026/
作者
Mars
发布于
2026年10月6日
许可协议