只是一时兴起写一篇教程,本篇教程内容与本组压制时所采取的处理方式无绝对关系

关于标题中的「低画质DVD源」:
因为各种关系国内一般拿不到里番DVD原盘,而只有使用hikiko123所压制的DVDRip。
而该源的画质比DVD要差非常多。

由于编写压制脚本非常复杂,本篇文章内容难以详尽,内容可能比较复杂、跳跃。

VapourSynth安装

一般使用该打包好的便携版:https://github.com/AmusementClub/tools/releases

安装微软的 Visual Studio Code
然后在拓展商店中安装Python、Pylance、Black Formatter(可选):

打开 设置 => 配置文件 => 显示配置文件内容 ,在settings.json中间添加:

{
    .......
    "files.associations": {
        "*.vpy": "python"
    },
    "python.languageServer": "Pylance",
    .......
}

keybindings.json中写入:

[
    {
        "key": "f5",
        "command": "workbench.action.tasks.runTask",
        "args": "vspreview"
    },
]

tasks.json中写入:

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "vspreview",
            "type": "shell",
            "command": "${command:python.interpreterPath}",
            "args": [
                "-m",
                "vspreview",
                "\"'${file}'\"",
            ]
        },
    ]
}

此时打开任意vpy文件后在vscode右下角选择解释器为便携版中python路径即可。
按下F5即可打开脚本预览。

基础处理

由于hikiko123的DVDRip存在诸多问题,一般需要进行一些处理后再超分。

import vapoursynth as vs
import mvsfunc as mvf
core = vs.core
core.max_cache_size = 4000
# OKE:INPUTFILE
src = "[230303][ばにぃうぉ~か~]OVA母乳ちゃんは射したい。#1セル版(No Watermark).mp4" # 输入
# OKE:DEBUG
debug = True
if debug != True:
    debug = False
# 使用精度高的LWLibavSource读取视频
# fpsnum和fpsden相除是帧率,一定要载入准确帧率(23.976即为24000/1001)
res = core.lsmas.LWLibavSource(
    src,
    fpsnum=24000,
    fpsden=1001,
)
# 提升精度至16bit进行后续处理
res = core.fmtc.bitdepth(res, bits=16)
# 由422提升至444
res = mvf.ToYUV(res, css="444", full=False)
# 输出视频
res.set_output(0)

其中,# OKE:INPUTFILE是标记输入文件的,OKEGui会自动替换;# OKE:DEBUG则是标记调试环境的。

按下F5就能打开vspreview进行预览:

选项功能都是比较好理解的,左下角进度条下的Output 0是当前输出节点,由于只有一个输出所以序号为0(可按数字切换输出节点)。
DVDRip是720*480,正常播放时播放器会识别像素宽高比但vspreview中不会,所以画面看上去窄了不少。

因为写set_output()有点麻烦并且有时需要快捷地调试输出效果,所以我个人会写一个专门处理输出的函数:

# 输出节点序号
output_num = 0
def c(clip: vs.VideoNode, label=None, preview=False):
    global output_num
    output_num += 1
    # 如果label带"_diff"字样则对clip的值进行扩大,可以更清楚对比差距
    if label != None and "_diff" in label:
        exp = "x {value} - 10 * {value} +".format(
            value=(1 << (clip.format.bits_per_sample - 1)) - 1
        )
        clip = clip.std.Expr([exp])
    # 将label的字添加在右上角
    if label != None:
        clip = core.text.Text(
            clip, text=label, alignment=7, scale=clip.height > 720 and 3 or 2
        )
    if preview:
        mvf.Preview(clip, depth=8).set_output(output_num)
    else:
        clip.set_output(output_num)
# 输出视频
c(res, "res")
# 如果不是debug环境则输出最终结果
if not debug:
    res.set_output()

这样可以在调试的时候输出res并加上角标,而在压制的时候直接输出res。

VapourSynth使用python,但就算不熟悉python也无妨,借助https://vsdb.top/上的各种滤镜很简单就能进行处理。
一些滤镜可能没有收录在vsdb上,如果需要可以谷歌或者在github上搜索。

预先处理

由于一些原因,部分片源的边界有几个像素宽的区域可能有问题,例如对于つるぺた守護騎士 エルフィナ堕ちる ~前編~:

四个边界都是有问题的——但直接这样看几乎看不出来。
Ctrl+滚轮放大到最大倍率观察左上角:

没错,有一个像素宽的区域明显很灰。

left = 1
top = 1
right = 1
bottom = 1
res = core.edgefixer.Continuity(res, left=left, top=top, right=right, bottom=bottom)
c(res, "fixedge")

一般可以考虑直接Crop剪切掉边界,或者修复这部分数据。
使用edgefixer后的效果:

然后进行抗锯齿:

import vsaa
from functools import partial
def sdaa(clip: vs.VideoNode, aatype: int = 1, sraa=None):
    # 抗锯齿只对亮度平面进行处理
    Y = core.std.ShufflePlanes(clip, planes=0, colorfamily=vs.GRAY)
    if aatype == 1:
        args = dict(field=1, dh=True, nsize=3, nns=2, qual=2)
        try:
            daa = partial(core.nnedi3cl.NNEDI3CL, **args)
        except:
            try:
                daa = partial(core.znedi3.nnedi3, **args)
            except:
                daa = partial(core.nnedi3.nnedi3, **args)
    elif aatype == 2:
        args = dict(field=1, mthresh=10, lthresh=20, vthresh=20, maxd=24, nt=50)
        try:
            daa = partial(core.eedi2cuda.EEDI2, **args)
        except:
            daa = partial(core.eedi2.EEDI2, **args)
    elif aatype == 3:
        args = dict(field=1, dh=True, alpha=0.5, beta=0.2, gamma=20.0, nrad=3, mdis=30)
        try:
            daa = partial(core.eedi3m.EEDI3CL, **args)
        except:
            daa = partial(core.eedi3.eedi3, **args)
    if aatype == 4:
        aaed = vsaa.upscaled_sraa(Y, **sraa)
    else:
        aaed = (
            daa(daa(Y).fmtc.resample(clip.width, clip.height, 0, -0.5).std.Transpose())
            .fmtc.resample(clip.height, clip.width, 0, -0.5)
            .std.Transpose()
        )
    # 对抗锯齿结果进行修复避免部分像素出错
    out = core.rgvs.Repair(aaed, Y, mode=2)
    out = core.std.ShufflePlanes([out, clip], [0, 1, 2], colorfamily=vs.YUV)
    return out
aatype = 4 # 1:nnedi3, fast weak; 2:eedi2, balanced; 3:eedi3, slow strong; 4:sraa
sraa = dict(
    rfactor=2,
    ssfunc=vsaa.Nnedi3(),
    aafunc=vsaa.Eedi3(),
)
res = sdaa(res, aatype=aatype, sraa=sraa)
c(res, "aa")

效果差不多是:

再是进行dehalo,一般直接使用finedehalo即可:

import havsfunc as haf
res = haf.FineDehalo(res, rx=2.0, darkstr=0, brightstr=1.0)
c(res, "dehalo")

效果为:

降噪和去色带:

Y = mvf.BM3D(core.std.ShufflePlanes(res, 0, vs.GRAY), radius1=1, sigma=2)
UV = core.knlm.KNLMeansCL(res, h=0.4, d=2, a=1, s=3, wmode=2, device_type="GPU")
res = core.std.ShufflePlanes([Y, UV], planes=[0, 1, 2], colorfamily=vs.YUV)
c(res, "denoise")
range1 = 12
y1 = 72
c1 = 48
range2 = 24
y2 = 56
c2 = 32
thr = 0.55
elast = 2.5
deband = core.neo_f3kdb.Deband(res, range1, y1, c1, c1, 0, 0, output_depth=16)
deband = core.neo_f3kdb.Deband(deband, range2, y2, c2, c2, 0, 0, output_depth=16)
res = mvf.LimitFilter(deband, res, thr=thr, thrc=thr * 0.8, elast=elast)
c(res, "deband")

经过上述所有处理后的细节效果:

这些只是简单的处理。
如果要得到更好的效果可以慢慢调试、选用效果更好(但可能降低处理速度)的滤镜。
中间一些环节可以优化,例如在处理完线条之后进行补偿性锐化(上面这个结果的锐度有些低了)。

超分

为了方便与WEB源对比,演示只超分到720p。

说明一下我个人对几个常用模型的看法:
Waifu2x,我个人最喜欢的模型,准确度够高、破坏力低,较为保守,降噪不要开2、3等级。
CUGAN,如果使用我个人推荐用legacy不要用pro,有alpha参数调节强度但推荐不要大于0.7,破坏力较强。
因为CUGAN会修特别多东西,所以用CUGAN的话大概率前面进行的处理没什么效果(全被CUGAN重新修了)。
降噪能关就关,CUGAN推测太多了关掉降噪都留不住什么噪点。
Real-ESRGAN,很多压制者都是用的这个,破坏力极强。
留不住噪点,锐度极其极其高——就算将锐度过低的DVDRip超分到720p跟WEB-DL对比,ESRGAN的线条都能细上一圈。
有一定强度的去雾效果,画面本身有一定模糊的地方会被锐化得跟常规区域锐度相近。
这些观点有明显的个人偏好,如果喜欢使用某个模型那用即可。

import vsmlrt
# scale为放大倍率,目标分辨率除以480然后向上取整即可
# noise为降噪等级
waifuarg = dict(
    noise=-1,
    scale=2,
    model=vsmlrt.Waifu2xModel.cunet,
    preprocess=False,
)
rgb = mvf.ToRGB(
    res,
    depth=32,
    sample=vs.FLOAT,
    full=False,
)
check = True
# 轮流尝试TRT、CUDA、NCNN
for i in [vsmlrt.Backend.TRT(), vsmlrt.Backend.ORT_CUDA(), vsmlrt.Backend.NCNN_VK()]:
    try:
        upscaled = vsmlrt.Waifu2x(rgb, **waifuarg, backend=i)
        if debug:
            print(i)
        check = False
        break
    except:
        continue
if check:
    raise
upscaled = mvf.ToYUV(
    upscaled,
    full=False,
    css="444",
    depth=16,
    sample=vs.INTEGER,
    matrix=mvf.GetMatrix(res),
)
# 用常规插值降回目标分辨率
res = core.fmtc.resample(upscaled, 1280, 720)
c(res, "upscaled")

将直接使用Waifu2x进行放大、经过上述处理之后Waifu2x放大、WEB-DL进行对比:

直接进行放大时,因为DVDRip本身处理问题造成线条非常脏,这些问题也保留到了超分结果中(并且可能更难处理了)。
其中,如果halo过重可能模型会当做线条进行锐化,让结果的halo更加难看。

不过Waifu2x对比其它模型还是偏保守了,线条的锐度不够高,对比WEB-DL就显得比较糊(一方面这个WEB-DL是高画质源,本身差距太大了)。
线条锐度多少还能用锐化救一救,但是背景树叶区域算是没救了,纹理过于复杂、DVD低分辨率本身信息量不足,不是模型所能够解决的。

如果直接用Real-ESRGAN进行放大:

锐度远超WEB-DL,不少线条被涂掉并且画面模糊效果损失殆尽。
要使用Real-ESRGAN的话应该需要用各种手段限制副作用,我个人不怎么用就不清楚了。

在超分之后可以考虑使用addgrain加噪。
输出前降回420和10bit:

res = mvf.ToYUV(res, full=False, css="420")
res = core.fmtc.bitdepth(res, bits=10)

最后压制可用命令行,简单快捷的话可以使用OKEGui。

结束语

hikiko123的DVDRip真的是太烂了,怎么来都只能死马当活马医。

文中的各种处理与我们平时常用的有不少出入(出于效率考虑),毕竟直接用WEB源进行处理根本就不需要这么折磨了。
以及本篇内容只是一时兴起随便写的,极其不详尽。