拓冰建站拓冰建站
首页 / 资讯中心 / 正文

相对总变分RTV:从纹理中提取结构的高效图像平滑算法

1. 为什么要把“结构”从“纹理”里抠出来先说个我自己的经历。前几年做图像卡通化的小项目输入一张风景照想输出类似手绘风格的扁平色块。最头疼的不是颜色量化而是草地的噪点、树叶的细碎光斑、瓷砖墙的花纹这些纹理它们会让量化后的色块变得脏兮兮的。我当时试了一圈平滑算法最后才找到这篇 SIGGRAPH Asia 2012 的文章——李徐Li Xu等人的《Structure Extraction from Texture via Relative Total Variation》RTV。一句话概括它做的事把图像里“大尺度的结构边缘”保留下来同时把“小尺度的重复纹理”尽可能抹平。这个需求在图形图像领域比你想的更普遍。边缘检测希望只输出物体轮廓而不被纹理干扰分割算法希望输入的是干净的色块图而不是满屏颗粒抠图、线稿提取、图像融合、seam carving 预处理全都受益于先把结构从纹理中剥离。这篇文章值得所有做图像处理、计算机视觉、甚至游戏美术工具链的人认真读一遍。1.1 纹理和结构本质区别在哪做算法的人最喜欢问一句话你的特征量到底区分的是什么纹理和结构在视觉上不难分但数学上的差异并不是“梯度大小”。砖墙的缝隙和墙面的边界都是高梯度草地上一根叶子的边缘和远处山峰的轮廓也都是高梯度单纯靠梯度的绝对值没法把它们分开。RTV 真正抓住的特征是两个方向一致性和空间分布。结构边缘附近所有像素的梯度方向基本一致。一条竖直的房檐边缘它周围的梯度基本都指向水平方向而且符号一致——亮的一侧统一偏左暗的一侧统一偏右。纹理区域则相反砖缝、草叶、布纹这些元素的方向随机、正负交替在局部窗口内互相抵消。一篇纹理杂乱的图像局部窗口里的梯度向量就像一堆各奔东西的箭头一条干净的结构边窗口里的梯度向量则像一队整齐行进的士兵。这篇论文的核心贡献就是把“方向一致/互相抵消”这件事变成了一个可以求导、可以放进优化框架里的量——相对总变分。1.2 这篇文章到底解决了什么问题在 RTV 之前做纹理结构分离的主流办法有双边滤波、导向滤波、L0 平滑各有各的缺陷。双边滤波和导向滤波本质上是局部加权平均只对“低对比度纹理”有效遇到强纹理、大幅度的纹理会把它们当成边缘保下来。L0 平滑是全局稀疏约束能去掉低幅度的细节但纹理如果对比度较高它也会犹豫容易把纹理的边缘和结构的边缘一起搞成“阶梯”。RTV 的思路和前面都不一样它不是在“平滑”和“保边”之间做折中而是显式地建模“什么是纹理、什么是结构”。先定义问题再设计特征最后优化求解。这也是这篇文章后来被大量引用、被做成各种变体的原因——它把“纹理结构分离”从一个调参活变成了一个有明确数学定义的问题。文章里用大量实验证明了它在卡通化、去纹理、边缘检测等任务上的一致性表现这也是我敢把它用进生产管线的原因。2. RTV的核心思想窗口内两种梯度的比值2.1 窗口总变分与窗口固有变分先定义两个量分别叫 windowed total variation 和 windowed inherent variation中文可以叫“窗口总变分”和“窗口固有变分”。窗口总变分记为 D就是我们熟悉的梯度绝对值在局部窗口内的加权和D_x(p) Σ g(p,q) · |∂_x S(q)|其中 g(p,q) 是空间高斯权重离中心像素 p 越远的像素贡献越小求和范围是以 p 为中心的一个窗口。D 度量的是窗口内所有像素的梯度幅度总和可以理解为这个窗口里“变化的剧烈程度”。窗口固有变分记为 L则不一样它是先把窗口内的梯度加起来再取绝对值L_x(p) | Σ g(p,q) · ∂_x S(q) |注意差别一个是“先取绝对值再求和”一个是“先求和再取绝对值”。就这么一个顺序差别正好抓住了纹理和结构的本质差异。如果窗口内的梯度方向一致、符号相同求和之后不会抵消L 会比较大。如果窗口内的梯度方向乱七八糟、正负交错求和之后大部分都抵消了L 会很小。2.2 相对总变分比值才是真正的判别量RTV 的精髓就是用这两个量的比值作为纹理判别项R D_x / (L_x ε) D_y / (L_y ε)ε 是一个极小的常数防止除以零。对纹理窗口D 大、L 小比值 R 很大对结构边缘窗口D 和 L 同量级地大比值 R 相对小。把这个比值作为正则项放进能量函数里优化过程就会主动惩罚“R 大的像素”——也就是纹理区域而结构边缘的惩罚很小因此被保留。放到生活里类比一下D 就像一群人一会儿往前跑一会儿往后跑的总步数L 是他们最终位移的绝对值。一堆人原地乱跑总步数巨大但位移为零一支队伍整齐前进总步数和位移差不多大。用“总步数除以位移”这个比值就能轻松区分“乱窜”和“挺进”。我当初第一次看到这个设计时觉得这真是把一个问题想透了才写得出来的表达式。2.3 目标函数与优化框架把上面的思想写成完整的能量函数min S → Σ_p (S(p) - I(p))² λ · Σ_p [ D_x(p)/(L_x(p)ε) D_y(p)/(L_y(p)ε) ]第一项是保真项要求输出 S 不能偏离输入 I 太远第二项是 RTV 正则项λ 控制两项的权重。整个优化是让 S 在“贴近原图”和“去除纹理”之间找一个平衡。这个目标函数里 D、L 都包含未知的 S 的梯度所以不是一个直接能解的二次问题。论文的做法是固定点迭代在每一步里把 D/(Lε) 当成已知的权重用当前的 S 估计去算这样目标函数就变成了关于 S 的加权最小二乘转成一个稀疏线性方程组解完更新 S再重新算权重如此迭代三四次就收敛。实际实现中方程组的形式是 (I λA)S I₀其中 A 是一个由 RTV 权重构造的稀疏矩阵可以用共轭梯度法或高斯-赛德尔迭代求解。3. 从公式到代码完整实现流程3.1 前置准备与梯度计算动手写代码之前先把变量说清楚。输入是一张灰度图 I彩色图可以转 YUV 或者直接在 RGB 三个通道分别处理后面会讲输出是平滑后的结构图 S。第一步是算梯度。建议用中心差分而不是前向差分中心差分对噪声稍微稳一点而且梯度对称。以 x 方向为例Ix(i,j) (I(i,j1) - I(i,j-1)) / 2y 方向同理。注意边界补零或者复制。第二步是构造空间高斯权重 g(p,q)。窗口半径根据 σ 定一般取 ceil(3σ) 左右。标准的做法是生成一个和窗口同尺寸的高斯核然后对 D 和 L 的计算做卷积。为了速度可以把二维高斯核拆成行列两个一维核分两次卷积。这一步在 OpenCV 里就是 getGaussianKernel 然后两次 filter2D在 MATLAB 里就是 fspecial(gaussian) 加 conv2。不要小看这个预处理窗口半径如果算错后面的所有数值都会偏移。3.2 计算窗口总变分和窗口固有变分有了梯度和高斯核D 和 L 的计算非常直接D_x conv(gauss, |Ix|) D_y conv(gauss, |Iy|) L_x | conv(gauss, Ix) | L_y | conv(gauss, Iy) |L 的计算是“先混叠再取模”千万不能先取绝对值再卷积那样就退化成了 D整个算法就废了。我见过有同学刚上手时在这里写反出来的结果和普通高斯平滑没什么区别还百思不得其解。其实这个顺序问题恰好是整个算法的命门D 衡量局部梯度的总强度L 衡量局部梯度的净和两者结合才能把“方向抵消”的信息提取出来。算完之后RTV 正则权重 w D_x/(L_xε) D_y/(L_yε)。这个 w 是一个和原图同尺寸的权重图纹理区域 w 大结构边缘 w 小后续的线性系统完全由它驱动。3.3 稀疏线性系统的构造与求解固定 w 之后目标函数变成关于 S 的加权最小二乘问题min S → Σ_p (S_p - I_p)² λ Σ_p w_p ( (∂_x S)_p² (∂_y S)_p² )注意这里把 RTV 正则项的绝对值形式近似成了平方形式。对 S 求导并令其为零得到一个类似于各向异性扩散的稳态方程S - λ · div(w · ∇S) I离散化之后就是一个五对角的稀疏矩阵。实际写代码时把算子的系数直接填到 scipy.sparse 或 MATLAB 的 sparse 矩阵里然后用共轭梯度法求解。不少人第一次写到这里会卡壳因为要手动处理“对每个像素取梯度、再把权重乘进去、最后散度回来”这一整套索引关系。我的建议是先画一个 5×5 的小图把每个像素对应的矩阵行手写一遍理解了规律再上完整图。加快收敛的小技巧把初始解 S 设成输入 I权重 w 用输入图像的梯度来算。迭代一次之后w 用新的 S 重新算。论文里一般迭代 2 到 3 次结果就稳定了继续迭代变化很小但耗时翻倍性价比不高。3.4 一套可直接上手的核心流程我给出一个 Python 风格的伪代码流程方便理解核心链路# S: 当前估计初始化为输入图 Ifloat 类型 [0,1] 归一化 for it in range(3): Sx center_diff(S, axisx) # 中心差分梯度 Sy center_diff(S, axisy) Dx gauss_filter(abs(Sx), sigma) # 高斯卷积 Dy gauss_filter(abs(Sy), sigma) Lx abs(gauss_filter(Sx, sigma)) Ly abs(gauss_filter(Sy, sigma)) w Dx / (Lx eps) Dy / (Ly eps) A build_sparse_operator(w) # 离散化 -div(w * grad) S solve_linear_system(I lam * A, I) # CG 或直接法用 Python 写的话梯度、高斯滤波用 OpenCV 的 filter2D 和 Sobel 就能搞定稀疏矩阵用 scipy.sparse 构建再调 scipy.sparse.linalg 里的 cg 方法求解。图像尺寸在 1000×1000 以内这个流程单次迭代大概一到两秒完全可接受。如果只做一次离线处理用 spsolve 直接求逆也可以省去折腾预条件子的麻烦。4. 参数调优与实践效果对比4.1 三个核心参数的物理含义RTV 有三个参数要调λ、σ、ε外加迭代次数。λ 是正则项的权重λ 越大去纹理越狠但结构边缘也会同步变钝。论文的默认值是 0.01我实际测下来0.008 到 0.02 是比较合理的工作区间。纹理很重比如密集的砖墙、树叶往 0.015 以上调纹理本身已经比较弱比如磨砂材质降到 0.008 左右再低就没什么效果了纯粹变成轻微模糊。σ 决定窗口大小也就是“多大尺度的纹理算纹理”。σ 越小窗口内梯度越容易方向一致一些大纹理块可能被当成结构σ 越大去除的纹理尺度越大但大结构边缘也会被影响。论文推荐 σ3这个值适合大多数自然图像。如果你的目标纹理很细密比如布纹、木纹σ2 就够如果目标纹理像砖墙那样有周期性的大花纹可能要 σ5 以上。还要注意σ 与图像分辨率强相关处理 4K 大图时要把 σ 适当放大否则原本的细纹理在更高分辨率下会被当成结构保下来。ε 只是避免除零的常数一般取 1e-5 到 1e-3。取太小在 L 接近 0 的纯平坦区域会出现数值上的大权重容易产生局部过冲取太大会把 RTV 的判别能力稀释掉。我通常取 1e-4表现稳定。迭代次数默认 3 次。第一次迭代是最关键的权重的初始估计决定了算法的基本走向第二第三次是修正。不要在第一次迭代就为了追求效果把 λ 拉得很大那样权重自己就不稳定了。宁可 λ 保持正常值多迭代一次。4.2 和双边滤波、L0平滑的实际对比我拿一组室内照片做过对比。输入是带瓷砖和木纹的场景目标是把墙面结构和家具轮廓提取出来。双边滤波的结果是细纹理被压下去一部分但瓷砖缝这种高对比度的纹理被当成了边缘保留了大量“假结构”而且靠近真实边缘时容易出光晕这是双边核在高对比度区域的固有毛病。L0 平滑的结果整体干净很多但有两个问题。一是平坦区域被“分段常数化”容易出现明显的阶梯感后续做边缘检测时会出现很多假轮廓二是纹理对比度一高L0 就分不清纹理和结构会留下残影。RTV 的结果纹理几乎被抹平而窗边、桌沿这类结构边缘保持锐利。与前面两种方法最关键的差异是RTV 的结果没有阶梯感因为它不是把梯度强行置零而是通过比值惩罚让纹理区域的梯度变小但不至于完全封死过渡更自然。如果你要做卡通化这种自然过渡比 L0 的硬分段更适合后处理。4.3 调参过程中容易被忽略的细节我踩过的坑里最有代表性的一个是对彩色图像的处理方式。直接对 RGB 三个通道分别跑 RTV容易在色块边界上出现颜色渗透因为三个通道的纹理强度不一致平滑程度不同。更好的做法是把图像转到 YUV 或 Lab 空间对亮度通道跑 RTV色度通道做较轻的普通高斯模糊即可输出时再转回 RGB。纹理主要影响亮度色度通道不需要强平滑这样能最大限度保留原色彩真实性。另一个容易被忽略的点是梯度和高斯核的边界处理。卷积推荐用 reflect 模式而不是 zero padding否则边缘处权重大幅变化图的第一圈像素会发黑或者发亮。这个细节在图像中心看不出来但一旦你 crop 图像的边缘区域问题立刻暴露。我曾在批处理大量小图时因为这个吃了大亏跑出来的缩略图四周全是一圈暗边排查了很久才找到原因。5. 常见问题与排查实录5.1 纹理去不干净怎么办现象是输出里还有明显的纹理残影尤其是对比度高的周期纹理。排查顺序先确认 λ 是否到位一般要 0.015 以上再确认 σ 是否匹配纹理尺度如果单个纹理单元的尺寸接近 σ它是不会被当作纹理的最后检查权重计算里的 L如果 L 的卷积核与 D 不一致也会破坏比值。有个容易被忽略的点图像本身的对比度和动态范围。如果输入图整体偏暗或者对比度被拉满D 和 L 的数值比例会变化同样的 λ 效果差很多。处理前把图像归一化到 [0,1] 区间会比直接用 0~255 的 uint8 稳定得多。我习惯在管线入口统一做归一化后面所有算法都按浮点算最后再转回 uint8。5.2 结构边缘被削平、发虚如果输出图里桌椅轮廓、人物边缘都变得雾蒙蒙的多半是 λ 或 σ 偏大了。降低 λ 是一个方向更有效的做法是缩小 σ让窗口内更偏向于“局部方向一致”这样结构边缘的 L 可以保持较大比例不会被高估。还有一种边缘发虚的隐蔽原因迭代次数过多。权重每次更新都会对边缘产生一次“侵蚀”迭代三次和迭代六次相比边缘清晰度差异肉眼可见。如果你只关心边缘保留把迭代次数固定在 2 次用稍大的 λ 去补去纹理力度。实测下来这个组合在绝大多数场景都比“少 λ 多迭代”的效果好。5.3 计算慢怎么优化RTV 的复杂度瓶颈在稀疏线性系统求解。三个思路第一降采样。把长边缩到 1000 像素以内跑 RTV得到平滑结果后上采样回原尺寸再做一次轻量的导向滤波把细节贴回去。这个组合拳在工程里非常实用处理 4K 图也能控制在几秒内。第二把高斯卷积换成积分图加速或用可分离滤波二维高斯可分离复杂度从半径平方降到线性。OpenCV 的 sepFilter2D 在这里明显比 filter2D 快。第三求解器换用带预条件处理的共轭梯度。我在 2000×2000 图上实测带对角线预条件子的 CG 比默认 CG 快一倍以上。如果矩阵结构固定、多个输入只改右侧常向量还可以预先做一次矩阵分解后面每次求解就是一次回代速度提升非常明显。5.4 常见问题速查表现象可能原因解决办法纹理残留明显λ 过小或 σ 小于纹理尺度增大 λ 到 0.015或增大 σ边缘发虚λ 过大或 σ 过大减小 λ同时把迭代次数降到 2平坦区域出现斑点ε 过小把 ε 从 1e-6 调到 1e-4图像边缘发黑/发亮卷积边界用了 zero padding改成 reflect 边界模式彩色图有颜色渗透直接对 RGB 分通道处理转 YUV只平滑 Y 通道结果与论文效果差异大没有归一化输入或迭代次数过多输入归一化到 [0,1]迭代 2~3 次6. 应用场景与周边工具6.1 真实项目里 RTV 能帮上什么我实际用 RTV 最多的场景有三个。第一个是边缘检测的前处理。直接用 Canny 处理原图草地、砖墙、树叶能产生成千上万条假边缘先跑一遍 RTV再上 Canny输出干净到可以直接拿去当线稿。这个操作在文档扫描、建筑图纸矢量化、漫画线稿提取里都极其实用。第二个是卡通化与风格化管线。RTV 输出作为区域平滑层再做颜色量化出来的色块既干净又有清晰的轮廓边界。相比直接对原图做 K-means 量化先 RTV 后量化可以杜绝纹理导致的色块噪声。第三个是图像融合和物体替换的辅助。把结构图作为融合权重的基础可以避免纹理在融合过程中产生莫尔纹也能显著减少接缝处的视觉跳变。还有人在做深度图预处理时利用 RTV 保持深度边缘同时抹平深度噪声效果也比纯双边滤波好。学者们在论文里还做了不少扩展应用比如 HDR 色调映射的基底层提取、遥感影像的建筑轮廓提取等。原理都一样先分离结构再针对结构做后续处理。6.2 关于 Intel Texture Works 这类插件的题外话在图形工作者圈子里“处理纹理”这个词很容易让人联想到 Intel Texture Works 这类 Photoshop 插件——它主要用于 BC 格式纹理压缩与 DDS 导出服务于游戏资产管线。这里要提醒一句工具层面的纹理压缩和算法层面的结构提取是两码事。前者解决的是“纹理存下来占用多少显存”后者解决的是“图像里的结构和纹理怎么分开”。如果你在游戏项目里既要压缩纹理又想要干净的轮廓辅助正确的做法是把 RTV 放在预处理阶段Texture Works 这类插件放在资源导出阶段两者不冲突也不可互相替代。理解它们各自的定位能避免在工具选型时把两件不相干的事情混在一起。6.3 扩展思路把 RTV 用在自己的代码库里如果你不想从零实现可以直接使用论文作者公开的 MATLAB 代码或者网上各种 OpenCV/NumPy 移植版。但我的建议是至少自己实现一遍核心的 D/L 计算和稀疏矩阵求解因为真正干活时你会遇到很多作者代码没覆盖的情况比如超大图分块处理、批量处理多张图、动态调整 λ。分块处理时要注意块与块之间的重叠区域直接用硬切会引入接缝。我的做法是每块向外扩 σ×2 的 margin跑完 RTV 后只保留内部区域接缝问题基本消失。批量处理时还可以把整批图像的 λ 做成一个和纹理密度相关的自适应函数先用一个小窗口算出局部梯度方差再映射成 λ 的缩放系数效果比全局固定 λ 好不少。最后再分享一个经验RTV 框架最值钱的不是那个具体公式而是“用窗口内梯度方向一致性来判别纹理”这个思想。我自己后来在做视频去纹理时就是把 RTV 的比值项拿出来做时域引导效果比直接逐帧跑 RTV 稳定得多也避免了帧间闪烁。有时候读懂一篇论文的最好方式不是背诵它的目标函数而是把它拆开、换一个场景重新组装一遍。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门