CoRRN协作式反射去除:双分支网络同时分离玻璃反光与背景 做图像处理的人多少都拍过几张“带玻璃”的翻车照。隔着车窗拍街景结果画面正中横着一条路灯拉出的光带在展柜前拍文物怎么对焦都对不到实物反而把玻璃上的倒影拍得清清楚楚。这种问题在专业领域叫反射去除reflection removal最近读到的一篇CoRRNCooperative Reflection Removal Network就是专门来解决这个事的。它会同时估计背景层和反射层让网络内部的两个分支互相协作而不是像很多老方案那样把反射当成噪声一遍遍擦除。这篇论文阅读笔记我会从问题建模、协作式架构设计、损失函数、训练细节和复现避坑几个角度展开适合正在研究图像复原、底层视觉或者打算给拍照App加去玻璃反光功能的人参考。1. 从玻璃后的“鬼影”说起单图反射去除到底难在哪1.1 一句话建模图像等于背景叠加反射反射去除最常见的物理模型是叠加模型。把隔着玻璃拍到的图像记为 I把真实背景记为 B玻璃上的倒影记为 R那么 I B R。这个式子看起来简单极了好像网络只需要学会“减去反射层”就行但真正做起来就会发现这个式子背后藏着一个非常不友好的反问题。先解释一下为什么是“叠加”而不是“混合”。玻璃本身基本不吸收光线严格说还存在菲涅尔效应的透射和反射比例差异但在绝大多数反射去除工作里研究者默认忽略透射衰减直接认为相机接收到的亮度是背景光与反射光的线性叠加。这个假设的好处是数学简单坏处是它让问题变成了一个纯粹的盲源分离问题我们手里只有一个混合信号 I却要拆出两个源信号 B 和 R。更麻烦的是在很多真实场景里反射层的能量占比很小。比如室内灯光反射到玻璃上可能只占画面整体亮度的10%到20%。如果把 I 和 B 的像素级损失直接作为网络目标模型为了降低损失最偷懒的做法就是把输出全部推向 I因为这样损失已经很小了根本不需要费劲去分离反射。这也是反射去除和去噪、去模糊不一样的地方噪声和模糊是有统计先验的而反射是一个有内容、有边缘、有颜色的完整图像层处理起来更像“图像分层”而非“信号复原”。1.2 为什么单张图去反射是个病态问题从数学角度看I B R 只要给定任意一个 B 的估计值都可以得到 R I - B。也就是说这个分解天然有无穷多个解。举个例子如果背景图 B 在某个区域是均匀的那么把这一区域的 R 整体增减一个常数照样能还原出同一个 I。模型根本不知道应该把那个“常数值”归给背景还是归给反射。还有个非常容易忽略的难点玻璃上的反射经常是“虚像”。反射图像的边缘可能比背景更模糊也可能更锐利反射内容会和背景内容在空间上高度重叠某些反射区域亮度非常高甚至把背景完全盖住。这些情况都会让网络很难从局部信息里区分哪些像素属于哪一层。传统优化方法面对这种病态问题通常会加一些手工先验比如反射层更平滑、背景层更锐利、反射文字具有特定边缘分布等。但真实场景的样本空间远大于手工先验能覆盖的范围所以这类方法的泛化能力一直不太行。后来大家开始用深度网络直接回归背景层虽然效果提升明显但单分支模型往往只学到了“把反射压暗”而不是“把反射去掉”。1.3 传统方法为什么不通用早期比较常见的做法是利用多张图像或者额外信息来“解毒”。比如改变拍摄视角多拍几张用视差区分背景和反射或者用偏振片拍摄利用反射光和透射光偏振方向不同来分离再比如用闪光灯拍一对“带闪/不带闪”图像。这些思路在受控环境下确实有效但落地到手机拍照、监控摄像头或者车载相机上就非常痛苦。普通用户不会带着偏振片出门单摄像头也没有多视角信息闪光灯在博物馆等场合还可能被禁止。所以近年来的研究重点转向了单图反射去除只给一张图让网络自己推断反射层和背景层。CoRRN 就是在这个背景下出现的。它的核心卖点不是简单换一个更强的 Backbone而是换了一个“任务定义”让网络同时预测两个输出并且让这两个输出在训练和推理时互相协作。2. 协作式拆解CoRRN 解决问题的“第一性原理”2.1 先想清楚我们要网络“擦除”还是“分解”单图反射去除常见的做法有两类。第一类是端到端单分支回归输入 I输出 B_hat中间靠注意力机制去定位反射区域。第二类是显式估计 R然后让 B_hat I - R。第一类实现简单但解释性弱网络会把那些看起来像反射的区域“糊掉”而不是真正分离出反射内容第二类逻辑清楚但 R 估计不准会直接污染 B 的输出。CoRRN 选择的是“分解”路线同时输出 B_hat 和 R_hat。表面上看这只是多了一个输出头其实意义很大。当网络知道它必须显式解释“多出来的那一层像素去哪了”时它就不会肆无忌惮地把 I 复制成 B_hat。反射层的存在相当于给网络增加了一个“解释压力的出口”让网络必须把一部分能量分给 R。我读这篇论文时印象最深的就是把这种“双分支分解”的能力从结构层面进一步放大。它不满足于两个分支各干各的而是让两个分支在特征提取阶段就反复交流。背景分支知道反射分支发现了什么反射分支也知道背景分支目前在哪个区域最有把握这种互相矫正的思路就是标题里 Cooperative 的核心。2.2 协作式双分支设计的三个层次按照我的理解CoRRN 的协作机制可以拆成三个层次缺一个效果都会明显下降。第一层是任务层协作。模型同时预测背景层和反射层在损失函数里同时监督这两个输出让两个任务共同约束编码器。这样编码器学到的特征必须同时满足两个任务的需求不会只朝“背景重建”一个方向偏。第二层是特征层协作。两个分支在各尺度特征图上不是独立的而是通过协作调制模块交换信息。背景分支可以把“我有把握这里是背景边缘”的信号传给反射分支反射分支也可以把“我检测到一片高亮区域可能来自光源倒影”的信号传给背景分支。有了这种信息交换网络对遮挡区域和低对比度区域的判断会稳很多。第三层是物理一致性协作。模型输出的 B_hat 和 R_hat 相加之后应该能还原输入 I这一点作为一项硬约束加入训练。这个约束非常关键它防止两个分支“互相甩锅”背景分支把难区域推给反射分支反射分支又把难区域推给背景分支。如果没有一致性约束两个分支很容易出现竞争式干扰而不是协同分工。2.3 和普通多任务网络的区别可能有人会说输出两个头再加几个损失这不就是普通多任务学习吗区别在于普通多任务学习的两个任务往往是同一个输入下的不同属性比如语义分割和深度估计而反射去除的两个输出在物理上有明确的加和关系。CoRRN 利用了这个关系把“任务相关性”直接建模到特征交互和损失约束里。另外普通多任务网络通常会共享绝大部分编码器只在最后分头两个任务之间的信息交换基本靠共享参数隐式完成。CoRRN 在解码器阶段还保留了两条独立的上采样支路并且每个尺度上都加显式交互。这样做的理论动机是背景和反射虽然共享输入但它们的纹理统计、边缘分布和语义内容差异很大如果完全共享参数网络很难同时表达两类差异巨大的特征如果完全不共享计算量又太高而且两个分支会各自走偏。协作式设计正好卡在“共享过多”和“共享过少”之间。3. 网络结构拆解双分支怎么搭特征怎么协作3.1 整体流程共享编码器两个解码器CoRRN 的整体框架可以用一条主线概括输入单张图像先过一个共享编码器提取多尺度特征然后把最深层特征分别送入背景解码器和反射解码器每个尺度的解码特征在融合之前都会过一遍协作调制模块。用伪代码表示大概是这样的逻辑class CoRRN(nn.Module): def __init__(self): super().__init__() self.encoder Encoder(out_channels[64, 128, 256, 512]) self.coop_module CoopModule(in_channels512) self.b_decoder Decoder(in_channels512) self.r_decoder Decoder(in_channels512) def forward(self, x): feats self.encoder(x) # 多尺度特征 f feats[-1] # 最深特征 f_b, f_r self.coop_module(f) # 协作调制后的两路特征 b_hat self.b_decoder(f_b) r_hat self.r_decoder(f_r) return b_hat, r_hat当然真实结构里的编解码器会更复杂通常包含残差块、上采样、跳连、注意力模块等。但论文阅读阶段先抓住主线条就够了共享编码器负责把 I 转成高层特征两个解码器负责把特征分别翻译成 B 和 R协作模块负责在特征空间里同步两个分支的“观点”。我个人很喜欢这个结构的一个设计细节两个解码器是结构对称的但不共享权重。因为背景和反射的视觉模式差异太大如果强行共享解码器参数模型容量不够分如果完全不共享训练时两个分支很容易各自为政。对称但不共享配合协作模块既保证了表达能力又留下了交互通道。3.2 协作调制模块的细节协作调制模块是整篇论文的“题眼”。它的输入是同一尺度下背景分支特征和反射分支特征输出是各自调制后的特征。模块不改变特征尺寸只对特征内容进行重新加权。我估计论文里的实现类似这样把两路特征拼接起来通过一个小卷积网络生成两个注意力权重图分别作用到背景特征和反射特征上。用公式表达就是$$ F_B F_B \otimes \sigma(\phi_{B}([F_B, F_R])) $$ $$ F_R F_R \otimes \sigma(\phi_{R}([F_B, F_R])) $$其中 $\otimes$ 表示逐元素相乘$\sigma$ 是 Sigmoid 激活$\phi_{B}$ 和 $\phi_{R}$ 是两个小卷积块。这个过程在训练初期就可以帮助背景分支有选择地“忽略”那些反射特征更强的区域同时让反射分支知道哪里是背景纹理哪里是倒影边缘。为什么需要注意力而不是简单相加因为相加会把两个分支的特征混在一起无法体现选择性。反射区域对背景重建没有帮助甚至有害网络应该学会把反射区域的权重压低反过来平坦背景区域对反射估计的意义也不大反射分支应该集中精力在高光边缘、文字倒影等区域。注意力机制正好提供了这种“按像素动态路由”的能力。3.3 重建一致性防止两个分支“互相甩锅”损失函数部分通常大家都会关注 L1 和感知损失但我觉得 CoRRN 里最值得玩味的是一致性约束$$ \mathcal{L}_{consistency} | \hat{B} \hat{R} - I |_1 $$这个约束看起来只是在重复物理模型但它对双分支训练有很强的稳定作用。没有这个约束时背景分支和反射分支可能会出现一种奇怪的竞争背景分支把所有难还原的区域全部解释成反射反射分支则把所有难分离的区域全部解释成背景最后两个分支的损失各自都不高但分解结果完全是错的。加了这个约束后两个分支必须保证输出之和能还原输入等于把“分工”的标准从“各自拟合标签”变成了“作为一个整体解释输入”。这是协作式设计里非常聪明的一笔也是我在复现时最容易忽略的一环。很多双分支网络效果不稳定不一定是网络结构不够强而是缺少这条物理一致性的锚。4. 训练策略与损失函数让网络学会计分工4.1 损失函数清单与权重设置CoRRN 的训练损失可以分成四组我按权重从高到低列一下。损失项监督目标作用重建损失$|\hat{B}-B_{gt}|1 \lambda_r|\hat{R}-R{gt}|_1$让输出贴合金标准确值一致性损失$|\hat{B}\hat{R}-I|_1$保持输出与输入物理自洽感知损失背景层在预训练网络中的特征差异提升视觉纹理真实感边缘结构损失背景层梯度域差异保留锐利边缘防止过度平滑反射层的 L1 权重 $\lambda_r$ 通常要小于背景层的权重。我自己的经验是设在 0.4 到 0.8 之间比较合适。原因很简单反射层整体能量低如果给它和背景层一样的 L1 权重模型会觉得“与其费力预测反射细节不如把反射输出全部置零”这样 L1 损失也能降下来。给反射层稍微低一点的权重同时用一致性损失兜底能避免反射分支偷懒退化。感知损失我建议作用在背景分支上不建议作用在反射分支上。反射层常常是虚像不需要“拟真”的语义纹理强行加感知损失反而会让反射层生成一些不存在的结构。边缘梯度损失则两边都可以加尤其对反射分支有很大帮助因为反射层的文字、窗框、灯带都有明显的边缘梯度监督能让反射分支更敏感。4.2 训练数据与数据增强反射去除的最大训练难点是真实数据难以获得。同一场景需要拍摄“无玻璃背景”和“有玻璃观测”两张图才能构成训练对这在真实场景里需要人工摆放和后期筛选工作量巨大。所以论文里的常规策略是用合成数据做预训练再用少量真实数据做微调。合成数据的思路是取一批自然图像当背景取另一批包含文字、高光、建筑边缘的图像当反射源通过随机变换后做叠加。需要注意几个问题。第一反射图不能是简单的全局叠加应该对反射层做随机缩放、旋转、透视扭曲和轻微高斯模糊模拟不同距离和曲率下玻璃倒影的形变。第二反射层的亮度分布要随机化有些样本让反射特别强有些反射特别弱迫使网络学会自适应。第三背景和反射之间不能完全没有空间重叠规律否则网络会退化成学一个简单阈值。真实数据微调时如果没有反射层标签可以把损失函数里的 R_gt 项去掉只用背景层损失加一致性损失。因为一致性损失只依赖观测图和背景输出不需要反射标签这给了双分支模型一个很好的半监督扩展空间。4.3 优化参数与训练节奏训练优化器常用 AdamW基础学习率设置在 $10^{-4}$ 这个量级batch size 看显存来定8到16之间比较常见。学习率用余弦衰减或者阶梯下降都可以。关键点是训练节奏如果从零开始就在真实小数据集上训练网络很快会过拟合背景区域学得不错但反射区域完全失控。建议先用大量合成数据训练足够多的轮次把协作模块和反射分支的基本能力培养起来再切到真实数据用低学习率微调。还有一个训练细节容易被忽略输入图像的分辨率。反射去除对高频边缘非常敏感patch size 太小会丢失反射目标的结构信息我一般把训练 patch 设到 $256 \times 256$ 以上反射层较轻薄时甚至用 $320 \times 320$。过小的 patch 会让网络只学会刻板地压暗高光而不是理解反射内容的整体形状。在推理阶段CoRRN 这种全卷积网络可以直接处理整张任意尺寸的图像不需要滑动窗口。如果显存有限可以用半精度推理或者把大图切成带重叠块的分块处理但输出后要记得对重叠区域做融合避免接缝。5. 实验效果与消融协作模块到底值多少5.1 定量结果怎么看论文的实验部分通常会在公开数据集上报告 PSNR、SSIM 和 LPIPS 等指标。我读的时候更关注两类对比第一类是和单分支直接回归方法比看“显式分解加协作”是不是真的比直接回归强第二类是和自己去掉协作模块后的变体比看协作设计本身贡献了多少。从作者报告的趋势来看协作式双分支相对单分支回归在 PSNR 上一般有 2 到 4 dB 的提升在 LPIPS 这种感知指标上提升更明显。这个结果符合直觉PSNR 主要衡量像素级逼近程度单分支回归把背景压暗一点就能换来不错的值但 LPIPS 评价的是视觉感知相似度反射残留造成的“鬼影感”和边缘伪影都会让感知分数大幅变差。所以如果只盯着 PSNR很容易低估协作式设计的价值。另外真实数据上的提升往往比合成数据上更明显。因为真实玻璃场景的反射层形式更多样合成数据里训练的单分支网络很容易把真实反射当成普通纹理保留下来。而双分支协作模型因为有反射分支在持续监测异常区域能够更好地泛化到没见过的新型反射。5.2 消融实验最有信息量消融实验是这篇论文里我最喜欢看的部分。如果作者做了三组实验结果通常会是这样实验设置对效果的影响去掉反射分支背景输出明显变“脏”反射残留严重双分支但去掉协作模块两个分支各自收敛但分解结果不稳定背景和反射串扰双分支协作模块一致性损失背景干净反射清晰整体最优去掉反射分支的效果好理解网络失去了显式解释“多余像素”的出口。去掉协作模块的效果更加微妙两个分支各自训练时都能把各自的损失降到很低但是由于两个分支的信息没有交流背景分支遇到的困难反射区域无法告知反射分支集中处理反射分支检测到的高亮区域也无法帮助背景分支避开干扰。最终结果是背景输出时有残留、时有过平滑反射输出则会产生很多伪纹理。一致性损失的消融也值得注意。去掉之后背景分支和反射分支的训练损失可能都会更低但分解结果的正确性会变差出现“背景把真实纹理让给反射层”的典型错误。我在自己复现时也遇到过这个现象背景输出特别干净干净到把背景里原本应该存在的纹理也删掉了打开反射输出一看那些纹理全被分到了反射层。一致性损失就是用来压制这种错误的。5.3 视觉结果里的成败案例视觉结果里表现最好的通常是反射物比较薄、反射内容边缘清晰、背景本身纹理丰富的情况。比如玻璃柜里的展品、车窗后的路牌网络能够把倒影里的文字轮廓从背景上抠掉同时保留背景本身的锐利边缘。失败案例也有很强的规律当反射层亮度极高、大面积覆盖背景时背景信息基本丢失网络只能“脑补”被遮挡的内容脑补出来的背景会有明显涂抹感。另外当背景本身就有类似玻璃的高光和边缘时网络可能把真正属于背景的高光误判成反射造成误删。这也是目前所有单图反射去除方法共同的天花板CoRRN 并没有完全解决只是在多数场景下把失败区域压缩得更小、更局部。6. 影响范围与应用场景反射去除能用到哪6.1 手机影像与文档扫描离普通用户最近的落地场景一定是手机拍照。很多人拍车窗风景、商场橱窗、博物馆展品照片里总有那么几道讨厌的反光。CoRRN 这种模型可以直接集成到相机的夜景模式或者编辑模块里作为“去反光”功能一键处理。不过手机端的算力有限原版模型的参数量和推理耗电不一定能直接吃下来实际落地大概率要裁剪、蒸馏或者只在云端处理。文档扫描是另一个非常实用的场景。办公场景里经常遇到把纸质文件压在玻璃台面上扫描结果扫出来一层天花板灯光的倒影。反射去除模型用在扫描类App里可以直接提升 OCR 识别准确率。这里有一个额外好处文档上的文字天然就是边缘密集区域和反光中的文字在结构上容易混在一起但如果双分支模型能把反射层单独分离出来OCR 系统可以直接使用分离后的背景层效果远比在含反射原图上做增强好。6.2 监控安防与车载视觉监控摄像头经常会隔着玻璃门或者防护罩拍摄。玻璃防护罩上落灰、起雾、反光都会影响画面质量。用上反射去除之后监控画面里的人物和车牌能更干净一些后续的视频分析模块也更容易工作。车载场景更有意思。行车记录仪或者自动驾驶摄像头如果安装在挡风玻璃后面玻璃上的倒影和仪表盘反射会成为视觉感知的干扰源。反射去除可以作为感知前处理的一部分在送入目标检测或车道线模型之前把反射层剥离掉。这个场景的难点是实时性要求极高车载芯片上的算力预算通常只有几十毫秒所以模型 lightweight 化是关键。6.3 对人脸识别、物体检测等下游任务的影响反射去除的研究有时候会被误认为只是“照片美化”但它对下游视觉任务的影响其实很大。人戴眼镜时镜片上的反光会挡住眼睛区域影响人脸关键点检测和人脸识别隔着玻璃拍摄的行人图像反射层会让检测框发生偏移或者置信度下降。把反射去除放在检测和识别模型前面作为预处理能够显著提升这些任务在真实复杂环境下的鲁棒性。另一个影响范围是图像生成领域。反射层分离本身就是一种“可控解耦”如果把这个思路推广到其他图层分离任务比如阴影去除、雨纹去除、水下散射去除协作式双分支的思想都是可以迁移的。它不局限于“玻璃反射”这一个物理场景而是给出了一种通用的“混合信号分解”框架。7. 复现心得与避坑指南从论文到可以跑的模型7.1 最小可复现的协作网络伪代码如果你想快速验证协作式反射去除的基本思想不需要一开始就复现论文的完整结构。可以先在单卡上搭一个简化版我给出的伪代码可以直接作为起点import torch import torch.nn as nn class CoopModule(nn.Module): def __init__(self, in_ch): super().__init__() self.merge nn.Sequential( nn.Conv2d(in_ch * 2, in_ch, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(in_ch, in_ch * 2, 3, padding1), nn.Sigmoid() ) def forward(self, fb, fr): w self.merge(torch.cat([fb, fr], dim1)) wb, wr w.chunk(2, dim1) return fb * wb, fr * wr class SimpleCoRRN(nn.Module): def __init__(self): super().__init__() base 64 self.encoder nn.Sequential( nn.Conv2d(3, base, 3, padding1), nn.ReLU(), nn.Conv2d(base, base * 2, 3, padding1), nn.ReLU(), nn.Conv2d(base * 2, base * 4, 3, padding1), nn.ReLU(), ) self.coop CoopModule(base * 4) self.b_dec nn.Sequential( nn.Conv2d(base * 4, base * 2, 3, padding1), nn.ReLU(), nn.Conv2d(base * 2, base, 3, padding1), nn.ReLU(), nn.Conv2d(base, 3, 3, padding1), nn.Sigmoid(), ) self.r_dec nn.Sequential( nn.Conv2d(base * 4, base * 2, 3, padding1), nn.ReLU(), nn.Conv2d(base * 2, base, 3, padding1), nn.ReLU(), nn.Conv2d(base, 3, 3, padding1), nn.Sigmoid(), ) def forward(self, x): f self.encoder(x) fb, fr self.coop(f, f) return self.b_dec(fb), self.r_dec(fr)这是我用来做思想验证的最小实现没有多层跳连也没有多尺度交互但已经能跑通训练闭环。你可以在这个基础上往深了加编码器换成残差块解码器加上跳连协作模块在多个尺度都插入。每加一个组件就做一次消融对比这样你对 CoRRN 里的每个设计为什么存在会有更直观的认识。7.2 我踩过的训练坑复现反射去除模型踩坑是必然的。我把最常见的几个问题整理成经验第一反射分支很容易退化输出全黑。原因是反射层整体亮度低L1 损失天然鼓励网络输出零值。解决办法是给反射分支的损失降低权重同时把反射层的监督信号归一化到合理范围或者额外加一个反射层梯度损失强制反射分支至少输出边缘结构。第二直接复制论文的损失权重不一定合适。不同合成数据集的反射强度分布差异很大如果数据里反射普遍较弱λ_r 可以降到 0.3 以下如果反射强λ_r 可以提到 1.0。我会把权重当成超参数在小验证集上做简单搜索不要迷信论文原始配置。第三BatchNorm 在反射分支上经常出问题。因为反射层在很多区域是零值BatchNorm 统计出来的均值和方差会在训练和推理之间漂移。改用 GroupNorm 或者 InstanceNorm 通常更稳定尤其当 batch size 不大时。第四数据加载的 pipeline 直接影响反射层分辨。如果为了方便把所有训练图缩到很小反射层里的文字细节会被直接抹掉网络根本不可能学会从边缘信息里分离反射。输入patch尺寸尽量大同时不要在加载阶段做太强的下采样。第五训练初期不要过早加入对抗损失。对抗损失梯度不稳定如果协作模块还没学会基本分工对抗损失会把整个训练带偏。我习惯先用纯 L1、一致性损失和感知损失把模型稳定下来最后再加对抗损失微调。7.3 如果在此基础上继续做我会优先试的三件事第一件事是多尺度协作模块的轻量化。原版网络在多个尺度上都插协作模块效果肯定好但计算量不小。我想试试只在编码器最后两个尺度做协作前面的尺度只做普通特征提取看看精度能保住多少。这个改动对手机端部署很有意义。第二件事是把一致性损失改造成一个可学习的融合约束。物理叠加模型 I B R 是近似成立的真实玻璃还有透射衰减和多次反射。我可以让网络额外预测一个透射系数 T把模型改成 I T × B R其中 T 可以随空间变化。这样对有色玻璃、贴膜玻璃等场景可能更稳。第三件事是利用视频时序信息辅助单图训练。当前模型完全靠空间结构去猜反射如果能用视频中相邻帧的运动差异构造伪标签就能在更多真实场景下做自监督微调减少对成对数据的依赖。这个方向如果做出来反射去除的真实数据瓶颈就能缓解不少。我读完 CoRRN 之后最大的感受是想解决反射去除先要重新定义问题而不是一上来就换更大的网络。把“直接擦反射”变成“同时分解背景与反射”再给两个分解结果加上物理一致性的约束这才是它真正值钱的地方。如果你也在做这个方向建议你先把最小协作网络跑通再做一组去掉协作模块的消融实验。自己亲手对比一次比看十篇论文的表格都有用。