惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
K
Kaspersky official blog
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
Recorded Future
Recorded Future
Engineering at Meta
Engineering at Meta
U
Unit 42
D
Docker
I
InfoQ
D
DataBreaches.Net
Google DeepMind News
Google DeepMind News
N
Netflix TechBlog - Medium
C
Check Point Blog
The Cloudflare Blog
美团技术团队
V
Vulnerabilities – Threatpost
博客园_首页
T
Threat Research - Cisco Blogs
Google DeepMind News
Google DeepMind News
Attack and Defense Labs
Attack and Defense Labs
A
Arctic Wolf
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Troy Hunt's Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
人人都是产品经理
人人都是产品经理
C
Cyber Attacks, Cyber Crime and Cyber Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Schneier on Security
Apple Machine Learning Research
Apple Machine Learning Research
MyScale Blog
MyScale Blog
P
Privacy International News Feed
云风的 BLOG
云风的 BLOG
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
The Blog of Author Tim Ferriss
GbyAI
GbyAI
The Last Watchdog
The Last Watchdog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
aimingoo的专栏
aimingoo的专栏
P
Proofpoint News Feed
The Register - Security
The Register - Security
博客园 - 三生石上(FineUI控件)
Forbes - Security
Forbes - Security
NISL@THU
NISL@THU
Y
Y Combinator Blog
T
Threatpost
Microsoft Azure Blog
Microsoft Azure Blog
L
Lohrmann on Cybersecurity

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派 万字剖析:千问App深度体验报告(2026) - 少数派 在2026年,如何真正防止别人抄袭你的作品 - 少数派 怎么用 50 块搭个 AI 语音助手?我踩了 3 天坑 - 少数派 YeeroAI:让 AI 对话真正成为知识管理的一部分 - 少数派 爬泰山 - 少数派 「旅图显影」 App 更新:这次,我们补上了一点「手感」 - 少数派 假期出门太折磨?我的 23 条经验帮你规划惬意旅行 - 少数派 工作流会变吗 - 少数派 Claude Opus 4.6 怎么用最省钱?我测了 5 种方案 - 少数派 GPT Image 2 让图文并茂不再稀罕 - 少数派 用户侧出发——什么是AI,我要不要学习? - 少数派 找片、转存、整理、播放一条龙!让你的付费网盘值回票价 - 少数派 欢迎试用!日课一问2.0插件 - 少数派 自己做的MDeditor,原本想购买 Typora 试了两次支付不成功,干脆自己做一个 - 少数派 vibe coding了一个 3MB 的小工具,让 ~/Downloads 彻底告别混乱 - 少数派 因为受不了 Mac 的风扇策略,我做了一个风扇控制工具 - 少数派 别只怪模型 - 少数派 Warp 终端的 AI 功能怎么用?我测了一周的体验 - 少数派 AI 写代码老是出 bug?这 5 个配置我后悔没早知道 - 少数派 「新玩意」苹果出相机可能就这样:Sigma BF + 45mm F2.8 DG Contemporary - 少数派 一个面向2030年的AI操作系统是什么样子的:浅谈cola这款有灵魂的Agent - 少数派 别只看写代码 - 少数派 每天解决10个问题,还是一口气攻坚解决400个? - 少数派 AI 交易机器人怎么搭?我用 Claude 跑了一周实盘 - 少数派 Maptoposter Online:把你爱的城市画成艺术海报 - 少数派 Function Calling 怎么用?我测了 3 个模型发现差距真大 - 少数派 Legend Talk:我做了个 AI 圆桌,让 160 位思想家围着你的问题转 - 少数派 如何找到自己的蓝方?在小县城寻找压力测试 - 少数派 语音输入与软件接口|2026年聊AI时,我们都聊些什么(上) - 少数派 混动已经卖爆,纯电又来补刀——钛7闪充版简直“不讲武德” - 少数派 本月玩什么|朋友收藏、识质存在、沙罗周期 - 少数派 为什么要每天坚持输出? - 少数派 Claude API 挂了好几个小时,你的项目有备用方案吗? - 少数派 Function Calling 没你想的复杂——我用它做了个有点用的工具 - 少数派 登录系统立即播放视频或者图片音乐的软件 - 少数派 我为什么创建 FlipHTML5 下载工具 - 少数派 残局没电?多品牌外设电量统一管理软件EasyBluetooth已支持RTSS游戏内显示以及AIDA64 - 少数派 前往通义路的路 - 少数派 太好看了,媲美Sun的个人导航页,NAS部署星云门户 - 少数派 乌黑嘴唇“一键检测”上线了 - 少数派 派早报:Claude AI 接入多个创意软件生态、FILCO 生产方接手品牌等 - 少数派 【更新】BearCLI、Claude 连接器与 MCP 服务器 - 少数派 记了上千条流水,还是看不懂财务?我做了一个让 AI 读懂账本的工作台 - 少数派 MINI R56 升级原厂 Sport 模式 - 少数派 新玩意 | 一棵柠檬树(仿真版) - 少数派 Momenta的“物理AI”野望,需迈过“含摩量”这道关 - 少数派 网页直接投屏控制手机!NAS一键部署PandaScrcpy,流畅丝滑可远程。 - 少数派 众测|邀你一同探索随身 AI 硬件入口 YoooClaw C·ONE - 少数派 2050大会:分享时间是真诚 参会记 - 少数派 iPad 赋能电影创作:国内首部宣纸手绘长片《燃比娃》的幕后故事 - 少数派 AI的审美:我用 8 个大模型给 100 张旅行照片打分 - 少数派 普通人如何破圈?去参加一个本地协会 - 少数派 把极空间的图标全换了,主题DIY全攻略打造你的专属NAS桌面 - 少数派 电子便签墙,帮你实现便签自由 - 少数派 我如何用三个 CLI 工具取代文档创建需求 - 少数派 原来真的有人可以玩一辈子 - 少数派 社区速递 139 | 派友热议三月买了啥、复古单反尼康 Df 体验 - 少数派 06 作品的赏析与评价 - 少数派 TDS REVIEW|索尼 WF-1000XM6 降噪真无线耳机体验 - 少数派 35.98万起售的第二代腾势D9,我看重的不是堆料,而是不凑合 - 少数派 鼠须管 Squirrel 皮肤配置指北 - 少数派 从watch ultra2换到redmi watch6 - 少数派 派早报:阿里巴巴发布视频生成模型 HappyHorse 1.0 等 - 少数派 别迷信1M - 少数派 家人们天塌了!网盘“大封杀”,多个渠道多条路,NAS部署PanHub - 少数派 AI与人勾心斗角!NAS一键部署AI狼人杀,假日休闲必备。 - 少数派 电商必备!Comfyui工作流批量生图插件,一次生成12张!支持Nano banana pro模型 - 少数派 Comfyui工作流配置Gpt-image-2模型教程,0.03/张 - 少数派 OpenClaw第三方APi怎么配置?可使用Gpt-image-2模型 - 少数派 会员社区话题精选 Ep. 103 - 少数派
01 如何对症下药,分类修复旧视频瑕疵 - 少数派
2025-06-09 · via 少数派

修复工程

读取视频

本次使用的视频素材是《功夫兔与菜包狗3:菜包狗大反击》于2009年发布的旧版,容器格式为 avi,编码器为 XViD,分辨率为 720p(1280x720)。我们首先用上期介绍的步骤,在 Vapoursynthe Editor(下文中称「编辑器」)中导入视频:

import vapoursynth as vs
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)

src.set_output()

按 F6 检查基本信息,得到原始视频是 YUV420P8 格式,即 YUV 格式,色度采样方式为 4:2:0,色深为 8 bit。

由于一般的原始视频色深是 8 bit,只有 256 种可能的取值,为了减小在修复过程中的计算误差,通常在修复开始前先用 mvsfunc.Depth 函数将视频转换为 16 bit,完成后输出时再转为 8 bit 或其他格式。因此我们将 src 由 YUV420P8 转为 YUV420P16 格式:

import vapoursynth as vs
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)

bit.set_output()

去色块

接着按 F5 即可在编辑器中预览原始视频,查看是否有明显的瑕疵。通常在检查瑕疵时可以使用若干倍放大,便于发现瑕疵。

随意选择几帧,即可发现画面中充斥着许多边缘不连续的块状纹理,图中红框内较为明显。这是一种名为「色块」(Blocking)的瑕疵,通常在 MPEG2编码器或前 x264 编码器如 XViD、DViX 等编码器编码的视频中较为明显,特别是在快速运动的场景。

消除这一瑕疵的方法称为「去色块」(Deblocking),通常用 Vapoursynth 的 deblock 插件或者 havsfunc.Deblock_QED 即可解决。(以下图像没有特别说明,均放大至原图像的 2 倍大小,以便读者更好地观察瑕疵与修复的效果。)

相比于 Vapoursynth 的 deblock 插件,havsfunc.Deblock_QED 函数专为这种 8×8 色块设计,经过尝试后,我们使用 havsfunc.Deblock_QED 进行去色块操作。为了方便比较去色块前后的画面,我们可以使用 Vapoursynth 标准库中的 Interleave 函数,它可以将多个视频逐帧交错排列,配合快捷键方向键查看相邻的两帧即可快速对比画面。

import vapoursynth as vs
import havsfunc as haf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)

out = core.std.Interleave([bit, deblock])
out.set_output()

可以看到去块后,色块的边界基本消失不见了,但背景的纹理也略有模糊,这是去块过程难以避免的副作用。也有一些比较弱的色块虽然被部分消除了,但还是可以辨认出大致的块状纹理,画面仍然不够平滑自然。

不过总体来说,去块带来的提升强于副作用,因此我们说这样修复是有效且合理的。这些较弱的色块残留可以在稍后的降噪步骤中被进一步去除。

抗锯齿

我们将去块后的 deblock 设为输出,继续观察是否还存在其他瑕疵。同样是这一帧,观察动画中组成角色的线条,我们可以发现他的轮廓并不连续,而是存在许多明显的「锯齿」(Aliasing)。这是 Flash 动画或者三维 CG 动画中常见的瑕疵,有时也可以因为劣质的缩放算法引起。

值得指出的是,有时锯齿是视频内容的表现手法之一,例如像素风的动画。如果可以确认锯齿是作为一种艺术形式而引入的,那么就不应该处理。

修复锯齿的方法称为「抗锯齿」(Anti-aliasing,aa)——对,它和游戏中用以增强画面的技术是相同的,只是实现方法可能略有不同。 Vapoursynth 函数库中有许多能够实现抗锯齿的函数,不过今天我们用 taa.TAAmbk。把抗锯齿加入脚本中,再将 deblock 和新生成的 aa 用 std.Interleave 交错比较,我们可以看到:

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)

out = core.std.Interleave([deblock, aa])
out.set_output()

可以发现,角色的线条变得平滑而连续了。当然,看起来也没有之前那么锐利,这也是抗锯齿的副作用,不过我们可以通过后续的锐化来解决。

降噪

继续观察画面,我们可以看出组成角色的线条附近有些晕轮状的纹理,明暗不一。这是由于编码器分配的码率不足时,「量化」造成的高频信息损失,在明暗突变处两侧产生的振荡式亮度变化而形成的,称为「振铃」(Ringing)1,属于「噪声」(Noise)的一种,应该加以去除。

诸如质量较低的参考、不良的运动预测与补偿等等因素也会在线条等高频信息附近产生各种各样的噪声,它们相互叠加在一起,产生这种脏兮兮的质感,因此俗称「脏边」。

数学原理决定了色彩或明暗变化平缓(即低频信息)的区域很少有此类噪声,但可能有由于其他原因引入的噪声,例如真人电影中的胶片噪声或数字传感器产生的噪声,后期制作中加入的噪声,数模转换引入的噪声,等等。

这些噪声有些属于特殊风格,例如胶片噪声或后期制作的噪声,此类噪声除非是出于压缩体积的需要2,一般不建议去除;而其余的一般属于瑕疵,应该尽可能去除。

虽然噪声的来源五花八门,但去除噪声的方法都是类似的,这个过程称为「降噪」(Denoise)。降噪的实现方法多种多样,包括傅里叶变换、小波变换、非局部平均(Non-local means,NL-means)以及三维块匹配(Block Matching 3D,BM3D)等等。

本次修复中我们结合非局部平均以及三维块匹配算法来实现降噪。显然,物体边缘的噪声比较明显,因此需要使用比较强力的降噪滤镜,我们先使用 mvsfunc.BM3D,再使用 knlm 滤镜来除去这类噪声。

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)
den_strong = mvf.BM3D(aa, [5, 3, 3])
den = core.knlm.KNLMeansCL(den_strong, 3, 3, 3, 3)

out = core.std.Interleave([aa,den])
out.set_output()

查看输出并与 aa 比较,很明显,这两个降噪滤镜的组合完美地除去了所有脏边。但我们同时注意到,这一降噪处理的力度是如此之大,以至于背景中桌子的花纹也被误伤,变得模糊一片了。

为了缓解这个问题,我们可以单独使用 mvsfunc.BM3D,并降低降噪的强度,来避免画面细节的过度损失:

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)
den_strong = mvf.BM3D(aa, [5, 3, 3])
den = core.knlm.KNLMeansCL(den_strong, 3, 3, 3, 3)
den2 = mvf.BM3D(aa, [3, 1, 1])

out = core.std.Interleave([aa,den2])
out.set_output()

很明显,这次背景的纹理大部分得以保留,但代价是脏边还有残留。我们当然可以继续调节降噪的强度,在细节保留和脏边消除之间尽可能达到一个平衡的效果。

但是,这种折衷的办法不是牺牲画面质量,就是牺牲降噪效果。有没有一种既要又要的方法呢?设想一下,如果我们能把低强度降噪的背景和高强度降噪的线条附近区域结合起来,就可以得到完美的输出。

正如那句经典台词,「小孩子才做选择,我全都要」,这样的方法的确存在,而秘诀就是「遮罩」(mask)的使用。遮罩允许我们以不同的比例混合两个视频,而这正是我们所需的。如何得到包含线条的遮罩成为这步处理的关键,这涉及计算机视觉中的「线条探测」算法。

所幸的是,Canny 算子已经是十分成熟且通用的线条探测算法,Vapoursynth 中包含了 tcanny 滤镜。为了使用这一滤镜,我们需要将 YUV 视频中的 Y(亮度)平面提取出来供 Canny 算子使用,Vapoursynth 的 std 库提供了用于完成这一操作的 ShufflePlanes 函数。

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)
den_strong = mvf.BM3D(aa, [5, 3, 3])
den = core.knlm.KNLMeansCL(den_strong, 3, 3, 3, 3)
den2 = mvf.BM3D(aa, [3, 1, 1])

y = core.std.ShufflePlanes(den2, 0, vs.GRAY)
edge = core.tcanny.TCanny(y, 1.5, t_h=8, t_l=3, scale=0.5)

out = core.std.Interleave([y, edge])
out.set_output()

观察 y 与 edge 对象,可以看到 TCanny 函数成功把强边缘作为遮罩提取出来了。

为了清除掉脏边,我们还需要把这个遮罩层扩大一些。Vapoursynth 的 std 库中提供了 Inflate 和 Maximum 函数来完成这一步骤。这一步的基本思路是用 std.Maximum 快速放大遮罩层以覆盖所有要处理的区域,再用 std.Inflate 柔化遮罩层边缘,从而实现平滑过渡。

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)
den_strong = mvf.BM3D(aa, [5, 3, 3])
den = core.knlm.KNLMeansCL(den_strong, 3, 3, 3, 3)
den2 = mvf.BM3D(aa, [3, 1, 1])

y = core.std.ShufflePlanes(den2, 0, vs.GRAY)
edge = core.tcanny.TCanny(y, 1.5, t_h=8, t_l=3, scale=0.5)
mask = core.std.Inflate(edge)
for i in range(0, 3):
    mask = core.std.Maximum(mask)
for i in range(0, 3):
    mask = core.std.Inflate(mask)

out = core.std.Interleave([edge, mask])
out.set_output()

在确定遮罩已经覆盖了绝大部分脏边后,我们就可以使用 std 库中的 MaskedMerge 函数,将强弱两种降噪力度的视频合并起来。它的原理是以遮罩层的像素值为比例,混合两个不同的视频,因此我们就可以将白色部分替换为强力降噪的视频,完全去除脏边,而将黑色部分替换为弱降噪的视频,保留背景细节。灰色部分则使用两者混合,确保处理后两个视频的交界地带能够平滑过渡,避免产生色块或色带。

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)
den_strong = mvf.BM3D(aa, [5, 3, 3])
den = core.knlm.KNLMeansCL(den_strong, 3, 3, 3, 3)
den2 = mvf.BM3D(aa, [3, 1, 1])

y = core.std.ShufflePlanes(den2, 0, vs.GRAY)
edge = core.tcanny.TCanny(y, 1.5, t_h=8, t_l=3, scale=0.5)
mask = core.std.Inflate(edge)
for i in range(0, 3):
    mask = core.std.Maximum(mask)
for i in range(0, 3):
    mask = core.std.Inflate(mask)

merged = core.std.MaskedMerge(den2, den, mask, [0, 1, 2], True, False)

out = core.std.Interleave([aa, merged])
out.set_output()

查看输出可以发现,脏边去除的同时背景的细节也基本保留了下来,到这里降噪步骤就完成了。

还记得在去块一节中,我们提到残留的弱色块可以被降噪解决吗?现在就让我们再来检查一下结果,看看残留的色块是不是都去除干净了。

容易看出,残留的色块即使在 4 倍放大下也几乎不可见,因此可以说色块已经去除得很干净了。

锐化

降噪也是去除高频信息的步骤,不可避免地会让画面出现一定程度的模糊。而人眼天生喜好锐利、对比度高的画面,因此最后通常使用两种方法对视频进行锐化。第一种是采取遮罩的方式,只对线条附近做锐化或者使用降噪前的视频代替,第二种是全局进行锐化,再用其他的一些方式减少过度锐化带来的瑕疵。

相比于线条锐利的2D 动画,3D 动画和真人视频对锐化的敏感度较低,可以允许更强的锐化而不出现瑕疵。锐化同样有多种方案,但这里我们采用一种简单而方便的方法,使用 Vapoursynth 的 cas 滤镜,它是由 AMD 开发的对比度自适应锐化(Contrast Adaptive Sharpening,CAS)算法移植到 Vapoursynth 而得,只有一个锐度参数,简单粗暴,容易调节。

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)
den_strong = mvf.BM3D(aa, [5, 3, 3])
den = core.knlm.KNLMeansCL(den_strong, 3, 3, 3, 3)
den2 = mvf.BM3D(aa, [3, 1, 1])

y = core.std.ShufflePlanes(den2, 0, vs.GRAY)
edge = core.tcanny.TCanny(y, 1.5, t_h=8, t_l=3, scale=0.5)
mask = core.std.Inflate(edge)
for i in range(0, 3):
    mask = core.std.Maximum(mask)
for i in range(0, 3):
    mask = core.std.Inflate(mask)

merged = core.std.MaskedMerge(den2, den, mask, [0, 1, 2], True, False)
sharp = core.cas.CAS(merged, 0.4)

out = core.std.Interleave([merged, sharp])
out.set_output()

我们可以直观地观察到,由于锐化带来的对比度的增强,画面看起来更「清晰」,桌面纹理也更鲜明了。不过,通常来说,不推荐过强的锐化,不仅是因为无脑锐化引入的严重瑕疵(包括光晕,噪点或瑕疵增强等),而且因为其对画面风格,特别是一些2D 动画风格的破坏。因此,虽然人眼喜欢锐利的画面,也请各位有节制地使用锐化。当然,我们的大前提还是成立的——视频的质量越差,就可以使用越激进的处理方法。

去光晕

同时,我们也注意到,锐化后的画面在某些黑色线条附近出现了白色光晕,白色线条附近则出现黑色光晕:

这是一种被称为「光晕」(halo)的瑕疵,由于增大对比度对于黑色线条和浅色背景是同时生效的,所以会导致黑色线条更黑,而浅色背景更白,从而导致线条周围出现白色光晕。去除光晕的方法称为「去光晕」(dehalo),通常使用 havsfunc 中的 FineDehalo 函数完成。

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)
den_strong = mvf.BM3D(aa, [5, 3, 3])
den = core.knlm.KNLMeansCL(den_strong, 3, 3, 3, 3)
den2 = mvf.BM3D(aa, [3, 1, 1])

y = core.std.ShufflePlanes(den2, 0, vs.GRAY)
edge = core.tcanny.TCanny(y, 1.5, t_h=8, t_l=3, scale=0.5)
mask = core.std.Inflate(edge)
for i in range(0, 3):
    mask = core.std.Maximum(mask)
for i in range(0, 3):
    mask = core.std.Inflate(mask)

merged = core.std.MaskedMerge(den2, den, mask, [0, 1, 2], True, False)
sharp = core.cas.CAS(merged, 0.4)
dehalo = haf.FineDehalo(sharp, rx=2.0, ry=2.0, darkstr=0)

out = core.std.Interleave([sharp, dehalo])
out.set_output()

可以看到,大部分光晕被显著去除了,而线条的锐利程度没有受到太大影响:

进一步检查没有发现其它需要处理的瑕疵,因此这个项目的修复部分就可以到此为止了。当我们确认所有处理工作都完成后,即可把 dehalo 设为输出,按 F6 确认输出视频的参数(分辨率、时长、帧率等)是否符合预期,随后用命令行工具将处理好的视频交由编码器压缩。

编码

编码与修复同等重要,实际上是视频修复工作的一体两面,因为大部分的视频瑕疵都来源于视频压缩,即编码过程。我们当然不想让辛辛苦苦修复的瑕疵在编码之后又全都跑出来,因此保证编码质量是非常重要的。我使用的完整参数如下所示:

vspipe -c y4m "X:\菜包狗大反击.vpy" - | x265-10b - --y4m --preset veryslow --crf 18 --deblock -1:-1 --bframes 12 --b-adapt 2 --aq-mode 3 --aq-strength 0.8 --ref 5 --subme 5 --min-keyint 1 --keyint 400 --ctu 32 --fades --no-rect --no-amp --tu-intra-depth 3 --tu-inter-depth 3 --max-tu-size 16 --max-merge 4 --qg-size 16 --rd 5 --rd-refine --limit-modes --limit-refs 1 --rskip 1 --no-open-gop --rc-lookahead 60 --no-strong-intra-smoothing --no-sao --weightb --cbqpoffs -2 --crqpoffs -3 --me umh --merange 32 --analyze-src-pics --qcomp 0.6 --rdoq-level 1 --psy-rdoq 0 --psy-rd 1 --pbratio 1.2 --fps 24 --colormatrix bt709 -D 10 --log-level 2 --csv-log-level 1 --csv "X:\Kung.Fu.Bunny.03.x265.stats.csv" -o "X:\Kung.Fu.Bunny.3.2009.720p.WEBRip.x265.10bit.mkv"

面对这一大堆命令行参数,想必各位读者是云里雾里。不过,编码器的参数大致上也符合二八定律,在所有参数中只有少数一些对画面和体积有显著的影响,其余参数都属于锦上添花类型的。

因此,虽然我使用了近 50 个参数,但其中作用最大的不过是 crfpresetaq-modeaq-strengthmemerangeqcomppsy-rdoqpsy-rd 几个。本教程的最后一节会详细介绍这些常用参数对视频码率和质量的影响,其余参数如有需要修改,可以到手册中查阅。

对 x265 来说,crf 参数设为 18 实际上已经相当低,对应于很高的质量。这里使用了 10 bit 输出,可以减小由 16 bit 到低色深的舍入误差,也有利于防止色带的出现。其余参数大致符合在一个比较平衡的速度下输出小体积、高质量视频流的目的。

质量检查

视频修复是一项系统性工程,因此最后一步是质量控制,我们需要检查编码后的视频与工作流完成后的视频流是否足够相似,通常随机取若干个点进行比较即可:

import vapoursynth as vs
import havsfunc as haf
import vsTAAmbk as taa
import mvsfunc as mvf
core = vs.core

source = r"X:\菜包狗大反击.avi"
src = core.lsmas.LWLibavSource(source)
bit = mvf.Depth(src, 16)
deblock = haf.Deblock_QED(bit, 24, 0, 1, 2, 1, 2)
aa = taa.TAAmbk(deblock, aatype=1)
den_strong = mvf.BM3D(aa, [5, 3, 3])
den = core.knlm.KNLMeansCL(den_strong, 3, 3, 3, 3)
den2 = mvf.BM3D(aa, [3, 1, 1])

y = core.std.ShufflePlanes(den2, 0, vs.GRAY)
edge = core.tcanny.TCanny(y, 1.5, t_h=8, t_l=3, scale=0.5)
mask = core.std.Inflate(edge)
for i in range(0, 3):
    mask = core.std.Maximum(mask)
for i in range(0, 3):
    mask = core.std.Inflate(mask)

merged = core.std.MaskedMerge(den2, den, mask, [0, 1, 2], True, False)
sharp = core.cas.CAS(merged, 0.4)
dehalo = haf.FineDehalo(sharp, rx=2.0, ry=2.0, darkstr=0)

source2 = r"X:\Kung.Fu.Bunny.3.2009.720p.WEBRip.x265.10bit.mkv"
src2 = core.lsmas.LWLibavSource(source2)
bit2 = mvf.Depth(src2, 16)

out = core.std.Interleave([dehalo, bit2])
out.set_output()

由于视频编码是有损压缩,因此编码后的视频质量必然会劣化。我们的目的不是要让编码前后的视频看起来一模一样,而是在体积和质量之间做一个取舍。在检查画面时,根据编码器的特性,我们应该重点关注两种区域:包含锐利线条的区域以及包含大量复杂纹理的区域。更详细的解释会在本教程的最后一节说明,敬请各位读者期待。

本着这些原则,我们可以看到:

左侧仙人掌的花盆纹理略有模糊,而右侧笔筒的网状纹理发生了变化,但花盆仍然是花盆,网仍然是网,不影响观感。上方画中出现了少量压缩伪影,但即使在 2 倍放大下也不甚明显,因此观众在观看时很难注意到——编码的艺术就是把人眼不易注意到的细节压缩。

锐利的线条附近也出现了少量压缩伪影,不过正常观看时难以察觉。

激烈运动的场景也表现很好,没有过多的瑕疵。读者可以自行比较以上三个例子中未突出显示的部分,相信最后会得出肉眼很难察觉二者区别的结论。这样,我们可以说,编码后的视频通过了质量检测,在体积和质量之间达到了平衡。

如果想要进一步削减压缩伪影,势必要进一步提高质量,使编码前后的视频更加相像,这需要付出更多体积的成本——而且收益是具有边际递减效应的。

意义与目的

行文到最后,一定有读者想问:视频修复的意义是什么?那么,接下来让我们比较一下原始视频和修复后重新编码的视频画面:

可以看到,大部分影响观感的瑕疵都被这样一个相对简单的脚本有效修复了,而其他部分没有受到太多影响。熟练后编写一个这样的脚本可能只需半小时甚至 10 分钟,能够用如此短的时间提升视频的质量,何乐而不为呢?

AI:灵丹妙药?

AI 修复技术近来也是突飞猛进,这可能给人一种错觉,觉得只要把原始视频喂给 AI 就可以当甩手掌柜了。这里我们就取文中一直用作例子的这一帧画面,分别用经典的 waifu2x(UpPhoto 模型)和较新的 Upscayl(Ultramix Balanced 模型)进行 4 倍放大,再用普通的拉伸算法(Spline36)把原图也放大 4 倍后,比较它们的不同3

就角色的轮廓来看,Upscayl 的 Ultramix Balanced 模型表现不错,无论是用修复前还是修复后的画面,都能有效去除线条附近的压缩伪影,获得光滑清晰的线条。

然而,当我们把目光转向之前色块严重的区域,我们可以看出,未修复的画面经 AI 放大后,色块很大程度上保留了下来,现实中的人手当然不存在这样的块状纹理,一般观众如果注意到这个问题就会感到十分诧异。

在这个简单的例子中,AI 难以区分画面的正常内容和瑕疵,如果不做任何处理就用 AI 拉伸,其结果往往是部分瑕疵被当作画面的一部分一起拉伸了,甚至变得更加明显。而 waifu2x 的表现更差,「忠实」地保留了画面中的各种瑕疵,这里就不再赘述。

因此至少就目前来说,AI 还没有先进到可以识别各类视频瑕疵并在超分辨率过程中去除的程度。专门训练的 AI 或许可以胜任这类工作,但或许是因为太过于小众,我尚未听说过这样的 AI 模型。视频修复的一大意义也在于此,就像在数据科学中,真正分析数据之前要对数据进行检查、清洗等预处理,我们的修复工作也可以认为是一种预处理,给后续其他处理打下基础。

总结与回顾

自己修复视频的一大好处是可控性强,丰俭由人,只要了解视频修复的原理,大可以根据自己的喜好对视频进行个性化的修复。

例如,我比较喜欢某个视频,可能会花更多的时间调试参数,或者附加诸如遮罩等操作在处理时保护更多的细节,甚至根据自己的喜好,用一些特别的方法处理瑕疵,或保留特定的瑕疵。而如果我只想简单快速地修复某个视频,也可以从旧脚本里复制粘贴,随便调两下参数就送给编码器压制。

以下是几组对比图,取自我曾经修复过的视频项目,来源五花八门,既包括动画,也包括真人视频。

绝大部分在这些项目中使用过的修复技术都会在后续教程中详细讲解,没讲到的部分要么是对于初学者来说过于艰深,要么是例子太少,我自己也不太熟练。

总而言之,视频修复的基本流程就是读取视频,检查视频中存在的瑕疵,再按一定的顺序修复这些瑕疵,用适当的参数对修复后的视频进行编码,最后检查编码前后的视频,判断编码参数是否合理,防止引入额外瑕疵。

在修复过程中,我们应该牢记奥卡姆剃刀原则,如果不能判断视频是否具有某一特定的瑕疵,就不要进行相应的处理,以免造成画面的劣化。

希望本篇教程能够给各位读者一个对于视频修复工程的大致印象,我随后会分节详解每个步骤,让各位读者能够根据自己的需要,掌握视频修复的不同环节。