迷之压制组 一键高压缩脚本说明 & 下载(2018.7Update)

NAZOrip@F
[email protected] 2018年06月21日
  • 在其它设备中阅读本文章
  • 更新内容位于文末 *

一、前言 & 功能简介

一键压制包括扫描信息、切割视频、批量生成脚本、批量压制、批量复检、批量封装等功能。下文将做简单介绍。

    如图,这是一个常见的使用HEVC编码、CRF模式压制的24分钟动画的比特率曲线。

    我们可以在图中看到若干个凸起,代表这些区域的码率突然增大,这通常是由于“转场产生的镜头快速平移”、“战斗场景产生的镜头剧烈晃动”、“回忆杀场景的加噪特殊效果”等原因,使瞬时动态增高而产生。而由于x265在CRF(恒定质量参数)模式下的特性,这些突然增加的信息往往被倾向保留,也就导致了体积不可控地增大。且上图展示的只是体积~1GB的常规压制动画的曲线,这种现象在低动态码率被压得相当低的高压缩中会更加明显。

    这也就导致了码率分配不均的问题,例如在常规的一集~150MB的动画中,可能存在十几秒到几十秒的高动态镜头,吃掉整个视频50MB的体积,而偏偏这些镜头却又是快速略过画面,由于人眼对其不敏感往往被观众所忽略,所以实际上相当于这部分体积被用来做了无用功。类似地,一个更加普遍的例子是,每集动画的OP和ED部分通常场景变动频繁、动态较高,但实际观影中直接跳过OP、ED的大有人在,极端情况下比如OP和ED的三分钟吃掉50%的体积,但却被观众跳过了,这部分体积相当于直接浪费,不得不说是一种遗憾,尤其对于高压缩Ripper来说更是不可接受。

    所以在这种场合,就会更加考验一名Ripper的技巧:如何更加科学地分配码率?用什么手段重新分配码率?使得珍贵的体积被用在“刀刃”上,这也是衡量一部压制作品好坏的一条重要标准。然而遗憾的是对于绝大多数压制组,在绝大多数场合之中,并没有足够的手段来对付这种情况。x265实际上提供了动态码率调整系统,即ABR(平均码率)模式,它的效果类似于滤波中的压缩处理,即限制影片的动态,使码率曲线更加平滑。然而由于我在VS搭建视频教学 里提到的种种原因(简言之是由于不稳定质量控制使得视频更容易出现量化瑕疵,且一旦出现就会继承到下一帧,累积效应惊人),ABR模式实际效果表现很糟糕。而CRF模式下其他动态相关参数(诸如qcomp、vbv等)限制效果又不尽人意。类似地,另一方面面对OP和ED占用空间的问题,我们又不能因为一部分人喜欢跳过OPED就直接将其剪掉。所以,绝大多数压制组对这种情况通常的处理方法就是不处理,这也实属无奈之举。

    关于解决方法,如大家所知地我组在过去的压制作品中会通过逐GOP调整RF(质量参数)值的方式,达到既限制码率,同时不因ABR机制产生量化瑕疵的效果。以往这项工序都是借由一些简单脚本半自动地完成,而由于组内OLD ASS生产积极性下降,恰好此时@msg7086大佬编译的mod版x265在摸鱼两年后终于想起了发表文章,被我们知道了分GOP压缩的使用方法。考虑到为了适应扩大再生产的需求,我们在这个时候把这套脚本完全地自动化,可以为以后的工作节省时间。同时在公网发表,如果正在看文章的各位有需要也欢迎自行取用。

简单来说,这套脚本会完成以下操作:

  1. 扫描放置于同一文件夹内的所有成序列的视频文件(比如一个季度的12集动画)。
  2. 生成每个文件对应的逐帧扫描文件,记录每帧的帧类型、帧空间等,并整理成比特率曲线。
  3. 根据每个视频的章节信息(以及Ripper的设定),将其分为若干段(如默认会单独提取出OPED),精确到帧。
  4. 依据Ripper设定的分段,对每个段落生成对应类型的vpy脚本(以及各自不同的CRF值等参数)。
  5. 依序对所有段落按设定进行压缩,采用x265mod的分GOP模式。
  6. 完成后扫描全部段落文件,获取全局平均码率。
  7. 以平均码率为基准,对码率过高的场景,动态计算CRF并重新压制。
  8. 整理后以原视频为单位封装成品(如12集输入即得到12集输出)

二、安装方法

    脚本使用python编写,为了维护方便并没有封装成exe程序,因为懒没有编写GUI,就保留它原原本本的样子了(摸了)。由于.py并不是windows默认的可输入类型(实际上可以通过修改注册表实现,不过考虑到一般人都不愿意改注册表),采用命令行传参的方式调用,具体安装方式如下:

  1. 首先搭建VS环境,具体请参考组员培训01,这里不再赘述
  2. 下载文件安装包(下载地址见文末),解压后得到如下文件
  3. 将site-packages内的文件,移动到你安装python36的目录下的\Lib\site-packages文件夹内 (建议配置完后,为所有扩展名为cmd的文件在桌面创建快捷方式)
  4. 将core64内的文件,移动到你安装vapoursynth目录的\core64\文件夹内(默认地址C:\Program Files (x86)\VapourSynth\core64\)
  5. 将ffmpeg放置到任意位置(推荐安装至C:\Program Files\)
  6. 如果你全部安装位置都是上文中推荐的默认位置,那么到这里就配置完成了。如果不是的话,你需要逐个打开(右键编辑)第三步当中的site-packages文件夹中的BitrateViewer_ALL.py / BitrateViewer_Onefile.py /G et_pack.py以及StartEncoding.py,修改每个文件开头的外部程序变量FFPROB、x265_pos、gop_muxer,使他们指向你的安装位置。(比如默认位置如下图,请根据情况自行修改)

三、使用方法(简易版)

    在配置完成后,可以试着进行第一次扫描检查安装是否正确。在上一章的安装过程中,我们得到了BitrateViewer_Onefile.cmd 、 BitrateViewer_ALL.cmd 、 Get_pack.cmd三个文件的快捷方式。他们分别的功能是:

  1. BitrateViewer_Onefile:拖入一个视频文件,调用ffprobe.exe进行扫描,后在视频目录下产生*_ffordered.csv和*_Bitrate Viewer.csv两个文件,分别记录该视频的逐帧信息,以及每秒的比特率。使用excel打开*_Bitrate Viewer.csv,以第三列数据产生簇状柱形图,即可得到文章一开头展示的HEVC编码比特率曲线了。
  2. Get_pack:在msg7086mod版x265中,如果采用gop模式,会输出一系列切除头文件的GOP数据流(如下图)。这些文件在普通状态下是没有办法查看的,需要事先通过命令提示行封装,具体方法可以参考msg7086的说明。Get_pack是一个简便通道,拖入一个gop数据文件(如A.data-999),会在同目录下生成仅含该GOP的MP4文件
  3. BitrateViewer_ALL:这个文件与上述两个简单功能不同,是第一章中提到的一键压制功能的输入口。由于精确到帧分割依赖视频的帧信息,所以必须先对所有视频进行扫描。使用方法,把同一季度所有视频放置于同一文件夹内,要求视频成序列命名,目录尽可能不含中文,如下图。

    将其中任一视频拖入BitrateViewer_ALL.cmd,即开始扫描,待出现“按任意键继续”的提示即扫描结束。扫描每个视频根据CPU和硬盘速度不同需要几十秒到几分钟不等。

    扫描得到的文件会储存在主目录下的Bitrate_Viewer文件夹中。同时主目录下会生成若干文件,分别是:

    Trim.txt:记录每个文件的分章节帧数、以及该段的名称,如果识别到OP和ED会单独记录他们的长度,如下图

    runfiles_setting.txt:记录压制设置。其中RUN表示希望压制的段落,EXCEPT表示希望排除的段落,后者优先级较高,段落间以半角逗号区隔。例如如果RUN为空,EXCEPT后输入OP,ED 表示希望压制Trim.txt中除了OP、ED之外的所有段落。例如RUN后输入A,则表示单独压制所有包含A的段落。

    x265_params.txt:输入x265压制参数,不包含CRF

    program log.ini:这个脚本没有做严格的异常测试,只是在编写的过程中随手加了一些异常记录(压制崩溃可能导致宕机等问题,故采用IO方案)。这个文件内会记录脚本运行日志,如果执行出错,请第一时间参考该日志。

    Start_Encoding!.cmd:在正确执行扫描流程后会产生该文件,作为接下来操作的入口,如果扫描失败则不会产生。(在正确配置默认vpy文件后)双击该文件则依次开始生成脚本、分段压制、逐GOP复检、统一封装等步骤。右键编辑打开,更改第三行的“mode0”可以更改执行模式,分别为:

  • mode0:一键模式
  • mode1:仅生成脚本
  • mode2:仅开启主压制进程
  • mode3:仅进行复检
  • mode4:仅进行封装

    该目录下同时会生成辅助文件check.vpy和compare.vpy,用来检查和对比单个或多个视频文件。生成的脚本会放置在Scripts文件夹下,封装好的文件会放置在Finished文件夹下。

重要:关于配置默认脚本Default.vpy
    作为默认批量生成vpy脚本的模板,Default.vpy当中需要用特殊文字序列标注位置,分别是__filenames__表示该位置可替换文件名,__triminfos__表示该位置可替换帧数,__crf=[浮点数]__表示该段落默认的CRF(如下图)。批量生成脚本的逻辑为:如果搜索到指定名称的脚本,则以其为模板,否则以Default.vpy为模板。例如,如果希望对OP单独使用与其他部分不同的脚本,并指定CRF为23,则需要在主目录下创建一个名为OP.vpy的文件,用同样的方法标记__filenames__和__triminfos__的位置,并更改CRF为__crf=23__,再运行Start_Encoding!.cmd,即可对所有OP分段采用单独脚本和CRF23压制,并不影响其他段落。


四、部分实现逻辑(上手者向)

为了让同学们更方便地使用脚本,本章简述部分功能的实现逻辑。其实一共没有几行代码你可以自己去看,这里写成自然语言方便一些不想看代码的懒人。

1、关于扫描与文件名:
    脚本对ffporbe扫描进行了多线程优化,默认会吃满你CPU的全部核心(由于ffmpeg官方不建议使用超过16线程,故最大限制在16线程)。并对扫描后可能产生的乱序文件重新排序,精确性请无需担心。
    脚本采用正则表达式识别同一文件夹内的序列文件,虽然简单写了写排异略微提高了兼容性,但仍然需要使用者在命名时遵照一些规则,否则容易出现识别错误的情况。简单来说,请保持在全英文文件名下,只用1-3位阿拉伯数字区分序号(默认将连续四位数全部排除),文件名的其他部分需完全相同。
    举例来说:

[ANK-Raws] 幼女戦記 01 (BDrip 1920x1080 HEVC-YUV420P10 FLAC).mkv
[ANK-Raws] 幼女戦記 02 (BDrip 1920x1080 HEVC-YUV420P10 FLAC).mkv ...能识别

[NAZOrip] CLANNAD 第1回 桜舞い散る坂道で (1080P.HEVC.YUV420P10.QAAC).mkv
[NAZOrip] CLANNAD 第2回 最初の一歩 (1080P.HEVC.YUV420P10.QAAC).mkv ...不能识别

00001.m2ts
00002.m2ts ...能识别

Angel Beats! 2010 - EP01 [BD 1920x1080 AVC-yuv420p10] - mawen1250.mkv
Angel Beats! 2010 - EP02 [BD 1920x1080 AVC-yuv420p10] - mawen1250.mkv ...能识别

Bakemonogatari 2009 - 0001 [BD 1920x1080 23.976fps AVC-yuv420p10].mkv
Bakemonogatari 2009 - 0002 [BD 1920x1080 23.976fps AVC-yuv420p10].mkv ...不能识别

2、关于目录名与中文:
    python本身是支持gbk编码的,所以所有中文都可以无缝输入。不建议使用中文目录是由于x265在使用中文目录的pipe传参时有一定概率会出现莫名的bug。同样地,建议目录尽量精简也是由于Yuukimod的拆分gop模式(在我个人测试时)对过长的目录出现了各种奇奇怪怪的bug,目前原因未知。如果你在运行脚本的过程中也出现了闪退问题,请更换目录试一试。

3、关于桥文件Trim.txt和分段:
    右键编辑Bitrate Viewer ALL/Onefile.py,你可以修改第七行的变量fenduan为任意值(默认为1,不建议修改值过高)。其逻辑为,将每个视频中的最长段落N等分,例如在如下默认分段EP01-A 0,31264;EP01-ED 31265,33421;EP01-EN 33422,34575的三段中,由于没有OP,显然A段最长,如果fenduan==2,则会将A段切割为A1和A2两段,每段长度约为15500帧。值得注意的是,识别默认采用I帧为分界点,即在本例中,二等分长度应为31264/2=15632,则程序则会在15632附近搜寻最近的I帧,并以其为分界点分割两段视频。
    脚本会优先判断是否已存在扫描结果,即经过一次扫描后之后再运行脚本只会生成新的分段文件,而不会再次扫描。
    Bitrate Viewer ALL扫描后得到与下一步有关联文件核心桥为Trim.txt。如果对自动分段不满意,你可以手动对该文件的分段进行修改,只要满足相同格式,后续步骤就会以此为准执行。
    显然,这种分段方案支持预处理结构,但不支持手动Trim分段(后采用不同方案)的脚本。

4、关于批量压制的逻辑:
    脚本会首先根据你希望压制的段落在main\Scripts\文件夹内生成若干个脚本,并随着压制的进行删除已完成脚本。如果检测到某个压制进程异常中断则会重新开始对该文件的压制,若检测到同一文件多次崩溃则会退出程序并记录日志。如果在该过程中人为希望结束可以直接按右上角X关闭,(在未更改配置的前提下)下次启动脚本时会自动搜索上次未完成的任务重新开始压制。压制生成的文件保存在main\Tempfiles\文件夹中。
    这也导致了,比如如果你希望对vs脚本进行修改,重新压制,那么你需要删除掉整个main\Scripts\文件夹后重新运行StartEncoding.cmd,否则程序会默认继续上次的任务。
    各段落脚本默认不会备份,如果你希望找到某个已完成(已被删除)的脚本,请更改至mode1运行StartEncoding.cmd
    参考模板等信息可以在日志文件中查到,如下图。

5、关于复检程序:
    具体逻辑为:扫描全部已完成GOP,计算出每个GOP的平均比特率列表,取该列表的中位数与平均比特率中的较小值,以该值作为整个视频的基准码率(典型的偏态分布可能导致平均数失衡,而中位数则可以取得良好效果。同样地,由于各GOP长短不一,在正态分布中中位数可能产生误差,此时使用平均数校正),也就是说脚本认为整个视频中“大部分”区域都接近于这个码率。进而对于每一个GOP,如果其码率超过该平均值若干倍,一旦触发阈值(默认4.5倍,可修改)之后,即假设对于一个平均码率1000kbps的动画,当某个GOP超过4500kbps以后,脚本即判断需要对该GOP的动态进行限制。
    具体的限制方法,由于在一定范围内随着CRF降低视频体积成指数级上升(通常认为14-28区间内每区隔6-7crf体积翻倍),我们采用了一条对数曲线计算应限制到多少CRF,这条曲线大概是这样一种感觉。

    横轴为比特率是均值的x倍,纵轴对应应增大y的CRF。例如在默认曲线中,如果比特率为5倍,CRF会增大1.44并重新压制。

6、关于重新压制的CRF比较逻辑:
    脚本在完成每个GOP时会生成文件,记录该GOP采用的参数、CRF等等。当重新压制时会调用这些参数并与目标CRF进行比较。例如设想以下一种情况:我们手动指定默认的CRF为21,OP部分的CRF为23并进行了第一轮全局压制,此时开启复检脚本,若发现OP段落中某GOP体积为均值的5倍,则理论上应该以CRF22.44重新压制。然而此时由于调用之前的完成数据进行判断,已有CRF23>22.44,故该GOP会跳过压制。而假如其下一个GOP体积为均值8倍,则理论上应以对应CRF21+3.32进行重新压制,由于24.32>23,此GOP即会被重新压制。差距倍率越大,增加CRF越多。

7、关于复检前的备份:
    复检并没有制作记录哪个GOP重新压过哪个并没有的功能(因为懒得做了),所以这导致一个问题,如果复检进程中途中断,重新开始复检时已经被重压过的文件会被识别为没重压过的文件,影响CRF倍率的判断。并且每经过一次复检平均CRF就会降低。所以为了操作方便,在复检进程开始之前我们备份了整个Tempfiles文件夹内的所有临时文件。如果复健进程中途中断,请删除Tempfiles文件夹,并将Tempfiles.backup改名,恢复该文件夹至初始状态。

8、关于文件自动覆盖:
    Bitrate_Viewer_ALL生成的文件除了Trim.txt和StartEncoding.cmd每次都会被覆盖,其他文件如果已存在则不会被覆盖,包括runfiles_setting.txt,program log.ini,x265_params.txt,Default.vpy,Compare.vpy,Check.vpy。
    每次执行StartEncode.cmd,Scripts文件夹内的脚本在除mode1模式外都不会被覆盖。Tempfiles文件夹内的文件除了已完成GOP外都会被重新覆盖。每次执行复检,目标GOP都会被覆盖,每次执行封装,封装后文件都会被无条件覆盖。如果使用Get_pack.cmd封装单个GOP,由于所有文件在备份状态下执行,则不会干扰已有文件。

9、关于封装:
    这套系统虽然在理论上没什么问题,在复检时我已添加--min keyint 9999的参数保证第二次压缩的GOP长度与第一次相同(否则实际上由于预载入帧数不同,x265插入I帧的倾向性会发生变化,可能导致即便在通参数下原本是一个的GOP被分段),但仍然不确定可能在某处留有奇怪的bug。作者在编写本文的时候想起来,原本还有计划在最终封装时检查封装文件是否与源文件长度一致,不过似乎最后忘记了(诶嘿嘿☆)。既然脚本已经发了近期可能没时间改了,就让它摸了吧。
    所以请在封装后手动检查一遍文件是否正常,是否多/少帧,如果出现欢迎反馈bug,虽然不会修就是了。


五、其他的一些提示

  • 该脚本针对使用对象仅为CRF10-30的高参数x265压制,由于Medium以上预设下x265插入I帧倾向性骤降,导致GOP几乎不以转场帧分隔,而是每个GOP都保持在最大GOP帧数,这种情况下会导致逐GOP调整的参考性严重降低。
  • 为了让每个文件都能独立使用,所有脚本之间没有写互相载入关系,不过函数是封好的,如果你要魔改应该也很方便。
  • 关于章节扫描,由于依靠调用MediaInfo动态链接库来实现,这款软件虽然对mkv没什么影响,但在对蓝光原盘的支持上有一些bug,诸如共享同一mpls文件的两个m2ts只能识别出前者的章节信息之类的,关于这方面目前还没什么办法。
  • 经过一段时间的测试,编码器引起的稳定性问题无法解决。原版脚本里虽然加入了外部调用结果侦测,但没有加入外部干预手段。这导致一个问题:x265直接崩溃不影响脚本运行,脚本会自动重新启动。但若x265发生异常而挂起,则脚本会被阻塞。前几天遇到这个问题,遂写了个一个外部监控,自动关闭挂起的x265-10b.exe(或vspipe.exe进程),使脚本可以顺畅运行。复制下述代码至.py文件并运行即可,需要提前安装psutil(pip install psutil):
import psutil,time,threading,functools
sleeptime = 2 #每2秒扫描一次
moni_list = ['x265-10b.exe','vspipe.exe'] #监视列表

def monitor(pname,num,sleeptime=sleeptime):
    global process
    while True:
        time.sleep(sleeptime)
        try:
            process[num] = psutil.Process(process[num].pid)
            if process[num].name() == pname:
                lst = process[num].children()
                if lst != []:
                    for i in lst:
                        if i.name() == 'WerFault.exe':
                            process.append(i)
        except:
            process[num] = None
            for proc in psutil.process_iter():
                pinfo = proc.as_dict(attrs=['pid','name'])
                if pinfo['name'] == pname:
                    process[num] = psutil.Process(pinfo['pid'])
        #若监视列表中任一程序抛出异常,则SIGKILL全部列表
        if len(process)>len(moni_list):
            mutex.acquire()
            for i in process:
                i.kill() if i !=None else None;
            process.pop()
            mutex.release()

process = []
mutex = threading.Lock()
for num,pname in enumerate(moni_list):
    process.append(None)
    threading.Thread(target=functools.partial(monitor, pname=pname,num=num)).start()

六、下载地址

Github / BAIDU 密码:zzhy/NAZOrip
内含FFmpeg、mediainfo以及x265Asuna_2.6,不保证最新,有需求可以去官网下载最新版替换。

    蛤仔
    蛤仔  2018-04-22, 17:26

    下载地址掛了喔~兩個都是。

      NAZOrip@F
      [email protected]  2018-07-16, 00:48

      台湾地区目前无法使用百度云

    凌璇23
    凌璇23  2018-03-24, 18:50

    “x265-10bit-full已停止工作”???我前几天还能用的。vpy已preview,无问题。
    C:Windowssystem32>"C:VapourSynthcore64vspipe.exe" --y4m "D:BaiduYunDownloadNAZOrip_ONE_KEY_ENCScriptsEP02-A.vpy" - | "C:VapourSynthcore64x265-10bit-full.exe" --y4m -D 10 --crf 21.5 --psy-rd 1.2 --psy-rdoq 1.6 --pbratio 1.20 --qcomp 0.65 --aq-mode 3 --aq-strength 0.7 --deblock=-1:-1 --preset veryslow --rd-refine --no-open-gop --no-info --ctu 32 --merange 44 --me 3 --subme 3 --cbqpoffs -2 --crqpoffs -2 --bframes 10 --b-intra --b-adapt 2 --weightb --rd 5 --ref 4 --scenecut 40 --keyint 480 --min-keyint 1 --rc-lookahead 72 --no-psnr --cu-lossless --no-ssim --no-sao --no-rect --no-amp --input-depth 10 --lookahead-slices 4 --output "D:BaiduYunDownloadNAZOrip_ONE_KEY_ENCTempfiles02A.gop" -
    y4m [info]: 1920x1080 fps 24000/1001 i420p10 frames 0 - 1509 of 1510
    x265 [info]: Using preset veryslow & tune none
    gop [info]: output file: D:BaiduYunDownloadNAZOrip_ONE_KEY_ENCTempfiles02A.gop
    Error: fwrite() call failed when writing frame: 4, plane: 0, errno: 32
    Output 13 frames in 49.40 seconds (0.26 fps)
    Core freed but 6220800 bytes still allocated in framebuffers

      凌璇23
      凌璇23  2018-03-24, 19:23

      把vpy拖入自己写的vspipe.exe-|x265.cmd,x265正常运行

        凌璇23
        凌璇23  2018-03-24, 20:13

        好吧,是目录太长的问题。(参考该文章"2、关于目录名与中文")
        视频文件改位置,重新运行就好了