按:本文是少数派共创栏目《经验卷轴:视频修复简明教程》第二篇文章的预览版本。在共创模式下,我们鼓励少数派作者向我们提交创作提案和样稿,择优秀提案发布在少数派共创平台上,以便读者能更早获知教程上新动态。同时,我们将邀请读者预览大纲和部分篇目,并就内容规划、写法和风格等问题提供反馈,以此作为决定是否上架及继续完善内容的依据。

首次发布预览版本后,我们收到了很多有益的反馈。有读者提出,希望预览修复具体视频的篇目。因此,本次预览篇目用一部定格动画作为例子,展示具体怎样用 Vapoursynth 修复视频,以及修复效果到底如何。本期涉及的函数、脚本库、瑕疵识别与修复技巧在正式版本中都会展开讲解。

如上次一样,我们继续欢迎读者的反馈。特别地,由于本栏目将包含大量修复前后效果对比,本期包含两种展示样式,希望征集各位意见。其中,「质量检查」一节之前的部分采用静态图片左右对照的样式,该节及以下的部分则用视频轮播。欢迎各位评论指出更偏好的展示方法,我们以此为依据决定上架版本采用的样式。


修复工程

读取视频

本次使用的视频素材是《功夫兔与菜包狗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 模型。视频修复的一大意义也在于此,就像在数据科学中,真正分析数据之前要对数据进行检查、清洗等预处理,我们的修复工作也可以认为是一种预处理,给后续其他处理打下基础。

总结与回顾

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

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

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

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

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

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

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