Keywords: Vapoursynth 修复 灰阶 错误 视频 Gray scale range color

查看对比

起因:前几天朋友发给我一段直播源,问我视频的灰阶是不是有点怪。我点开一看,确实是有些偏白,起先还以为是舞台干冰的效果,直到画面切换时,查看黑点的 RGB 值居然为 (16,16,16)。综合判断,我估计视频的灰阶范围只有 16~235,这才导致画面“黑”不下来,也“亮”不上去。

问题根源:16~235
修复后的结果(视频截图)

错误的黑阶范围严重影响了观感,我决定尝试修复。我使用的方式是编写 Vapoursynth 脚本将 16-235 的灰阶范围扩展成 0-255,最后输出给 x264 转码(电脑性能有限,x265 速度比较慢)以实现最终目的。

首先,你需要有一个 Vapoursynth 环境和 Vapoursynth 脚本编辑器,安装教程可以在 Google 上查看。

我使用的是 VapourSynth-Editor,可以在 Github 上找到。在视频同目录下新建脚本 Untitled.vpy ,先导入库和视频,接着,我尝试如下代码,把 limited 扩展到 full

from vapoursynth import core
src = core.lsmas.LWLibavSource(r"video.mp4")

#expand limited to full
clip = fvsfunc.Depth(someinput, bits, range_in='limited', range='full')

F5 预览,结果却没有变化。再次分析,发现问题并不是出在范围上。查看一下 MediaInfo

Video
ID                                       : 1
Format                                   : AVC
Format/Info                              : Advanced Video Codec
Format profile                           : High@L4
Format settings                          : CABAC / 1 Ref Frames
Format settings, CABAC                   : Yes
Format settings, Reference frames        : 1 frame
Format settings, GOP                     : M=1, N=60
Codec ID                                 : avc1
Codec ID/Info                            : Advanced Video Coding
Duration                                 : 1 h 32 min
Bit rate                                 : 4 855 kb/s
Width                                    : 1 920 pixels
Height                                   : 1 080 pixels
Display aspect ratio                     : 16:9
Frame rate mode                          : Variable
Frame rate                               : 30.000 FPS
Minimum frame rate                       : 29.412 FPS
Maximum frame rate                       : 30.303 FPS
Color space                              : YUV
Chroma subsampling                       : 4:2:0
Bit depth                                : 8 bits
Scan type                                : Progressive
Bits/(Pixel*Frame)                       : 0.078
Stream size                              : 3.13 GiB (97%)
Color range                              : Limited
Color primaries                          : BT.601 NTSC
Transfer characteristics                 : BT.601
Matrix coefficients                      : BT.601
Codec configuration box                  : avcC

可以看到 Color range 并没有问题,不知道直播前视频流经过了什么错误转换,导致从 YUV 格式转回 RGB 后,范围还是有问题,和 Color range 无关。于是我决定使用数学表达式,将 16-235 变换到 0-255。这里请教一下 ChatGPT:

chatgpt's response

通过一番沟通,得到了如下的代码:

# 转换YUV420P8,bt601到RGB24
import vapoursynth as vs
rgb_clip = core.resize.Bicubic(src, format=vs.RGB24, matrix_in_s="470bg", transfer_in_s="601", primaries_in_s="170m", range_in_s="limited")

# RGB16~235到0~255的线性拉升
def rescale_value(value):
    return (value - 16) * 255 / (235 - 16)

expr = "x {0} - {1} * {2} /".format(16, 255, (235 - 16))

# 处理拉升
rescaled_clip = core.std.Expr(rgb_clip, [expr, expr, expr])

# 转回YUV空间
output_clip = core.resize.Bicubic(rescaled_clip, format=vs.YUV420P8, matrix_s="709", transfer_s="709", primaries_s="709", range_s="full")

这里要注意,由于视频流的 Color primaries Transfer characteristics Matrix coefficients 比较特殊,因此要额外注意第 3 行的参数。

最终得到的代码如下:

from vapoursynth import core
from fvsfunc import Depth
core.max_cache_size = 12000
src = core.lsmas.LWLibavSource(r"pre.mp4")
 
# 转换YUV420P8,bt601到RGB24
import vapoursynth as vs
rgb_clip = core.resize.Bicubic(src, format=vs.RGB24, matrix_in_s="470bg", transfer_in_s="601", primaries_in_s="170m", range_in_s="limited")
 
# RGB16~235到0~255的线性拉升
def rescale_value(value):
    return (value - 16) * 255 / (235 - 16)
 
expr = "x {0} - {1} * {2} /".format(16, 255, (235 - 16))
 
# 处理拉升
rescaled_clip = core.std.Expr(rgb_clip, [expr, expr, expr])
 
# 转回YUV空间
output_clip = core.resize.Bicubic(rescaled_clip, format=vs.YUV420P8, matrix_s="709", transfer_s="709", primaries_s="709", range_s="full")

out.set_output()

尝试预览:

final result (out)

成功!

对比滑块

最后,使用命令行将 Vapoursynth 的输出传递给 x264,就可以得到一个仅包含视频流的.h264 文件。

vspipe --y4m Untitled.vpy - | x264 --demuxer y4m --preset slow -o output_video.h264 -

当然,你也可以修改 x264 的参数以提升压缩的视频质量,像这样:

vspipe --y4m Untitled.vpy - | x264 --demuxer y4m --cabac=1 --ref=4 --deblock=1:0:0 --analyse=0x3:0x133 --me=umh --subme=10 --psy=1 --psy_rd=1.00:0.00 --mixed_ref=1 --me_range=24 --chroma_me=1 --trellis=2 --8x8dct=1 --cqm=0 --deadzone=21,11 --fast_pskip=0 --chroma_qp_offset=-2 --threads=9 --lookahead_threads=1 --sliced_threads=0 --nr=0 --decimate=0 --interlaced=0 --bluray_compat=0 --constrained_intra=0 --bframes=8 --b_pyramid=2 --b_adapt=2 --b_bias=0 --direct=3 --weightb=1 --open_gop=0 --weightp=2 --keyint=300 --keyint_min=1 --scenecut=40 --intra_refresh=0 --rc_lookahead=60 --rc=crf --mbtree=1 --crf=17.0 --qcomp=0.70 --qpmin=0 --qpmax=32 --qpstep=4 --ip_ratio=1.40 --aq=3:1.00 -o output_video.h264 -

最后使用 ffmpeg 提取原视频音轨并与新的视频流封装:

ffmpeg -i video.mp4 -vn -acodec copy audio.m4a
ffmpeg -i output_video.h264 -i audio.m4a -c copy output.mp4

鸣谢:Kukoc Ryougi