组内新人培训(五):自适应处理、贝塞尔曲线、以及动态侦测!

F@NAZOrip
F@NAZOrip 2017年11月03日
  • 在其它设备中阅读本文章

    自适应处理,嗯,听起来似乎很高端的话题。我们经常能在各种压制组的制作札记里看到类似的字眼,每每读到此处,路人皆以为牛逼。按照无知观众的想法,自适应就代表着电脑会自动地识别片源逐帧不同的情况,并以此为根据施加不同的处理。所以一个优化得当的自适应处理方案,应该代表着我们所能达到质量的最高标准。或者最起码,但凡经过了自适应处理,画面应该会达到某种程度上的固定质量,无知萌妹A说:"嗯,就像设定了一个CRF一样!"。

    那么事实究竟是怎么样的呢?
    自适应:我不是,我没有...
    今天让我们来深入自适应的内心世界。

一、自适应的原理与视频的固有属性

    自适应处理的概念是比较宽泛的,实现的方式也多种多样,导致它有时候可能会与我们的想象有些出入。有时我们希望自适应方案就应该像一个老练的ripper一样“帮我们完成一切”——我不是在讽刺它,事实上以我们现有的技术也确实可以实现,比如以训练逐层激活的卷积神经网络的方式识别瑕疵、再配以另一个模块修复。不过显然地,这种方案虽然听起来很屌炸,实现起来成本也同样高昂。由于缺乏用爱发电的工程师,所以更多时候我们则是很传统地设计一个数学算法,利用视频的某种固有属性对信息进行分离,以控制不同区域处理强度的方式让rip的处理变得更加科学。

    自适应离我们并不遥远,比如我们在遥远遥远的过去就已经知道,利用UnsharpMask对画面进行锐化并以LimitDiff限制其结果,可以使高频信息变化幅度弱于低频,以达到避开线条锐化纹理的效果。并且由于它不是简单地一分为二:比如规定将线条部分锐化1,纹理部分锐化2。而是根据频率使不同区域在1-2之间过度,实现平滑的梯度调整。而这实际上就是一种利用频率对信息分层的,相对廉价的自适应实现方式。

    理论上,看到这篇文章的同学们应该是在完成了全部的VCB公开课程之后,跟随我们做了前几期组员培训的专题一路听到这里。那么理论上,大家应该已经建立起一些自己对于压制的理解,我们也更多地变成了交流(而不是教学)。我们认为,我们所说的学习视频处理技术,其中主要的部分就是学习对信息的分离技术。无论是我们在视频教学第二章接触到分离器+限制器的组合时产生的层思想(然而本文发表的时点视频教学2并未完成),或是我们利用自建的调色板处理画面,实际上都是在对信息进行分离。而我们今天将其做系统性的整理后,他们就变成了我们说的自适应处理技术。

    关于信息分离,实际上对于一个画面(影片片段),我们可利用的分离依据并不多,图像常用三大属性:亮度、频率、与结构(并不常用),加上图像连成视频后产生的动态,共同构成了我们可以使用的理论基础:

  1. 亮度(包括色度)即我们说的调色板工具,以及基于调色板衍生出的广泛应用:分离器、限制器等等。
  2. 频率则可以被直接用于计算(比如计算方差),或是以将其转化为亮度信息的方式被我们利用,即我们常用的各种各样的Mask工具(TCanny、Sobel)。
  3. 动态信息可能目前为止大家接触到的不多,不过VS中还是给我们留下了MVtools等接口,下文会讲稍安勿躁。

    我们提取到这些信息后,将其作为我们施加处理的一个权重参数。比如说常见的实现方式是,我们准备一个处理层,以这个权重调整其与源的混合比例,实际上就达到了对于不同画面(同一画面内不同信息)的自适应调整。

    在具体的操作中,让我们举一些简单的例子。比如当我们希望对视频进行去色带处理时,经过这么长时间的学习我们都知道有很多小把戏可玩,比如创建一个遮罩框选线条以实现只针对大平面的处理,或者即便对于平面上的信息,我们也可以使用nr-deband对杀伤的细节进行修复,这些都是大家已经会做的,我们知道这些是基于频率信息的处理方案。但在今天的课程后,我们意识到还有其他信息可供参考,比如亮度。根据人眼对暗场颜色梯度变化更加敏感的原理,通过计算当前画面有多暗来控制去色带力度,亮度越低平滑力度越大,让人眼获得比较好的观感。比如提取当前场景的平均亮度,并以此为权重参数调整降噪层的贴回强度(当然你也可以用来直接调整f3kdb的平滑力度,一切随你的脑洞),这种时候我们就称这个处理具有亮度自适应性。


( ↑ 利用std.PlaneStats提取当前帧的帧数&亮度信息(GRAYS)并输出在左上角)

    上述逻辑使用亮度属性构建自适应处理,实际上表述的是DBmbk的方案。
    再举个例子,利用频率信息构建自适应处理要更好理解一些:

    比如我组在你名Project V2发布时,在附注中开玩笑说我们使用了神秘黑魔法。实际上由于你名源的特殊性我们发现V1版本中瑕疵修复的副作用比较明显(比如AA降低了线条的锐度),导致我们希望制作一个不同于V1定位的V2版,并不完全以更好的瑕疵修复和更多的细节保留为目标,而是单纯追求制作一个能在大屏幕上看的更‘爽’的版本,更讨好观众们的眼睛。我们知道这种爽感主要是通过提高锐度的方式实现,但显然我们不能利用锐化这种LowB手段。在实际处理过程中,一方面我们惊喜地发现,由于这部动画电影的原生高分辨率作画,mawen制作的4K版本你名在downscale到1080P后有大量的高清细节清晰度要高于FHD原盘,不过另一方面,在进一步检查中我们同时发现不知是由于录制过程中产生的副作用还是什么其他原因,4Kdownscale版本也并不是全面性地领先,在部分纹理区域的质量又偏低。综合考虑瑕疵、锐度与细节保留,结论是4K和1K各有所长,所以为了达到最佳效果,在这个项目的处理中实际上我们是创建了一个宏观锐度mask来分别侦测两个经过预处理的同级别1080P画面,择取其中高锐度部分融合,并以此作为后续处理的输入源,这也是我们说我们压出来的效果更好是由于在一开始就获得了不同输入信息的原因。这部分内容比较简单,本篇文章中不再赘述。

二、拒绝直线,拥抱曲线

    线性,即一次性。vs操作中,我们经常需要进行线性处理。

    比如使用expr进行YC变换,将色彩范围从0-255压缩到16-235,这是一种线性处理。比如利用Bilinear算法对画面进行缩放是取各像素的加权均值,这也是一种线性处理,比如maskedmerge实际上是根据亮度取两平面的线性平均数、这些都是基于线性算法的处理。但很显然,我们不能依赖线性算法解决所有问题,很多时候我们希望能摆脱lowB的直线,使用更加优雅的曲线来描述一种关系。

    比如在处理最近BD正在绝赞发售中首卷1500的末日三连第一卷的时候,该作涉及到天空相关画面时人工加噪十分不稳定(推测由于不同后期师处理风格不统一造成),在处理的过程中就给我们带来比较大的麻烦。
    我们的高压缩定位让我们区别于其他压制组,无论如何也不能保留噪点层(这是由编码器量化阶段的参考-差异存储机制和熵编码阶段的机制(希望元素数量越少越好、分布越集中越好)共同造成的,如果以后有机会的话也许会写文章细讲)。而如果源的不同场景之间噪点强度差别过大的话,在我们目前使用的以频率分离噪点的方案下,一些高噪场景中的噪点(因为比噪点平均强度要强)就可能会被识别为纹理,这会导致处理后得到一个一部分噪点被抹去、另一部分噪点被保留的肮脏画面,这是我们不能接受的。
    比如在故事一开头(第一话正片),导演为了向观众们介绍故事发生的所在世界的背景设定,采用了一个从从城镇中高耸钟楼的塔尖一直向下拍到平民生活的小巷的长镜头。这个镜头的一开始包含天空的部分噪点比较重,如果使用普通的纹理增强方案会让天空变得比较肮脏。但同时,如果改用对整个镜头采用涂抹较强的方案,则又会把街景中的许多诸如墙壁上纹理之类的细节抹掉。所以为了解决这个问题,我们自然而然地想到让这个镜头在两种方案间过度:在这个长镜头的前半部分天空部分用涂抹增强方案,在后半部分街景部分用纹理增强方案,利用一个特别建立的渐变遮罩,使两个画风差距较大的画面平滑过度。


(原盘已经删了,天空部分为人工添加的高斯噪点,大家领会精神。实际原盘的噪点颗粒要更大一些)

这个处理如果写成代码的话大概会是这种感觉:

for i in range(0,100):
    tmp_blackmask = core.std.BlankClip(width=1920, height=1080,format=vs.GRAYS,fpsnum=24,length=1,color=(i/100))
    if i == 0:
        blank_mask = tmp_blackmask
    else:
        blank_mask = blank_mask + tmp_blackmask
modeA = core.std.Trim(...)
modeB = core.std.Trim(...)
res = core.std.MaskedMerge(modeB, modeA, mask=blank_mask, planes=[0,1,2], first_plane=True)

    但是观察实际处理效果的时候我们发现效果并不好,原因是由于我们希望最大限度保留细节,我们想让过渡开始的早一些,在天空完全移出镜头后即关闭涂抹模式(墙面细节被涂掉的就会少一些)。但由于我们使用的线性函数,在天空还没有移出画面的时候纹理增强效果就已经比较明显了(比如在我们设置的100帧渐变区间内,在进行到第50帧的时候实际上天空的颜色已有一半权重来自于天空表现比较肮脏的纹理增强画面,这不是我们希望看到的)。那么在改进的过程中,我们就想能否让这个渐变在开始的时候变化缓慢,在天空快要移出镜头前后的几帧内过渡快速,这样可以兼具双方优点。为此我们需要改进函数,最终我们使用了一个幂函数解决了这个问题,过渡图像大概像这样。

    这个例子就提醒我们,耿直的一次关系往往不能满足我们的全部需求,作为一个高雅的ripper有时候曲线是必不可少的。然而vs中能够让利用的曲线实际上并不多,仔细想了想似乎掰掰手指头一只手数的过来:幂函数、指数(对数)函数....没了。并且这些函数还很是江硬,唯一能控制的似乎是曲率,起点和终点难以控制,想要中途改变单调性还需要用矩阵翻转(比如没准哪天我们希望用到一条S型曲线呢?),真是不胜神烦。
    基于实际生产需求我们急切地希望寻找到一种具有以下优良特性的曲线:它要能够可以轻松控制曲率、起点和终点、以及变化趋势(单调性)。
    存在吗?存在,这就是我们今天要讲的第二部分主题,贝塞尔曲线了。

贝塞尔曲线是什么?

引用概述如下。简单来说,它是使用电脑绘图、应用在设计领域的曲线。

贝塞尔曲线的数学基础是早在 1912 年就广为人知的伯恩斯坦多项式。但直到 1959 年,当时就职于雪铁龙的法国数学家 Paul de Casteljau 才开始对它进行图形化应用的尝试,并提出了一种数值稳定的 de Casteljau 算法。然而贝塞尔曲线的得名,却是由于 1962 年另一位就职于雷诺的法国工程师 Pierre Bézier 的广泛宣传。他使用这种只需要很少的控制点就能够生成复杂平滑曲线的方法,来辅助汽车车体的工业设计。

正是因为控制简便却具有极强的描述能力,贝塞尔曲线在工业设计领域迅速得到了广泛的应用。不仅如此,在计算机图形学领域,尤其是矢量图形学,贝塞尔曲线也占有重要的地位。今天我们最常见的一些矢量绘图软件,如 Flash、Illustrator、CorelDraw 等,无一例外都提供了绘制贝塞尔曲线的功能。甚至像 Photoshop 这样的位图编辑软件,也把贝塞尔曲线作为仅有的矢量绘制工具(钢笔工具)包含其中。然而钢笔工具的曲线控制手感就是一坨屎

    逻辑上简单讲,它是一种有起点、终点与若干个参考点,实际动点从起点出发向终点运动,但在运动过程中不断向每个参考点倾向(只是靠近,一般不会经过),使运动轨迹发生变化的一条曲线。(因为计算机绘图并不像我们平常用笔画一样单纯,有的时候连确定起点终点都比较困难。而bezier可以自如控制起点与终点(区别于样条曲线等反例)也是其强大描述能力的体现之一。)


    比如说我们今天重点介绍的二阶贝塞尔曲线构造原理就非常单纯:两动点分别在AC和BC(图中分别为P0P1和P1P2)上匀速运动,而曲线轨迹点就在这两条动点的连线上运动。我们很容易发现,这条动连线的长度是变化的,故轨迹点亦非保持匀速,而是保持【其在动线段上的比例】与【两个匀速点在固定线段上的比例】一致。

    那么在vs中如何实现呢?
    首先来看简洁优美的n阶贝塞尔曲线定义式:
    
    进而得到二阶贝塞尔曲线参数方程如下:
    

    我们很容易地发现,除非类似过起点P0(0,0)和终点P2(1,1)、控制点P1在直线(a,1-a)上这种特殊情况(实际上NAZOrip工具箱中的渐变函数使用的就是这个方案)我们可以尝试联立方程消去参数t:
{x=(1-t)^2 0 + 2t (1-t) a + t^2 1
{y=(1-t)^2 0 + 2t (1-t) (1-a) + t^2 1
    否则对于一般性的控制点P1,直接取得x关于y的对应关系是困难的。

那么该怎么画?二阶贝塞尔曲线知x求y的过程@Kewenyu在DBmbk中为我们提供了一种思路,简单写大概是这样:

def bezier(i,depth=16,left=60,right=14,anc_x=0.5,anc_y=99):
    accur=0.0001
    def bezier_x(t):
        return 2 * anc_x * t * (1 - t) + t ** 2 
    def bezier_t(x):
        t = 0
        while t <= 1:
            if abs(bezier_x(t) - x) < accur:
                return t
            else:
                t = t + accur
    def bezier_y( t):
        return left * (1 - t) ** 2 + 2 * anc_y * t * (1 - t) + right * t ** 2
    return int(min(max(bezier_y(bezier_t(i/(2<<depth-1)))*(2<<depth-1)/100,0),2<<depth-1)+0.5)

    可以注意到的是,在参数方程由x过渡到y的过程中,求参数t的方法并非通过直接计算,而是以一种相当聪明的方式:通过枚举t的可能值并验算、通过验算即为求解的方式达成,现在我们终于明白为什么这个算法提出这么早但在计算机流行后才开始普及。
    按照我们这里的写法,将上述函数复制到py或vpy中,就可以直接使用bezier(x,位深,参数)函数得到当前x值在指定贝塞尔曲线下对应的y值。其中left和right指定其在第一象限内的起始点和终点(值域[0,100]),anc_x和anc_y分别指定参考点的坐标(出于个人习惯将anc_x的值域设定为了[0,1],效果与[0,100]是相同的,anc_y的比例尺为0-100但实际取值不受定义域限制)

    与我们这些确定三个关键点就知道画出的曲线大概是什么样的OLD ASS♂不同,新人刚接触赛贝尔曲线通常需要多观察以提高感性认识。上述函数提供计算单个x对应y的方法,那么如何才能知道整条函数曲线是什么样的呢?你可以选择下载DBmbk利用其中写好的快捷功能显示图像,或者干脆自己写一个ShowCurve()的函数。不过无论你用哪一种方式,都需要先安装python中matlab的模拟接口matplotlib以绘制图像。

    首先在联网状态下打开控制台(命令提示行),输入pip打开python自带的亲切友好的扩展包安装工具。接着输入命令pip install matplotlib就会自动展开安装进程了。安装包非常轻量大概8M,安装速度也很快。完成后可以输入pip list检查已安装的模块和包。(如果出现不认识pip(环境变量问题),请参考NAZOrip工具箱中的说明
    接着我们使用IDLE(或者VPY)调用matplotlib绘制图像,代码大概像这样:

import matplotlib.pyplot as plt
import NAZOripFunctions as nazo
x=[]
y=[]
for i in range(1023):
    x.append(i)
    y.append(nazo.bezier(i,10,60,90,0.3,2))
plt.plot(x,y)
plt.axis([0,1023,0,1023])
plt.show()

    大家可以很简单地看出来,上述脚本中在载入matplotlib.pyplot后,只需要将两个元素个数相同的列表(通过for循环生成了10bit下x对应y的列表,这里为了书写简单载入了NAZOfunc的快捷功能,实际上使用的是与上文相同的bezier函数)输入给plt.plot()它就会自动为你生成一条表示两者关系的函数图像。plt.axis()可以为图像指定坐标轴,plt.show()可以显示目前储存的图像。

    IDLE中可以直接执行,而如果你在vpy中调用,为了照顾vs的脾气还需要在plt.show()后面调用.set_output()方法输出。按F5执行,就可以绘制出各种各样优美的贝塞尔曲线了。

    熟练掌握二阶贝塞尔曲线的绘制方法后你会发现它有着广泛的应用,简单来说,几乎你在创建任何关系的时候都用到它,或者你可以用它替换任何你已知的线性关系,没有它做不到,只看你脑洞有多大。

    比如你现在能完全地理解上文中提到过的NAZOripFunctions中的过渡函数了。为了创建一个能在任何曲率下使剪辑A优美地(不要忽快忽慢、对称地)过渡到剪辑B的函数,我们放弃了使用指数(对数)或幂函数的方案转而采用贝塞尔曲线。有时候我们希望变化由慢到快,有时候我们希望变化由快到慢,为了保证变化的一致性,我们希望使这个函数(假设在第一象限内由(0,0)运动到(1,1))统一地以x=y为轴对称,所以理所当然地其参考点应该在k=-t+1(t∈[0,1])上运动,并分别在t的上下界使曲线获得最大曲率。我们将t做简单的(t-0.5)*2运算将其缩放到更好理解的[-1,+1]区间,这样我们就得到了一个可以在统一规律下调整曲率的优美函数。

        ( ↑ -1、0、+1的图像大概像这样,这里用1024个采样来表述x与y的变化关系,t(未在图中标示)如果标明的话应该分别在左上角、中心与右下角)

    再比如,用曲线关系替代原有线性关系的实验,我们可以试试用贝塞尔曲线绘制一条关于y=-x对称的曲线,代替原来的YC伸张方案。即在YC伸张(魔改版)的过程中,使中间区域的变化幅度减小,极亮和极暗区域的变化程度增大,看看能否通过变化趋势缓急的调整减小YC变换带来的负面效应。
    简单画了一下希望达到的变化曲线大概是这样(暂且忽略chroma平面):

    不过有一个问题是,由于我们使用的色彩模型中的色彩都经过伽马矫正,实际上我们所使用的亮度曲线是非线性的(比如用来表示暗色的颜色区间实际上比亮色多近一倍),这使我们难以找到一条中心线使曲线的黑白部分对称,所以我们在转换前需要先将色彩调整至线性以便我们寻找到y=1023-x这一条对称中心,上图表现的也是这种情况下的曲线。实际的BT.709曲线如下图,关于这部分在VCB公开课程中有详细描述,这里不再赘述。

代码写出来大概是这样:

import vapoursynth as vs
import mvsfunc as mvf
import NAZOripFunctions as nazo

src = core.lsmas.LWLibavSource(r"...",threads=1)
src16 = mvf.Depth(src,depth=16)
luma = core.std.ShufflePlanes(src16, 0, colorfamily=vs.GRAY).fmtc.transfer(transs="709",transd="linear")
luty = [nazo.bezier(x,16,0,100,0.7,30) for x in range(65536)]
luma = luma.std.Lut(0, luty).fmtc.transfer(transs="linear",transd="709")
res = core.std.ShufflePlanes([luma,src16],[0,1,2], vs.YUV)
res.set_output()

    上图为变换的实际效果(从上到下依次是源/正常YC伸张/贝塞尔曲线伸张),我们可以看到最终效果与上文中曲线预测一致地向暗色大幅偏移。高亮区域的细节倒是确实清晰了不少(注意樱花树),虽然同时也让画面变得更暗了,让这种处理并没有什么卵用。YC变换的正确方式就是线性变换,这里演示的方式是错误的。但这个例子提示给我们一个道理,当你学会使用二阶贝塞尔曲线工具以后,vs中任何的线性关系都可以转变为曲线,没有它做不到,只有你想不到。

    本篇文章主要介绍的是二阶贝塞尔曲线,高阶贝塞尔曲线不在介绍之列。vs的实际使用中,由于双转折曲线关系不常用,而在形容单一极值的曲线时三阶相对于二阶并无本质优势,却容易造成同一x对多y映射的问题,故几乎没有使用。高阶贝塞尔曲线如下图,在使用亮度自适应去色带逻辑时,若使用五阶方案,则可在传统暗场强亮场弱的基础上另外添加一个极值点,比如在加强暗色区域的处理强度之外单独强调对蓝色的天空的处理(因为其色带重灾区的身份,即便在亮场也常产生强色带),个中用法还请大家自行探索。

三、动态侦测

    在自适应处理中,动态则又是一个经常被引用的信息。鉴于人眼对快速掠过的物体不敏感的特性,如果我们找到一种放方法可以侦测到当前区域的动态处于一个非常高的值,那么很大程度上它意味着我们可以放飞自我瞎几把处理,因为反正一晃而过观众也看不清楚。这对高压缩制作组而言尤其有效,因为削减高动态区域信息量是提高压缩率的重要手段。

( * 正常的动态判断要同时判断一个区间内多帧情况才能保证准确性。但由于以大家目前的知识,如果在VS的线性结构中实现多帧预处理,随着预处理帧数的增加视频处理花费会成几何倍数增高(不现实),实用的预处理结构下一章也许会讲,这里我们暂且以单帧动态识别为例简述一下逻辑。)

    对于动态的最基本概念,我们知道如果一个画面中大部分区域保持静止不动(比如两个人在对话的文戏),那么它的前一帧和后一帧中绝大部分像素的色彩应该是相同的。基于此我们发现,只要通过拼接剪辑一个片段和它的副本,让他们交错排列,这种时候再用调色板(lut2或expr)对两帧进行分析,即可计算当前帧与前后帧之间的差异。简单来说比如这种感觉:

lut2=[]
n=24
for x in range(1024):
    for y in range(1024):
        if x-y<=n:
            lut2.append(0)
        else:
            lut2.append(1023)
src = (...)
src0 = core.std.Trim(src,1,main.num_frames-1) #删去第一帧使其相错
core.std.Lut2(src, src0, planes=[0,1,2], lut=lut2).set_output()

    实际上上述代码实现了一个类似于內建SCDetect的功能,由于只是单纯计算色差,如同作者所说它非常粗糙,在很多情况下都会瞬间暴毙发生误判。

    不过我们可以一点一点优化我们的逻辑,比如我们目前使用的是当前像素与前后帧相同位置的色彩信息之间的对比,进一步地我们想,能否调用像素的邻域信息来进行对比呢?答案当然是可行的,虽然vs的lut工具机能比较孱弱,但我们仍可以通过一些取巧的方式将其实现。比如同时创建两个参考帧的副本,并分别对他们进行膨胀和收缩(Maximum和Minimum\将当前像素替换为邻域内最大最小值),再设置一个阈值并将他们与源一同丢入expr,就可以很简单地实现以下逻辑:如果当前像素比它之前一帧的相同位置周的围9像素的最大值还要大n个值以上(或比周围9像素内最小值还要小n个值以上),那么我们就认为该像素发生了运动,标记为白色,否则我们认为该帧未发生运动(或运动幅度小到可忽略),标记为黑色。

    如果还要更进一步,我们可以使用svp对视频进行插帧,让动画每帧间的差距进一步减小(相当于变相扩大邻域范围),来提高预测精确度。由于预处理相比于精度更重视速度故mvtools在这里并不合适,百无一用的svp终于也有被人需要的一天,感动到落泪。上面两种思路结合起来简单写一下大概是这个样子:

#输入src
src8 = core.fmtc.resample(src, int(src.width/2), int(src.height/2), kernel='cubic').fmtc.bitdepth(bits = 8)
#svp setting
super_params="{pel:2,gpu:1}"                                                                                                
analyse_params="{gpu:1}"
smoothfps_params="{rate:{num:48000,den:1001,abs:true},algo:23}"                                                             
#frame doubling
super  = core.svp1.Super(src8,super_params)
vectors = core.svp1.Analyse(super["clip"],super["data"],src,analyse_params)
smooth = core.svp2.SmoothFps(src8,super["clip"],super["data"],vectors["clip"],vectors["data"],smoothfps_params)
srcsvp = core.std.AssumeFPS(smooth,fpsnum=smooth.fps_num,fpsden=smooth.fps_den)
#difference detection
srcsvp = mvf.Depth(srcsvp,10).std.ShufflePlanes( 0, vs.GRAY)
srcsvp_1 = core.std.Trim(srcsvp,1,length=srcsvp.num_frames-1)
srcsvp_1max = core.std.Maximum(srcsvp_1,planes=0)
srcsvp_1min = core.std.Minimum(srcsvp_1,planes=0)
limit = 2 ; times = 4
pre_motion = core.std.Expr([srcsvp,srcsvp_1max,srcsvp_1min], ["x y "+repr(limit)+" + > x y - "+repr(limit)+" - "+repr(times)+" * x z "+repr(limit)+" - < z "+repr(limit)+" - x - "+repr(times)+" * 0 ? ?"])
motion_1 = core.std.SelectEvery(pre_motion,2,0)
motion_2 = core.std.SelectEvery(pre_motion,2,1)
motion_2times = core.std.Merge(motion_1,motion_2)
f_clip = core.std.BlankClip(width=src8.width, height=src8.height,format=vs.GRAYS,fpsnum=24,length=1,color=0)
f_clip = mvf.Depth(f_clip,10).std.AssumeFPS(motion_2times)
motion_2times2 = f_clip + motion_2times
motion_2times3 = core.std.Trim(motion_2times,1,length=motion_2times.num_frames-1)
motion_2times = mvf.Max(motion_2times,motion_2times2)
motion = mvf.Max(motion_2times,motion_2times3)
motion = mvf.Max(motion_2times,motion_2times3).fmtc.resample( src.width, src.height, kernel='linear')
#motion = mvf.ToYUV(motion,css='420',full=True)
motion.set_output()

    简单解释一下,在上述脚本中我们首先使用svp将视频插成倍帧,而后使用上文逻辑判断出哪些像素运动了并将其标白。接下来我们再将视频流拆成两部分融合使其重新回归原来的帧数与帧率。之后我们让每帧的动态表现为周围三帧动态的最大值(为了解决动画制作中的一拍二与一拍三问题)。最后为了降低运算成本,我们让以上运算都在1/4分辨率下运行。

    通过上述逻辑,我们得以将动态信息转化为亮度信息,进而就可以利用这种有效的信息形式更精细的处理,比如我们可以利用PlaneStats计算帧平均亮度(即表示该帧动态),并设定在达到某阈值上即触发适应性高斯模糊(进一步地,可以设置平面区域的模糊力度比线条区域更大),简单写一下大概像这样:

import vapoursynth as vs
import functools
import math

def avg(n, f, clip, core):
    average_luma = f.props.PlaneStatsAverage
    sigma_edge = average_luma * 3
    sigma_nedge = average_luma * 7     
    mask_blur = core.tcanny.TCanny(clip_RGB, mode=1, op=3, sigma=1.8).std.Expr(["x 800 > 1023 x 200 > x 200 - 600 / 1023 * 0 ? ?"])
    blur_edge = core.tcanny.TCanny(clip_RGB, sigma=sigma_edge,  mode=-1)
    blur_nedge = core.tcanny.TCanny(clip_RGB, sigma=sigma_nedge,  mode=-1)     
    return core.std.MaskedMerge(blur_nedge, blur_edge, mask_blur, [0,1,2], True)
        
def lumaAvg(clip,matrix_s=None):
    core = vs.get_core()
    avgclip=core.std.PlaneStats(clip, plane=0)
    clip_RGB = core.resize.Bilinear(clip, format=vs.RGB24)
    last = core.std.FrameEval(clip_RGB, functools.partial(avg, clip=clip,core=core),prop_src=avgclip)
    last = core.resize.Bilinear(last, format=clip.format.id, matrix_s=matrix_s)
    return last
core = vs.get_core()
core = vs.get_core()
main = core.lsmas.LWLibavSource(r"...",threads=1)
main = #######上文描述的动态侦测处理#######
main = lumaAvg(main).set_output()

    上述例子中的动态搜索部分实际上演示的是FluxSmooth的(相当于没有的)动态侦测思路,总的来说是一个比较粗糙的方案,事实上除了高速外它几乎一无是处,用带有运动矢量侦测功能的插件来单纯计算帧间色差无异于脱裤子放屁。实际使用中我们为了提高搜索精度倾向于混合使用mvtools/Scxvid等带有矢量搜索的方案。同时为了不误伤有效信息需要对转场帧(I帧等)进行保护,而进一步地,为了使GOP区间内模糊强度平滑过度(出于编码器节约空间效率的考虑)又需要用到预处理结构。这里我们已经窥到了动态侦测的冰山一角,实用化的处理总的来说实现起来还是比较复杂的。怕是要再开一篇文章也未必讲得完。这里不再详述,看看下期组教学中有没有机会补完吧。

迷之压制组,这个组的一切都是迷。
我们下期再见。