Skip to content

Latest commit

 

History

History
751 lines (540 loc) · 51.8 KB

高清视频转码进阶指南.md

File metadata and controls

751 lines (540 loc) · 51.8 KB

高清视频转码进阶指南

说明:并非所有内容都是100%正确的,部分内容仍待完善。 源项目地址:https://git.concertos.live/Encode_Guide/Encode_Guide/

1. 工具

1.1 eac3to

尽管我们并不推荐使用闭源软件,但毫无疑问,在没有真正优秀的方案的情况下, eac3to^1^ 是一个很有用的分流处理工具。而如果你不想下载闭源软件(更不要说支持它),你也可以简单地使用带有一个单独的分流器的 MKVToolNix^2^ 。

对于 Windows 用户,只需在 Doom9 (译者注:一个国外压制论坛)即可简单地下载到。Unix 用户必须通过 Wine 来使用 eac3to 。When specifying the path, either use Winepath or swap out the slashes accordingly yourself.

1.2 VapourSynth

VapourSynth 的安装十分简单。无论是 Windows , Mac 上的 Homebrew ,还是各个发行版的软件源,都有 VapourSynth 的安装程序。

VapourSynth 的文档列出了存放你的插件的路径。对于 Windows ,推荐的路径是 <AppData>\VapourSynth\plugins32 或 <AppData>\VapourSynth\plugins64 。Unix用户们可以创建一个配置文件来指定路径。

用于 VapourSynth 的 Python 脚本应当被放置在你的 Python site-packages 文件夹。在 Arch Linux ,这位于 /usr/lib64/Python3.*/sitepackages/ 。Windows 用户们可以在他们的账户 AppData 文件夹中找到这一路径。

1.3 编辑器

目前有4种主流的 VapourSynth 编辑器:

  • VapourSynth Editor^3^
  • Yuuno^4^
  • VapourSynth Preview^5^
  • AvsPmod^6^

每个 Linux 软件源都有 VSEdit 。例如,在 Arch 中,其位于 aur/vapoursyntheditor ;Windows 下有一个对应的安装包; Mac 用户能够通过 Homebrew 来安装它。

Yuuno 是一个 Jupyter notebooks 的扩展,能够允许你编辑和输出 VapourSynth 脚本。你可以通过以下代码来安装:

$ pip install yuuno
$ yuuno jupyter install

注意,你必须已经事先安装过了 Jupyter 或 Jupyter Lab 。

VapourSynth Preview 使用一个单独的文本编辑器或 IDE 来编辑和输出 VapourSynth 脚本,这使它对于那些不喜欢 VSEdit 内置的编辑器的人来说十分有用。

AvsPmod 是一个用于 AviSynth 的编辑器,但是也支持 VapourSynth 。

这些编辑器都拥有各自的一些优点和缺点,但对于新手,我更推荐使用 VSEdit 来进行本地编辑;而 yuuno 则更适用于那些还需要在服务器上编写脚本的用户。这是因为只需要很少量的配置,就可以远程使用 Jupyter 。对于 yuuno 用户,建议尝试使用 Jupyter Lab 来代替 Jupyter 。

对于这两者,更多的区别如下:

VSEdit

  • 不需要保持浏览器开启。
  • 内置了基准测试和转码工具。
  • 通过 CTRL + SHIFT + 方向键,可以轻松地浏览视频。
  • 更稳定。
  • 几乎不需再进行开发。
  • 更多的PT用户在使用它,因此更容易得到帮助和支持。
  • 针对 VapourSynth 语法规则特别定制的高亮显示和输入建议。
  • 允许存储模板。

yuuno

  • 很便于远程使用。
  • 通过 iPython 的魔法命令可以很容易地进行输出。
  • Way better comparison tools via %%vspreview clipa --diff clipb ⇒ hovering over preview changes clip.
  • 不太成熟,因此可能会导致更多的崩溃。
  • 允许你在同一个 Jupyter 文本中编辑和输出多个脚本。

1 https://forum.doom9.org/showthread.php?t=125966

2 https://mkvtoolnix.download/

3 https://bitbucket.org/mystery_keeper/vapoursynth-editor

4 https://yuuno.encode.moe

5 https://github.com/Endilll/vapoursynth-preview/

6 https://avspmod.github.io/


1.4 x264 和 x265

安装 x264 和 x265甚至比安装 VapourSynth 更为简单,同时你也可以使用一些修改版。最主流的 x264 修改版是 tmod 。相比于原版,值得注意的改动包括:淡化补偿(fade-compensate),这对**宏块树(mbtree)**设为开启时的淡化转码十分有用;更多的自适应量化模式(aq mode)等。除非你不使用以上这些选项(目前几乎已经没人这样做了),那么你可以仅使用原版的 x264 。而如果你希望在转码时更加精确地控制,毫无疑问你应该尝试 tmod 并捆绑使用它提供的 aq mode ,这样你就能够榨取出更高的质量,从而在保护噪点的同时不至于破坏线条。

最主流的 x265 修改版则是 yukki ,却并不那么让人眼前一亮。All this includes is some cosmetic changes like a nicer ETA.

1.5 Opus,qAAC,以及 fdkaac

为了转码 Opus ,你需要安装 opus-tools 软件包。

qAAC 则需要 iTunes 运行库。在 Mac 上,这没什么要说的。Windows 用户可以安装 iTunes 或者 QuickTime 。另外一个可行的方案是绿色版程序,在无需安装iTunes的同时囊括了必要的运行库。Arch 可以在 AUR 中找到 qaac-wine 软件包。

1.6 MKVToolNix

请确保安装最新版本的 MKVToolNix 。它将会安装多个工具,其中包括最重要的 mkvtoolnix-gui 和 mkvextract 。

3 视频编码

3.1 你的第一个脚本

一个没有额外的过滤的最基本的标准VapourSynth,看起来像这样:

# 如果你使用yuuno作为你的VSEdit编辑器,那么你需要取消下面两行注释
#%load_ext yuuno # 这个告诉Jupyter载入yuuno
#%%vspreview # 这个允许将与允许你预览输出内容
import vapoursynth as vs
core = vs.core 
src = core.ffms2.Source(r"/path/to/source.mkv") # Windows环境下,要使用r''
out = core.std.Crop(src, top=138, bottom=138) # 切除黑边
out.set_output() # 为vspipe标记输出的变量

3.1.1 调整大小(Resizing)

Firstly, note that there’ll be a separate section later on for descaling。在这里,我将解释调整大小方法以及,不用方法的好处。

如果您想要调整大小,重要的是不要对纵横比进行不必要的修改。如果要缩小比例,首先要弄清楚宽度和高度应该是多少。例如,你想缩小到720p,首先需要裁剪,然后弄清楚你是缩放到720高,还是1280宽。如果是前者,则宽度应为:

width = round(720 * src.height / (2 * src.width)) * 2

如果是后者,代码很相似:

height = round(1280 * src.width / (2 * src.height)) * 2

您还可以使用awsmfunc中的cropresize封装函数来执行这些计算和调整大小。

调整的大小方法有很多,主要的几种如下:

  • PointPoint也被称为最近邻大小调整,是最简单的大小调整方式,因为除了放大每个像素或缩小时获取周围每个像素的平均值,它实际上没有做任何其他事情。它的产生的结果是很糟糕的,但因为在放大的时候,他不会进行任何的模糊处理,所以它适合进行放大后查看每个像素的数值。It is also self-inverse, so you can scale up and then back down with it and get the same result you started with.
  • Bilinear调整大小的速度非常快,但是会导致非常模糊的结果以及明显的锯齿。
  • Bicubic调整大小的速度同样很快。但由于混叠(noticeable aliasing)而导致的结果非常模糊。可以通过修改参数以获得更清晰的结果,但这将导致更多的混叠(noticeable aliasing)。
  • Lanczos调整大小的速度比较慢,可以获得非常清晰的结果。但是它会产生非常明显的振铃声。(However, it creates very notice- able ringing artifacts.
  • Blackmanminlobe调整大小(需要使用fmtconv9进行调整)是经过修改的lanczos调整大小方法,具有较少的(ringing artifacts)。对于YUV444编码的色度提升,绝对值得考虑使用这种大小调整器(稍后会详细介绍)。
  • Spline调整大小的速度很慢,但效果却很好。可以使用多种样条调整器,其中Spline16比Spline36快,但结果稍差;Spline36与Spline64结果十分相似,因此没有理由使用后者。 所以Spline36是用于缩小内容的推荐大小调整方法。
  • Nnedi3
  • FSRCNNX

在附录中提供了这些缩放方法的比较,附录中的图16和图17分别是缩小尺寸和放大尺寸,此外,由于bicubic方法在更改其参数将在输出上产生非常显着的结果,因此在附录图18还包括了对不同参数配置的bicubic放大比例的比较示意。为了满足额外的好奇,我在图19中加入了缩小到原始分辨率的后的对比,以及在附录中图20中展示了同一缩放方法下先缩小然后放大的结果。

虽然这些截图可以帮助你更好地了解不同的大小,但它们只是单个花苗的一小部分。如果你想更好地了解这些大小调整方式的效果,我建议你自己做这些放大,(watching them in motion),然后交错它们(std.Interleave)。

对于进行缩小尺寸时候,不用的调整方法没有放大时差异那么明显。但是,我们不建议以此为借口,在缩小时选择调整大小的方式时偷懒。

TL;DR: Use core.resize.Spline36 for downscaling.

3.2 Filtering

有几件事值得在这里先提一下。首先,大多数蓝光的颜色是YUV420P8,范围有限(most Blu-rays come in YUV420P8 with limited range.)。这里的第一组信息是YUV。这代表了我们视频的平面,Y是亮度U和V是我们的色度平面。

其次关于颜色,本例中是4:2:0,指定了我们不同平面的大小。常见的有一共有三种:4:2:0,表示色度平面是亮度平面的一半大小(例如一个1920×1080的视频,它的色度平面为960×540)。4:2:2,则色度平面在水平轴上是一半大小,在垂直轴上是全部大小,4:4:4,即所有平面大小相同。在回放过程中,视频播放器将色度平面放大到与亮度相同的大小。较小的色度平面不明显,但可以肯定知道,当放大时,就可以看出明显的差异。为了说明这一点,下面是一个来自AnoHana的例子,分别是从720p放大到1080p的4:2:0和4:4:4 的两个版本。

图片

在这种情况下,最差的调整大小(bilinear)被用于色度平面。如果您使用类似mpv的KrigBilateral,这应该看起来更好。

另一个例子,

图片

下面要说明的是P8。这指的是位深度,这个例子中它是8-Bit。大多数视频现在是以8-Bit方式储存,因为8-Bit的AVC(H.264)有最好的硬件兼容性。然而,由于8-Bit导致没有足够的值(0-255)可用,它很容易引入诸如条带(Banding)之类的错误。较高的Bit在这方面则不会有太大的问题,而且由于其更好的准确性,高位深(比如10-Bit)实际上更擅长在较低的文件大小情况下存储视频。然而,10-Bit的AVC几乎没有硬件兼容性,而且需要更长的编码时间,因此使用private trackers 的人通常不喜欢它。大多数内容实际上是10-Bit或12-Bit生成的。由于精度的提高,过滤中最常用的位深度是16-Bit。值得注意的是,超高清蓝光(UHD)的格式是YUV420P10,意思是10-Bit。

最后一部分,则要讲述的内容是限制范围。在全范围8-Bit中,我们的范围是0到255之间的所有值。然而,电视通常无法显示所有这些值,亮度被限制在16 - 235,色度被限制在16 - 240。许多消费者的内容,例如蓝光也在有限的范围内。如果你给电视一个全范围的视频,它会简单地把所有的值都设置为16和或235/240和高于相同的值(例如黑色或白色)。(it will simply make everything 16 and under or 235/240 and over the same value (e.g. black or white). )

3.2.1检查你的源

这可能会占用最多的时间:检查源中的问题。这需要你遍历观看整个源文件并亲自检查是否存在:条带(Band),混叠(aliasing ),脏线(dirty lines ),以及其他可能出现的问题。好消息是,VSEdit允许您简单地通过CTRL + SHIFT +左/右箭头键自定义步宽款浏览源视频。这个步宽可以在预览窗口的右下角定义。我建议在1秒到3秒之间。显然,速度越慢越好,因为你将覆盖更多的帧。

将一个过滤器应用到一个给定的范围,使用如下的脚本:

filtered = my_filter(src)
out = awf.rfs(src, filtered, mappings="[FIRST_FRAME LAST_FRAME]")

3.2.2 抖动(Dithering)

3.2.3 去色带(Debanding)

这是最常见的问题。Deband发生时,码率不足(bitstarving )和低质量的设置导致平滑的渐变突然变成颜色变化,这显然使最后看起来很糟糕。好消息是更高的位深度可以帮助解决这个问题,因为可以使用更多的值来创建渐变。因此,很多Deband是在16位完成的,然后抖动到10位或8位再过滤过程完成。

关于Deband,需要注意的一件重要的事情是,你应该一直使用mask,最好是边缘mask或类似的mask。详见3.2.14 !

对于VapourSynth来说,有两种很棒的工具可以用来固定绑带:f3kdb16和fvsfunc的gradfun3。后者不太常用,但有一个内置的mask。

让我们先看看f3kdb: VapourSynth相关代码默认设置如下:

deband = core.neo_f3kdb.Deband(src = clip, range = 15, y = 64, cb = 64, cr = 64, grainy = 64, grainc = 64, dynamic_grain = False, output_depth = 8)

这些设置对一些人来说可能不言自明,但下面是它们的解释:

3.2.7 Deringing 消除振铃效应

术语“振铃”(ringing)效应指图像中边缘过渡的伪影,最常见的是蚊式噪声(mosquito noise)和边缘增强伪影(edge enhancement artifacts)。振铃现象在低质量视频源上非常普遍。但是,由于记录设备的不足和糟糕的压缩方法,即使是高码率的音乐会也容易出现这种情况。要解决此问题,建议使用HQDeringmod或EdgeCleaner(来自scoll),我建议使用前者。这些方法主要目的是模糊和锐化边缘,然后通过边缘遮罩(edgemasks)合并。它们非常简单,因此你可以自己阅读它们,并且应该对其用途有个不错的了解。由于rgvs.Repair可能非常暴力,因此,如果你使用这些函数并且默认值不能产生令人满意的结果,那么请尝试使用修复值。

![图片alt](图片地址 ''图 10: 左图为原图,右图HQDeringmod(mrad=5, msmooth=10, drrep=0)。这种消除振铃效应的方式非常暴力,我不太推荐。图片来源自One Ok Rock音乐会蓝光,码率37 mbps'')

3.2.8 Dehaloing 去晕

"光晕"(Haloing)听起来很像:边缘周围的粗而亮的线条。这些内容在糟糕的大小调整 后很常见。你也可能会发现糟糕的降尺度(descaling)或者对低质的视频源进行降尺度后会出现明显的光晕。要解决此问题,应该使用havsfunc的DeHalo_alpha或其已被屏蔽(masked?)的同类产品FineDehalo。如果使用前者,则必须编写自己的蒙版(mask),因为未进行蒙版处理的去晕通常会导致不好的结果。关于如何编写简单的去晕蒙版,请查看encode.moe的指南30

由于FineDehalo是DeHalo_alpha的包装器,因此它们共享一些参数:

FineDehalo(src, rx=2.0, ry=None, thmi=80, thma=128, thlimi=50, thlima=100, darkstr=1.0, brightstr=1.0, showmask=0, contra=0.0, excl=True, edgeproc=0.0) # ry defaults to rx
DeHalo_alpha(clp, rx=2.0, ry=2.0, darkstr=1.0, brightstr=1.0, lowsens=50, highsens=50, ss=1.5)

AviSynth上的wiki已经说明的很好到了: http://avisynth.nl/index.php/ DeHalo_alpha#Syntax_and_Parameters and http://avisynth.nl/index.php/FineDehalo# Syntax_and_Parameters

3.2.9 Denoising 降噪

降噪是一个相当棘手的问题。实景编码(live action encoder)永远不会降噪,而动画编码通常需要大量降噪。你要为动漫做这件事的主要原因是它本身并没有噪点,但是压缩会引入噪点,而位深转换则会引起抖动(dither)。前者是不需要的,而后者则是我们想要的。你可能还会在闪回(flashbacks)之类的过程中遇到明显的颗粒感(grains)。去除不必要的噪点将有助于压缩并消除一些轻微的抖动/颗粒感;降噪对于10bit编码很有用,平滑的信号源可以更好地进行编码,效果出众;对于8bit则更好,更多的颗粒可以防止出现条纹问题等。有时,在压缩以外也需要进行降噪工作。比如,假设你要对一部动画电影进行编码,在该动画电影中,具有闪回原有情节的场景。动画电影通常是以1080p制作的,但大多数动画剧集并不是。因此,可能会遇到很多放大的影片,充满了1080p的噪点颗粒。在这种情况下,需要对噪点颗粒进行去粒化(degrain),重新缩放(rescale)和然后再将噪点合并(merge)回去。31(通常,在去粒化的剪辑片段上建立边缘遮罩,缩小源剪辑片段比较合理,但这样处理也并不糟。)

degrained = core.knlm.KNLMeansCL(src, a=1, h=1.5, d=3, s=0, channels="Y", device_type="gpu",   device_id=0)
descaled = fvf.Debilinear(degrained, 1280, 720)
upscaled = nnedi3_rpow2(descaled, rfactor=2).resize.Spline36(1920, 1080).std.Merge(src, [0,1])
diff = core.std.MakeDiff(src,  degrained,  planes=[0]) merged = core.std.MergeDiff(upscaled, diff, planes=[0])

3.2.10 Graining 粒化

由于颗粒和抖动是最难压缩的东西,很多视频源都很少包含颗粒或者颗粒被明显破坏。为了解决这个问题,或者只是为了压缩没有颗粒的区域,手动添加颗粒通常是有好处的。在颗粒被破坏的情况下,通常需要先去除颗粒,然后再重新添加回去。这对于动画压制非常有用,缺少颗粒通常会使重编码难以维持渐变。 当我们人为添加颗粒时,我们可以选择使用静态颗粒。在动画中这不是很明显,并且压缩效果要好得多,因此通常是对动画内容编码的最佳选择。但是,它在实景内容中通常非常显著,因此,静态颗粒不常用于私有种子服务器(private tracker)的编码中。

标准的粒化函数为grain.Add(也有其他函数):

grained = core.grain.Addclipvar = 1constant = False)`

此处var选项表示强度。你可能不想调得太高。如果调得太高,你会发现它会会变得非常明显,以至于你最好尝试匹配颗粒从而使其不明显。

最著名的粒化函数是GrainFactory3。使用此函数可以指定如何将Grain.Add应用到三个不同的亮度等级(亮,中,暗)。它也用resize.Bicubic来缩放亮度,通过修改Sharpen选项的b和c参数对其进行锐化。你必须修改大小,锐度和阈值参数,进行匹配可能很难。但是这会产生很棒的效果,特别是于需要更多自然颗粒的实景内容。 自动化程度更高的选项是adaptive_grain。它的工作原理和GrainFactory3类似,但是是根据整个帧的亮度值和特定区域的亮度值,将可变数量的颗粒应用于帧的各个部分。选项越少越好用,且对动画效果很好。对整个帧画面的平均亮度的依赖性也使其产生非常好的效果。

除了这两个函数之外,还可以将两个称为adptvgrnMod32的组合使用。这会将GrainFactory3的锐度和尺寸规格选项添加到adaptive_grain。由于颗粒仅以一种尺寸添加到一个(通常小于帧画面)图像上,因此该函数运行很快。如果在不同的亮度水平下颗粒大小没有变化(通常是数字生产的颗粒),那么与上述两个函数相比,这可以带来更好的结果。 如果你对这很好奇,请参考图4中Mirai的去色带(debanding)的示例,该示例中使用了adptvgrnMod进行了粒化处理。

3.2.11 Deblocking 解块/去区块

解块通常等效于平滑,通常是在画面顶部使用另一个蒙版。最常用的函数是havsfunc中的Deblock_QED。主要参数是

  • quant1:块边缘解块的强度。 默认值为24。你可能希望大幅调高该值。
  • quant2:块内部解块的强度。 默认值为26。调高该值可能也有帮助。 其他流行的选项是deblock.Deblock,它非常强大,总是有用;dfttest.DFTTest,它虽然较弱,但仍然非常暴力;还有fvf.AutoDeblock,它对于对MPEG-2源进行解块非常有用,可以用于整个视频。另一种流行的方法是简单去色带,因为解块和去色带非常相似。对于AVC蓝光源来说,这也是一个不错的选项。

3.2.12 Detinting 色彩校正(?)

If you’ve got a better source with a tint and a worse source without a tint, and you’d like to remove it, 你可以通过时间块和DrDre的色彩匹配工具(Color Matching Tool)。首先,将两个参考快照添加到工具中,导出LUT(Look Up Table?),保存并通过以下方式添加:

clip = core.resize.Point(src, matrix_in_s="709", format=vs.RGBS) 
detint = core.timecube.Cube(clip, "LUT.cube")
out = core.resize.Point(detint, matrix=1, format=vs.YUV420P16)

![图片alt](图片地址 ''图 11: Source with tint on left, tint removed on right. 这个示例来自D-Z0N3压制的你的名字(Your Name,2016)。该帧也经过了抗锯齿处理。'')

同样,如果你有所谓的伽马错误,或更准确地说,是双范围压缩(将有限范围的压缩完全应用于已经过范围受限处理的剪辑片段),则只需执行以下操作(适用于16bit):

out = core.std.Levels(src, gamma=0.88, min_in=4096, max_in=60160, min_out=4096, max_out=60160, planes=0)`

![图片alt](图片地址 ''图12:左侧为双倍范围压缩,右侧为gamma错误修复。'')

通常会要求使用0.88的值,其他伽玛值也不是不可以。但当黑色的亮度值为218而不是235,就必须要要用0.88。请勿在低位深视频中执行此操作,原因如图23所示。如果色度平面也受到影响,则必须分别处理它们:

out = core.std.Levels(src, gamma=0.88, min_in=4096, max_in=61440, min_out=4096, max_out=61440, planes=[1, 2])

你也可以使用awsmfunc中的fixlvls包装器执行这些操作。

如果你的源具有不正确的颜色矩阵,则可以使用以下方法解决:

out = core.resize.Point(src,matrix_in_s ='470bg',matrix_s ='709')

“470bg”通常称为601。调整大小的原因是矩阵转换发生在YUV到RGB转换之间,这意味着我们需要扩大色度。用点调整大小是强制性的。你需要一些参考源知道是否应该这样做,最好别是网络源。从技术上讲,你可以识别出不好的颜色,然后知道有必要改变一下矩阵。

![图片alt](图片地址 ''图13:燃烧(Burning,2018)中进行矩阵转换的示例。此示例使用TayTO的重编码。最值得注意的区域是她的粉红色胸罩和背景中的红色。'')

3.2.13 Dehardsubbing and Delogoing 去除硬字幕以及去除台标

尽管此问题在动画中尤为常见,但在某些实景视频源中也很常见,以及带有台标的电视台上播放的音乐视频或音乐会,因此需要了解如何去除硬字幕或台标。对于台标,可以考虑Delogo插件。你需要台标的.lgd文件来使用它。你可以通过自己喜欢的搜索引擎简单地查找它,应该能找到一些东西,之后就很简单了。 去除硬字幕的最常见方法是比较两个视频源,一个带有硬字幕,一个参考源不带有硬字幕。我推荐的函数是kagefunc33的hardsubmask和hardsubmask_fades。前者仅对带有黑白字幕的来源有用,而后者可用于台标以及去除硬字幕。两者的重要参数都是expand选项,这意味着要调用std.Maximum。根据你的视频源质量和检测到的数量,可能需要降低这些参数值。

我们还可以使用Delogo执行类似的操作来创建各种蒙版:

此处尚无示例脚本。

准备好蒙版后,你将需要合并没有硬字幕的参考源与主要源。此过程中可能需要使用一些着色处理,因为二者可能有色差。值得注意的是,这样做要远好于将好的视频源(有硬字幕)替换成差的视频源。如果你很懒,通常可以毫无问题地将这些蒙版应用到整个剪辑片段中,无需遍历整个视频来寻找硬字幕区域。

3.2.14 masking 蒙版/遮罩

这是最复杂的部分,除了动画重编码,大多数重编码都倾向于忽略。蒙版有助于保护重要的细节,防止其被过滤器破坏。PT重编码最流行的蒙版是二值化蒙版(binarize masks?):

y = core.std.ShufflePlanes(src, 0, vs.GRAY) 
mask = core.std.Binarize(y, 5000)
merge = core.std.MaskedMerge(filtered, src, mask)

在这种情况下,我假设我们使用的是16bit。std.Binarize在此执行的操作是使我们的位深所允许的每个小于5000的值成为最低,而使大于5000的每个值成为最大值。这意味着将需要复制源上每个在5000以上像素。通常这被称为“ 亮度蒙版(luma mask)”,用于去色带,一般只有深色区域需要这些处理。

我们还可以使用其中一个色度平面来执行此操作:

u = core.std.ShufflePlanes(src, 1, vs.GRAY) 
mask = core.std.Binarize(u, 5000)
mask = core.resize.Bilinear(mask, 1920, 1080) 
mask = core.std.Inflate(mask)
merge = core.std.MaskedMerge(filtered, src, mask)

你可能已经注意到,我已经执行了相同的二值化操作,但同时调整了蒙版的尺寸和使其扩大。调整大小的原因显然是因为色度平面在YUV420中的分辨率较低,但调整尺寸的选择似乎有些特殊。使用双线性调整器(bilinear resizer)会导致模糊,即周围的像素也会受到影响,这通常对消除锯齿很有用。出于相同的原因,我又添加了一个std.Inflate,尽管通常它对于亮度蒙版比色度蒙版更有用。

更有趣和有用的蒙版是边缘蒙版(edgemasks)和去特定色带的蒙版(debanding specific masks)。对于边缘蒙版,VapourSynth用户具有很大的优势,因为kgf.retinex_edgemask非常准确,效果出色。该边缘蒙版获取源图像,使用retinex算法提高暗区的对比度和亮度,然后将输出的TCanny蒙版层叠加在Kirsch蒙版上。两个常见用途是去色带和抗锯齿。

retinex = kgf.retinex_edgemask(src)
antialiasingmask = retinex.std.Binarize(65000).std.Inflate() 
antialiasingmerge = core.std.MaskedMerge(src, antialiasing, antialiasingmask)
debandmask = retinex.std.Binarize(7000).std.Maximum().std.Inflate() 
merge = core.std.MaskedMerge(deband, src, debandmask)

对于去色带,你通常希望从视频源获得尽可能多的信息,以免破坏细节,因此,我们将在地质进行二值化,并使用std.Maximum和std.Inflate扩展蒙版。我们要用此蒙版将视频源中的内容添加到需要去色带的剪辑片段中。人们可能想使用多种不同的方式来操作蒙版,例如将阈值以上的所有内容乘以某个特定值(Expr(retinex, "x 7000 > x 10* x ?"),只进行最大化扩展,而不管它是什么或你拥有什么。

通常,我们通常想将反锯齿应用于明显的边缘,需要在高值进行二值化。调用std.Inflate可以让我们获得应用了抗锯齿的全部结果。此处,我们要通过蒙版将抗锯齿功能添加到视频源中。(?重复了)

其他有用的边缘蒙版包括:

  • std.Prewitt
  • std.Sobel通常比Prewitt更准确,还是建议同时测试是否不能用Kirsch或Retinex型蒙版。
  • tcanny.TCanny 这基本上是在模糊剪辑片段上丢一个Sobel蒙版。
  • kgf.kirsch 作为retinex_edgemask组成部分之一,在明亮的场景中可以产生与其几乎相同的结果。速度比其他方法慢,因为它使用更多的方向(directions?),但效果很好。 这些蒙版的比较可以在附录中的图24和25下找到。

尽管边缘蒙版非常适合去色带,但它们通常还会检测出色带本身的边缘,速度很慢。很好的一个替代选项是GradFun3的蒙版的去色带蒙版。后者速度很快,可以为你带来更好的结果,可能有些夸大其词。对于GradFun3,可以使用Frechdachs的Fag3kdb包装器。我建议尽可能使用去色带蒙版,边缘蒙版通常会在较暗的场景中产生更好的效果,因此请进行一些测试。要从GradFun3中获取蒙版输出,可以执行以下操作:

mask = fvf.GradFun3(src,debug = 1)

![图片alt](图片地址 ''图14:GradFun3(左上),debandmask(右上),retinex_edgemask(左下)和retinex_edgemask.std.Maximum().std.Inflate()(右下)的比较。'')

对使用和不使用蒙版进行强的去色带操作区别感到好奇的人,请参考附录中的图26,以比较简单的边缘蒙版对去色带的影响。使用亮度/色度蒙版时,请不要忘记测试你是否应该在以前的蒙版上使用合适的边缘蒙版或去特定色带蒙版,因为简单的亮度蒙版不会破坏边缘!其示例包含在图27和28下的附录中。 现在,我们已经介绍了常见的蒙版,接下来继续介绍可帮助你操作蒙版或创建自己的蒙版的功能。我会很简短,因为它们的文档很棒:

  • std.Maximum / Minimum(标准/最大值/最小值):使用此值可以增大或缩小蒙版,你可能还希望将 coordinate = [0、1、2、3、4、5、6、7] 应用于任何适合你的数字,以便指定周围像素的权重。
  • std.Inflate / Deflate:类似于以前的功能,但是它不合并最大像素,而是合并它们,从而使边缘略微模糊。在大多数蒙版操作的末尾很有用,可以在蒙版区域之间略微过渡。
  • std.Expr:已知为非常复杂的函数。通过反向波兰语符号应用逻辑(Applies logic via reverse Polish notation)。如果你不了解,请在Wikipedia上阅读相关条目。你可以执行的一些很酷的操作是使某些像素变亮,同时保持其他像素不变(而不是像使用std.Binarize那样使它们变暗): std.Expr(" x 2000 > x 10 * x ?") 这将使每个大于2000的值乘以10,而其他值则不变。一个很好的案例是介于两个值之间: std.Expr(" x 10000 > x 15000 < and x {} = x 0 = ?".format(2 ** src.format.bits_per_sample-1)) 这使介于10000和15000之间的每个值成为位深度允许的最大值,并使其余的归零,就像std.Binarize那样。几乎每个功能都可以通过该函数表达出来。
  • std.MakeDiff和std.MergeDiff:不言自明,可以在去粒化处理的剪辑片段上应用某些内容,然后将其颗粒合并回去,如“降噪”部分中所述。
  • std.Convolution:将矩阵应用于像素。其文档对此进行了很好的说明,如果没有理解,请仔细阅读。许多蒙版是通过卷积内核定义的。你可以用它来做很多事情,例如std.Expr。举个例子,如果要平均像素周围的所有值,请执行std.Convolution([1,1,1,1,0,1,1,1,1])
  • std.Transpose:转置(即翻转)剪辑片段。
  • std.Turn180:旋转180度。
  • std.BlankClip:一帧纯色。你可以用它替换不良的背景,或者在整个影片中添加颗粒感但又不希望片尾字幕充满颗粒感的情况下使用。要获得电视的范围,可以对8bit黑色使用std.BlankClip(src,color = [16,128,128])也可用于制作基于区域的蒙版。
  • std.Invert:不言自明。你也可以只交换通过蒙版合并的剪辑片段,而不这样做。
  • std.Limiter:你可以使用此功能将像素限制为某些值。用于维持电视范围(std.Limiter(min = 16,max = 235))
  • std.Median:这将每个像素替换为其附近的中值。没多大用。
  • std.StackHorizo​​ntal / std.StackVertical:将剪辑片段堆叠在另一个之上/之下。
  • std.Merge:这使你可以合并两个具有给定权重的剪辑片段。权重0将返回第一个剪辑片段,而权重1将返回第二个剪辑片段。你给它的第一项是剪辑片段列表,第二项是每个平面的权重列表。这是将第二个剪辑片段中的色度合并到第一个剪辑片段中的亮度的方法:std.Merge([first,second],[0,1])。如果没有给出第三个值,则将第二个复制到第三个平面。
  • std.MaskedMerge:根据给定的蒙版将第二个剪辑片段合并到第一个剪辑片段。
  • std.ShufflePlanes:从剪辑片段中提取或合并平面。例如,你可以使用std.ShufflePlanes(src,0,vs.GRAY)获得亮度平面。 如果只想在某些区域应用某些内容,则可以使用包装器rekt34或rekt_fast。后者仅将你的功能应用于给定的区域,从而加快了速度,对于抗锯齿和类似的慢滤波器非常有用。关于它的一些包装器已经有了,比如用于抗锯齿的rektaa。 rekt_fast和lambda函数相关,因此你需要输入rekt_fast(src, lambda x: core.neo_f3kdb.Deband(x)),而非core.neo_f3kdb.Deband(src)。

另外一个非常特殊的函数是std.FrameEval。它允许剪辑你执行的操作是评估剪辑片段的每个帧并应用特定于帧。这很令人困惑,但是VapourSynth的文档中有一些很好的示例:http://www.vapoursynth.com/doc/functions/frameeval.html。除非你对编写需要此功能的函数感兴趣,否则剪辑你将永远不会使用它。但是,许多函数都会用它,包括kgf.adaptive_grain,awf.FrameInfo,fvf.AutoDeblock,TAAmbk等。我可以想到的一个示例展示了如何根据帧类型应用不同的解带器:

import functools
def FrameTypeDeband(n, clip):
if   clip.get_frame(n).props._PictType.decode()   ==   "B":
return core.neo_f3kdb.Deband(clip, y=64, cr=0, cb=0, grainy=64, grainc=0, keep_tv_range=True, dynamic_grain=False)
elif clip.get_frame(n).props._PictType.decode() == "P":
return core.neo_f3kdb.Deband(clip, y=48, cr=0, cb=0, grainy=64, grainc=0, keep_tv_range=True, dynamic_grain=False)
else:
return core.neo_f3kdb.Deband(clip, y=32, cr=0, cb=0, grainy=64, grainc=0, keep_tv_range=True, dynamic_grain=False)

out = core.std.FrameEval(src, functools.partial(FrameTypeDeband, clip=src))

如果想了解更多信息,建议阅读Irrational Encoding Wizardry GitHub组的指南:https://guide.encode.moe/encoding/masking-limiting-etc.html,并阅读大多数你喜欢用的Python和VapourSynth的函数说明。几乎所有的优秀压制都应该使用一些蒙版,或者针对特定的情况开发自己的蒙版。

3.2.15 Filter order

滤镜使用的顺序错误可能导致破坏性或糟糕的结果。建议使用以下顺序:

st=>start:Load source
o1=>operation:Crop
o2=>operation:High bit depth
o3=>operation:IVTC and MPEG-2 deblock
o4=>operation:Fix dirty lines
o5=>operation:Detint
o6=>operation:Resize or descale
o7=>operation:Denoise
o8=>operation:Dehalo and dering
o9=>operation: Anti-aliasing
o10=>operation:Deband and deblock
o11=>operation:Graining
e=>end:Dither to output depth
st->o1->o2->o3->o4->o5->o6->o7->o8->o9->o10->o11->end

图15:应用每个滤镜程序的推荐顺序。在某些情况下,可能必须先进行降噪处理。

3.2.16 Example Scripts 示例脚本

Mirai (2018):

import vapoursynth as vs
core = vs.get_core()
import fvsfunc as fvf
import mvsfunc as mvf
import kagefunc as kgf
import vsTAAmbk as taa
import havsfunc as haf
from debandmask import *
from rekt import *
from adptvgrnMod
import * src = core.ffms2.Source("")
# Going up to 16-bit, as I like to  work  in this depth.  b16 = mvf.Depth(src, 16).std.Crop(top=20, bottom=22)

# Filling the first row and filling the chroma of  the  first two  rows.  fb1 = core.fb.FillBorders(b16, top=1)
fb2 = core.fb.FillBorders(b16, top=2)
b16 = core.std.Merge(fb1, fb2, [0,1])

#   Super   light   denoising.   No   point   in   BM3D   for   denoising   this   weak. 
b16 = core.knlm.KNLMeansCL(b16, a=3, h=0.1, d=2, device_type='gpu', device_id=0, channels='Y')
b16 = core.knlm.KNLMeansCL(b16, a=2, h=0.2, d=1, device_type='gpu', device_id=0, channels='UV')

# Soft dehalo.
b16 = haf.FineDehalo(b16, rx=2.0, thmi=80, thma=128, thlimi=25, thlima=100, darkstr=0.5, brightstr=0.5)

# Dirty lines during  credits.  Cleaning  edges,  then  halos. cf = core.std.Crop(b16, left=94, top=292, right=1018,
bottom=290).fb.FillBorders(top=1, left=1, bottom=1).edgefixer.ContinuityFixer(left=[2, 2, 2], top=[2, 2, 2],right=2, bottom= [0, 2, 2], radius=15)
fb = rekt_fast(b16, lambda x: core.fb.FillBorders(x, left=2, top=2, right=1, bottom=2).std.Merge(cf, [1,0]), left=94, top=292, right=1018, bottom=290)
dh = rekt_fast(fb, lambda x: haf.FineDehalo(x, rx=2.0, thmi=80, thma=128, thlimi=25, thlima=100, darkstr=0.5, brightstr=2.3), left=94, top=292, right=1018, bottom=290)
sf = fvf.rfs(b16, dh, "[1434 2296]")

cf = core.std.Crop(b16, left=94, top=302, right=1018, bottom=300).fb.FillBorders(left=1).edgefixer.ContinuityFixer(left=[2, 2, 2], top=1, right=1, bottom= [1, 2, 2], radius=5)
fb = rekt_fast(b16, lambda x:  core.fb.FillBorders(x,  left=2,  top=1, right=1, bottom=2).std.Merge(cf, [1,0]), left=94, top=302, right=1018, bottom=300)
dh = rekt_fast(fb, lambda x: haf.FineDehalo(x, rx=2.0, thmi=80, thma=128, thlimi=25, thlima=100, darkstr=0.5, brightstr=1.5), left=94, top=302, right=1018, bottom=300)
sf = fvf.rfs(sf, dh, "[133711 135117] [135360 136057] [136143 137216] [137282 138288] [138377 138757] [138820 140782]")

cf = core.std.Crop(b16, left=94, top=302, right=1018, bottom=300).fb.FillBorders(left=1).edgefixer.ContinuityFixer(left=[2, 2, 2], top=1, right=1, bottom= [1, 2, 2], radius=5)
fb = rekt_fast(b16, lambda x:  core.fb.FillBorders(x,  left=2,  top=1, right=1, bottom=2).std.Merge(cf, [1,0]), left=94, top=302, right=1018, bottom=300)
dh = rekt_fast(fb, lambda x: haf.FineDehalo(x, rx=2.0, thmi=80, thma=128, thlimi=25, thlima=100, darkstr=0.5, brightstr=1.5).neo_f3kdb.Deband(y=48, cb=0, cr=0, range=5, grainy=64, grainc=32, output_depth=16, keep_tv_range=True), left=94, top=302, right=1018, bottom=300)
sf = fvf.rfs(sf, dh, "[135118 135296] [138305 138376]")

mask = core.std.ShufflePlanes(b16, 0, vs.GRAY).std.Trim(2400, 2401) * src.num_frames
mask = rekt(mask, core.std.BlankClip(b16, 1920, 1038, format=vs.GRAY16), left=666, top=292, right=1114, bottom=744)
dh_lim = core.std.MaskedMerge(dh, b16, mask) sf = fvf.rfs(sf, dh_lim, "[2297 2329]")

# 4:3 cropped scene. Replacing borders with my own black borders in order  to keep them  from having a different shade of black.
crop = core.std.Crop(b16, left=254, right=254)
fb = core.fb.FillBorders(crop, left=1, right=1).std.Merge(crop, [1,0]).edgefixer.ContinuityFixer(left=1, right=1, top=0, bottom=0, radius=50).std.AddBorders(left=254, right=254, color=[4096, 32768, 32768])
sf = fvf.rfs(sf, fb, "[33448 34196]")

# Placebo edgemask binarized so we only get the obvious edges, then inflated.
mask = kgf.retinex_edgemask(b16).std.Binarize(65500).std.Maximum().std.Inflate()

# Strong aliasing.
aa = taa.TAAmbk(b16, aatype=2, mtype=0, opencl=False) aa = core.std.MaskedMerge(b16, aa, mask)
sf = fvf.ReplaceFramesSimple(sf, aa, mappings="[4225 4727] [18340 18387] [129780 131148]")

# Mild aliasing.
aa = taa.TAAmbk(b16, aatype=3, mtype=0, opencl=False) aa = core.std.MaskedMerge(b16, aa, mask)
sf = fvf.ReplaceFramesSimple(sf, aa, mappings="[55394 55451] [55649 55782] [120840 120901]")

# Very strong aliasing.
aa = taa.TAAmbk(b16, aatype=6, mtype=0, repair=16) aa = core.std.MaskedMerge(b16, aa, mask)
sf = fvf.ReplaceFramesSimple(sf, aa, mappings="[107405 107462]")

#   Strong   aliasing   that   I   tried   to   fix   with   a   terrible   mask. 
mask = kgf.retinex_edgemask(b16).std.Binarize(65500).std.Maximum().std.Minimum(coordinates=[1,0,1,0,0,1,0,1]).std.Deflate().std.Deflate()
aa = taa.TAAmbk(b16, aatype=6, mtype=0, opencl=False) aa = core.std.MaskedMerge(b16, aa, mask)
sf = fvf.ReplaceFramesSimple(sf, aa, mappings="[55510 55580]")

#   I   simply   marked   this,   it   would   require   a   lot   of   work,   so   I   just   decided against doing this.
#sf = fvf.rfs(sf, ?, "[65880 66478]")
#sf = fvf.rfs(sf, ?, "[120902 121051] [121790 121905] [122388 122528] [123038 123153] [126686 126812] [128740 128953]") #Banding? [121063 121095] [121906 121968] [122530 122576]

# Graining an area with no grain.
gr = adptvgrnMod(b16, strength=2.5, size=1.25, sharp=35, static=False, luma_scaling=3, grain_chroma=False)
sf = fvf.rfs(sf, gr, "[120840 120901]")

#   Debanding   with   the   standard   debandmask.   All   of   these   debanding   areas   had almost no grain, so I added some on top.
dbmask = debandmask(b16, lo=6144, hi=12288, lothr=320, hithr=384, mrad=2) deband = core.neo_f3kdb.Deband(b16, y=34, cb=0, cr=0, range=10, grainy=16, grainc=8, output_depth=16, keep_tv_range=True)
merge = core.std.MaskedMerge(deband, b16, dbmask)
merge = adptvgrnMod(merge, strength=2, size=1.5, sharp=25, static=False, luma_scaling=5, grain_chroma=True)
sf = fvf.rfs(sf, merge, "[3174 3254] [3540 3655] [7463 7749] [41056 41597] [63482 64106] [91033 91164]")

# Debanding with retinex. mask =
kgf.retinex_edgemask(b16).std.Maximum().std.Inflate().std.Maximum().std.Inflate()  
deband = core.neo_f3kdb.Deband(b16, y=48, cb=48, cr=48, range=15, grainy=16,
grainc=16, output_depth=16, keep_tv_range=True)
merge = core.std.MaskedMerge(deband, b16, mask)
merge = adptvgrnMod(merge, strength=2.2, size=1.25, sharp=15, static=False, luma_scaling=5, grain_chroma=True)
sf = fvf.rfs(sf, merge, "[77952 78034] [93358 93443]")

#   Debanding   with   gradfun3   mask.
deband = Fag3kdb(b16, thry=54, thrc=54, radiusy=10, radiusc=6, grainy=32, grainc=16)
sf = fvf.rfs(sf, deband, "[25 263]")

# Dithering back to 8-bit.
final = mvf.Depth(sf, 8, dither=7)

# Replacing black areas with a simple black screen in order to keep slight variations from happening. Usually not necessary, though.
blank = core.std.BlankClip(src.std.Crop(top=20, bottom=22), 1920, 1038, color=[16, 128, 128])
final = fvf.rfs(final, blank, "[0 24] [1352 1433] [58945 59016] [75563 75633] [78351 78421] [81130 81141] [81261 81272] [93967 94062] [99889 99959] [118093 118147] [140928 140951]")

final.set_output()

Sword Art Online: The Movie - Ordinal Scale (2017):

Sword.Art.Online.The.Movie.Ordinal.Scale.2017.ITA.1080p.BluRay.AC3.x264.D-Z0N3


import vapoursynth as vs
core = vs.get_core()
import fvsfunc as fvf
import kagefunc as kgf
import havsfunc as hvf
import vsTAAmbk as taa
import fag3kdb
import nnedi3_rpow2 as nnrp
src = core.ffms2.Source("")
resize = src # I called this and was too lazy to change it.

# Rescaling a flashback with grain.
dn = core.knlm.KNLMeansCL(src,   d=3,   a=1,   s=0,   h=1.5,   device_type="gpu", device_id=1, channels="Y")
diff = core.std.MakeDiff(src, dn,  planes=[0])  ds = fvf.Debicubic(dn, 1280, 720)
us = nnrp.nnedi3_rpow2(ds, 2, 1920, 1080, kernel="Spline36") merged = core.std.MergeDiff(us, diff, planes=[0])
src = fvf.ReplaceFramesSimple(resize, merged, mappings="[3418 3507] [3508 5145] [75916 76205] [76253 76323] [77720 77790]")

# Rescaling a flashback without grain.
ds = fvf.DescaleAA(dn,  1280,  720).std.MergeDiff(diff,  planes=[0])  
src = fvf.ReplaceFramesSimple(src, ds, mappings="[3298 3417]")

# Going to 16-bit. The above parts are in 8-bit because I was scared of performance issues.
src = fvf.Depth(src, 16)

# I like to establish a separate variable for 16-bit and leave src for 8-bit, but didn't do that here. This is so I could copy-paste commands. 
b16 = src

# Anti-aliasing. As  you  might  be  able  to  tell,  the  crop  and  stacking  could  now be replaced by rekt_fast or simply rektaa.
aa = core.std.Crop(b16, left=400, right=1006)
aa = taa.TAAmbk(aa, aatype=-3, preaa=-1, strength=0, mtype=2, opencl=True) 
left = core.std.Crop(b16, right=1920 - 400)
right = core.std.Crop(b16, left=1920 - 1006)
aa = core.std.StackHorizontal([left, aa, right]).std.Crop(top=208, bottom=456)
top = core.std.Crop(b16, bottom=1080 - 208) 
bottom = core.std.Crop(b16, top=1080 - 456)
aa = core.std.StackVertical([top, aa, bottom])
sfaa = fvf.ReplaceFramesSimple(b16, aa, mappings="[42583 42813] [58812 59050] [65211 65281] [92132 92274]")

#   Debanding   with   a   standard   ass   mask.
db = b16.neo_f3kdb.Deband( range=15, y=60, cb=60, cr=60, grainy=22, grainc=22, output_depth=16)
mask = kgf.retinex_edgemask(b16).std.Inflate() 
merged = core.std.MaskedMerge(db, b16, mask)
sfdb = fvf.ReplaceFramesSimple(sfaa, merged, mappings="[3508 3603] [17600 17706] [41865 42113] [76922 77488] [78444 78598] [81054 81280] [150853 150933] [152057 152288] [152324 152424] [152443 152508] [152521 152686] [171669 172433] [172561 172643] [170283 170557]")

# Debanding values that were outside of the range of 10000-25000.
db = b16.neo_f3kdb.Deband(range=10, y=160, cb=0, cr=0, grainy=28, grainc=0, output_depth=16)
mask = core.std.ShufflePlanes(b16, 0, vs.GRAY).std.Expr("x 10000 < x 25000 > or x 10 * x 10 / ?")
merged = core.std.MaskedMerge(db, b16, mask)
sfdb = fvf.ReplaceFramesSimple(sfdb, merged, mappings=" [96133 96273]")

#   Fixing   dirty   lines   during   credits.   Again,   rekt_fast   would've   been   useful back then.
bot = core.std.Crop(sfdb, top=1080 - 330)
middle = core.std.Crop(sfdb, top=318, bottom=330).edgefixer.ContinuityFixer(top=1, bottom=1, left=0, right=0, radius=5)
fb = core.fb.FillBorders(middle, top=2, bottom=2)
middle = core.std.Merge(fb, middle, [1, 0])
top = core.std.Crop(sfdb, bottom=1080 - 318)
merge = core.std.StackVertical([top, middle, bot])
right = core.std.Crop(merge, left=1920 -  134)
middle = core.std.Crop(merge, left=1018,right=134).edgefixer.ContinuityFixer(left=2, right=2, top=0, bottom=0, radius=5)
fb = core.fb.FillBorders(middle, left=2, right=2)
middle = core.std.Merge(fb, middle, [1, 0])
left = core.std.Crop(merge, right=1920 - 1018)
merge = core.std.StackHorizontal([left, middle, right])
sfc = fvf.ReplaceFramesSimple(sfdb, merge, mappings="[165067 167168] [167403 169466] [169842 170557] [170558 171041]")

# Dithering the result back to 8-bit. final = fvf.Depth(sfc, 8)

final.set_output()

BTS - Blood, Sweat and Tears BTS - Blood, Sweat & Tears 2016 1080p ProRes FLAC 2.0 AVC x264 10-bit - A.R.M.Y bandmask35的快速说明: 边缘蒙版通过发现梯度很大的区域起作用,所以最简单的卷积是: $$ \left[ \begin{matrix} 0 & 0 & 0 \ 1 & 0 & -1 ,\ 0 & 0 & 0 \end{matrix} \right] $$ 这表示我们在检查左侧和右侧像素之间的差异。通过检查与原始像素的差异,我们可以通过移动相减识别没有颗粒的区域: $$ \left[ \begin{matrix} 0 & 0 & 0 \ -1 & -1 & 0 ,\ 0 & 0 & 0 \end{matrix} \right] $$ 不过,这并不完全是这里发生的事情;我们会在多次卷积迭代之后执行减法运算,因此可以在更多像素上获得渐变。然后,我们将其二值化,最小化以去除噪声,然后再次最大化。

import vapoursynth as vs
core =  vs.get_core()
import fvsfunc as fvf
import kagefunc as kgf
import havsfunc as hvf
from adptvgrnMod
import * from rekt import *
from bandmask import *

# Load, go to high bit depth, and crop.
src = core.ffms2.Source("Blood, Sweat & Tears_BTS -187617728.mov")
hbd = fvf.Depth(src, 16)
crp = hbd.std.Crop(top=134, bottom=134)

#  One  scene  has  four lines missing at the bottom  and  dirty lines elsewhere. 
# Every plane was fixed individually.
acr = crp.std.Crop(bottom=4)
ycr = acr.std.ShufflePlanes(0, vs.GRAY)
ucr = acr.std.ShufflePlanes(1, vs.GRAY)
vcr = acr.std.ShufflePlanes(2, vs.GRAY)

ufx = rektlvls(ucr, [ucr.height - 2], [-6.5], prot_val=0).fb.FillBorders(top=1, mode="fillmargins").cf.ContinuityFixer(bottom=1, radius=3)
vfx = vcr.cf.ContinuityFixer(bottom=3, radius=5).fb.FillBorders(bottom=2, top=1, mode="fillmargins")
lvl = rektlvls(ycr, [ycr.height - 2, ycr.height - 3], [-30, -6], prot_val=10)
fmg = lvl.cf.ContinuityFixer(bottom=2, radius=3).fb.FillBorders(bottom=1, top=1, mode="fillmargins")

# Merge fixes and change subsampling to 4:2:0, then add borders.
acr = core.std.ShufflePlanes([fmg, ufx, vfx], [0, 0, 0], vs.YUV)
rsz = acr.resize.Spline36(format=vs.YUV420P16)
adb = rsz.std.AddBorders(bottom=4)

#   Rest   of   the   video   only   has   one   line   missing   top   and   bottom.   Fixed   and subsampling changed.
fmg  =  crp.fb.FillBorders(top=1,  bottom=1,  mode="fillmargins")  
rsz = fmg.resize.Spline36(format=vs.YUV420P16)

# Spliced the fix in with the rest.
trm = rsz.std.Trim(0, 5232) + adb.std.Trim(5233, 5260) + rsz.std.Trim(5261)

# Luma debanding during a couple scenes with graining.
dbn = trm.neo_f3kdb.Deband(y=64, cr=0, cb=0, range=6, grainy=0, grainc=0, output_depth=16)
msk =kgf.retinex_edgemask(trm).std.Binarize(11000).std.Maximum().std.Inflate()
mrg = dbn.std.MaskedMerge(trm, msk)
grn = adptvgrnMod(mrg, size=1.4, sharp=90, luma_scaling=4, grainer=lambda x: core.grain.Add(x, var=1.6, uvar=1.0, constant=False))
snf = fvf.rfs(trm, grn, "[1612 1616] [7779 7794]")

#   Some   blocking   was   fixed   with   bandmask,   since   no   edges   were   present.
bmk = bandmask(trm, 200).std.Crop(top=500).std.AddBorders(top=500)
grn = adptvgrnMod(dbn, size=1.3, sharp=90, luma_scaling=4, grainer=lambda x: core.grain.Add(x, var=.4, uvar=.3, constant=False))
mrg = trm.std.MaskedMerge(grn, bmk)
snf = fvf.rfs(snf, mrg, "[7964 7980] [8009 8018]")

# One  scene has missing grain, so used bandmask  and adptvgrnMod  to fix this.  
# I  got  frustrated  and  started  copy  pasting  random  Maximize/Inflate  calls.  
bmk = bandmask(trm, thr=300)
lmk = trm.std.ShufflePlanes(0, vs.GRAY).std.Binarize(55000).std.Maximum().std.Maximum().std.Maximum().std.Maximum
gmk = core.std.Expr([bmk, lmk], "x y -").std.Crop(bottom=225, top=100).std.AddBorders(bottom=225, top=100).std.Maximum().std.Inflate().std.Maximum().std.Inflate().std.Maximum().std
gmk = kgf.iterate(gmk, core.std.Maximum, 3)
grn = adptvgrnMod(trm, size=1.2, sharp=80, luma_scaling=1, grainer=lambda x: core.grain.Add(x, var=1.4, uvar=1.0, constant=False))
mrg = core.std.MaskedMerge(trm, grn, gmk) snf = fvf.rfs(snf, mrg, "[7579 7778]")

#  Dehalo  on  one  scene. fdh = hvf.FineDehalo(trm)
grn = adptvgrnMod(fdh, size=1.2, sharp=80, luma_scaling=8, grainer=lambda x: core.grain.Add(x, var=.8, uvar=.5, constant=False))
snf = fvf.rfs(snf, grn, "[6523 6591]")

# Dither to output depth. out = fvf.Depth(snf, 10)

out.set_output()

Other fun scripts to read through:

3.2.17 Forum and Blog Posts 论坛和博客

Public: