同样是 1080P,哪个画质更好?用 VMAF、PSNR、SSIM 给视频打分
你手上有两个版本的同一个视频——一个从 B 站下载的 H.264 版本,一个从 YouTube 下载的 VP9 版本。文件名不同、码率不同、编码不同。肉眼对比看了半天,感觉差不多,但又隐隐觉得哪个更清晰一点。"画质好坏"这件事,有没有客观的量化标准?答案是有的:PSNR、SSIM 和 VMAF 是视频行业里最常用的三个客观质量评估指标。这篇文章讲清楚它们各自算什么、怎么算、哪个最接近人眼感受,以及怎么用 FFmpeg 一行命令跑出 VMAF 分数。
TL;DR:PSNR 算像素误差(最快但最不准),SSIM 算结构相似度(比 PSNR 靠谱),VMAF 是 Netflix 开源的机器学习模型(最接近人眼感受,但也最慢)。日常使用推荐 VMAF——用 FFmpeg 的 libvmaf 滤镜,一行命令就能对两个视频做质量对比。VMAF 分数 93 分以上基本看不出区别,75 分以下画质明显劣化。
目录
- 一、人眼为什么不是可靠的评判标准
- 二、PSNR:最古老也最简单的指标
- 三、SSIM:从像素到结构
- 四、VMAF:Netflix 的机器学习方案
- 五、实战:用 FFmpeg 跑 VMAF
- 六、三个指标的对比与选择
- 七、批量对比不同编码参数
- 八、合规与温馨提示
一、人眼为什么不是可靠的评判标准
用人眼看画质有几个问题:
第一,累。 对比两个视频需要反复切换、暂停、放大,看个十分钟眼睛就酸了。对比十组编码参数更是灾难。
第二,不客观。 你知道哪个是"原片"哪个是"压缩版"之后,会不自觉地给压缩版挑毛病。这叫"确认偏误"。双盲测试当然可以避免这个问题,但操作太复杂。
第三,分辨率骗人。 一个 4K 视频如果码率极低,分辨率再高也是满屏马赛克。但很多人看到"4K"标签就默认画质好。同样,H.265 在低码率下的表现远超 H.264,但光看码率数值你无法量化这个差距。
第四,不同内容的敏感度不同。 动画片在低码率下观感依然不错,但体育比赛的草地和观众席在低码率下惨不忍睹。同一组编码参数对不同类型内容的"伤害"是不一样的。
所以需要客观指标。
二、PSNR:最古老也最简单的指标
2.1 PSNR 算什么
PSNR(Peak Signal-to-Noise Ratio,峰值信噪比)的思路非常直接:逐像素对比原始视频和压缩视频,算出每个像素的误差,然后取对数。
PSNR = 10 × log₁₀(MAX² / MSE)
其中:
MAX = 像素最大值(8bit 视频 = 255)
MSE = 所有像素误差的平方的平均值
翻译成人话:PSNR 衡量的是"压缩后的画面和原片差了多少"。数值越高越好。
| PSNR 值 | 含义 |
|---|---|
| > 45 dB | 几乎看不出区别 |
| 35-45 dB | 轻微劣化,仔细看能发现 |
| 30-35 dB | 明显劣化,但不影响观看 |
| 25-30 dB | 较差,有明显的压缩痕迹 |
| < 25 dB | 很差,严重影响观看 |
2.2 PSNR 的致命缺陷
PSNR 的问题在于:它对所有像素一视同仁。但人眼不是这样工作的。
场景 A:画面整体变暗了 2%(人眼几乎察觉不到)
→ PSNR 可能只有 30 dB(看起来很差)
场景 B:画面中有一个小块出现了严重的色块(人眼一眼就能看到)
→ PSNR 可能有 40 dB(看起来还不错)
→ PSNR 的分数和人眼的实际感受经常不一致
另一个问题:PSNR 对平移、旋转、缩放极度敏感。两个画面如果差了一个像素的对齐,PSNR 会暴跌——但人眼完全看不出这一个像素的偏移。
2.3 用 FFmpeg 算 PSNR
ffmpeg -i original.mp4 -i compressed.mp4 \
-filter_complex "psnr" \
-f null /dev/null 2>&1 | grep PSNR
输出:
[Parsed_psnr_0 @ 0x...] PSNR y:38.24 u:40.15 v:39.87 average:38.67 min:35.12 max:42.33
y:亮度通道的 PSNRu/v:色度通道的 PSNRaverage:三个通道的加权平均
三、SSIM:从像素到结构
3.1 SSIM 的思路
SSIM(Structural Similarity Index,结构相似性)的设计者意识到:人眼对"结构信息"比"像素误差"敏感得多。一张照片亮度整体变暗,人眼不太在意;但边缘变得模糊,人眼立刻能发现。
SSIM 从三个维度评估画面:
SSIM(x, y) = [l(x,y)]ᵅ × [c(x,y)]ᵝ × [s(x,y)]ᵞ
l(x,y):亮度相似度(Luminance)
c(x,y):对比度相似度(Contrast)
s(x,y):结构相似度(Structure)
输出值在 -1 到 1 之间,1 表示完全一致。实际使用中通常乘以 100 来呈现。
| SSIM 值 | 含义 |
|---|---|
| > 0.98 | 几乎看不出区别 |
| 0.95-0.98 | 轻微劣化 |
| 0.90-0.95 | 明显劣化 |
| 0.80-0.90 | 较差 |
| < 0.80 | 很差 |
3.2 SSIM 比 PSNR 好在哪
一个经典的例子:
原图 → 高斯模糊处理 → PSNR: 28 dB, SSIM: 0.85
原图 → JPEG 压缩 → PSNR: 28 dB, SSIM: 0.72
两个处理的 PSNR 一样(28 dB),但:
- 高斯模糊后人眼看不出太大区别 → SSIM 0.85,相对准确
- JPEG 压缩后色块明显 → SSIM 0.72,更接近人眼感受
3.3 用 FFmpeg 算 SSIM
ffmpeg -i original.mp4 -i compressed.mp4 \
-filter_complex "ssim" \
-f null /dev/null 2>&1 | grep SSIM
四、VMAF:Netflix 的机器学习方案
4.1 VMAF 的诞生背景
Netflix 在 2016 年开源了 VMAF(Video Multi-method Assessment Fusion)。他们的痛点很具体:Netflix 有数千种不同的编码配置组合(分辨率 × 码率 × 编码器 × preset),需要自动判断哪种配置在给定的带宽下画质最好。
PSNR 和 SSIM 都不够准,人眼测试又太慢。所以他们训练了一个机器学习模型。
4.2 VMAF 怎么算
输入:原始视频 + 压缩后视频
步骤 1:逐帧对齐两个视频
步骤 2:对每帧计算 6 个基础指标:
- VIF(视觉信息保真度):信息论指标,衡量压缩丢失了多少"有用信息"
- DLM(细节损失度量):检测边缘模糊和细节丢失
- 时域运动特征:画面中的运动量
- 还有 3 个辅助特征
步骤 3:把 6 个特征输入训练好的 SVM 回归模型
步骤 4:输出 0-100 的分数
VMAF 的核心创新:不只看单帧,还看时域信息——画面中的运动量。同一组编码参数,对静态访谈视频和对高速运动的体育比赛,画质影响完全不同。VMAF 会根据运动量自动调整评分权重。
4.3 VMAF 分数解读
| VMAF 分数 | 含义 | 使用建议 |
|---|---|---|
| 93-100 | 与原始几乎无差异 | 浪费码率,可以再压一点 |
| 85-93 | 轻微差异,需仔细对比才能发现 | 推荐的平衡区间 |
| 75-85 | 可察觉的差异,但观感可接受 | 适合移动端观看 |
| 60-75 | 明显劣化 | 只适合预览/缩略图 |
| < 60 | 很差 | 不建议使用 |
4.4 VMAF 的一个反直觉结果
同一视频、同一码率下,不同编码器的 VMAF 分数:
libx264 (H.264), CRF 23 → VMAF: 87.3
libx265 (H.265), CRF 28 → VMAF: 87.1
H.265 用更高的 CRF 值(更低码率)就达到了和 H.264 几乎一样的 VMAF。
这意味着 H.265 在相同画质下可以节省大约 30-40% 的码率。
这种量化对比,用人眼几乎不可能做得这么精确。
五、实战:用 FFmpeg 跑 VMAF
5.1 环境准备
# 检查 FFmpeg 是否编译了 libvmaf
ffmpeg -filters 2>/dev/null | grep libvmaf
# 如果没有,需要重新编译 FFmpeg 或下载预编译版本
# Windows 用户可以从 https://www.gyan.dev/ffmpeg/builds/ 下载 full 版本
5.2 基础用法
# 对比两个视频的 VMAF 分数
ffmpeg -i original.mp4 -i compressed.mp4 \
-filter_complex "libvmaf" \
-f null /dev/null
输出:
VMAF score: 89.427301
5.3 完整参数
ffmpeg -i original.mp4 -i compressed.mp4 \
-filter_complex \
"libvmaf=model=version=vmaf_v0.6.1:\
log_path=vmaf_result.json:\
log_fmt=json:\
n_threads=8:\
feature=name=psnr:\
feature=name=float_ssim" \
-f null /dev/null
参数说明:
| 参数 | 含义 |
|---|---|
model |
VMAF 模型版本,默认 vmaf_v0.6.1 |
log_path |
结果输出文件 |
log_fmt |
输出格式(json/xml/csv) |
n_threads |
并行计算线程数 |
feature |
额外输出的指标(psnr/ssim 等) |
5.4 结果 JSON 解读
{
"frames": [
{
"frameNum": 0,
"metrics": {
"vmaf": 89.42,
"psnr_y": 38.24,
"float_ssim": 0.9621
}
},
{
"frameNum": 1,
"metrics": {
"vmaf": 91.15,
"psnr_y": 39.10,
"float_ssim": 0.9715
}
}
],
"aggregate": {
"VMAF_score": 90.28,
"min": 82.10,
"harmonic_mean": 89.85
}
}
注意 harmonic_mean(调和平均数)——VMAF 的最终分数用的是调和平均,不是算术平均。这是刻意设计的:调和平均对低分帧更敏感,如果视频中有几帧画质很差,算术平均可能被高分帧拉高,但调和平均会把低分帧的"惩罚"体现出来。
5.5 不同分辨率视频的对齐
VMAF 要求两个视频的分辨率一致。如果不一样,需要先缩放:
# 把压缩版缩放到和原片一样
ffmpeg -i original.mp4 -i compressed.mp4 \
-filter_complex "[1:v]scale=1920:1080:flags=bicubic[scaled];\
[0:v][scaled]libvmaf" \
-f null /dev/null
5.6 只对比特定片段
完整对比 2 小时的电影太耗时,通常只截取有代表性的片段:
# 只对比第 60 秒到第 120 秒
ffmpeg -ss 60 -t 60 -i original.mp4 \
-ss 60 -t 60 -i compressed.mp4 \
-filter_complex "libvmaf" \
-f null /dev/null
六、三个指标的对比与选择
6.1 一图对比
PSNR SSIM VMAF
计算原理 像素误差 结构相似 机器学习融合
与人眼相关性 ★☆☆☆☆ ★★★☆☆ ★★★★★
计算速度 ★★★★★ ★★★★☆ ★★☆☆☆
是否需要原片 是 是 是
适用场景 快速初筛 中等精度 最终决策
6.2 实际使用建议
日常开发/调试 → PSNR(够快,能快速发现问题)
编码参数对比 → SSIM + VMAF 双重验证
最终画质决策 → VMAF(最接近人眼)
论文/报告 → 三个指标都跑一遍,附 VMAF 帧级曲线图
6.3 一个常见误区
很多人认为 VMAF 越高越好,追求 95 分以上。实际上 VMAF 93 分以上人眼已经无法分辨差异,继续提高码率是浪费。Netflix 自己的推荐是:
1080P: VMAF 85-93
4K: VMAF 90-95
移动端: VMAF 75-85
七、批量对比不同编码参数
7.1 Python 自动化脚本
import subprocess
import json
import os
from pathlib import Path
def compute_vmaf(original, compressed, output_json=None):
"""计算两个视频的 VMAF 分数"""
if output_json is None:
output_json = 'vmaf_result.json'
cmd = [
'ffmpeg',
'-i', original,
'-i', compressed,
'-filter_complex',
'libvmaf=log_path={}:log_fmt=json:n_threads=4'.format(output_json),
'-f', 'null', '/dev/null' if os.name != 'nt' else 'NUL',
'-y'
]
result = subprocess.run(cmd, capture_output=True, text=True)
# 解析 JSON 结果
with open(output_json, 'r') as f:
data = json.load(f)
aggregate = data.get('aggregate', {})
return {
'vmaf': aggregate.get('VMAF_score'),
'harmonic_mean': aggregate.get('harmonic_mean'),
'min': aggregate.get('min'),
}
def benchmark_encoding_params(original, output_dir, presets=None):
"""对比不同编码参数下的画质"""
if presets is None:
presets = ['ultrafast', 'superfast', 'veryfast', 'faster',
'fast', 'medium', 'slow', 'slower', 'veryslow']
results = []
for preset in presets:
compressed = os.path.join(output_dir, f'encoded_{preset}.mp4')
# 编码
print(f"编码: preset={preset}...")
subprocess.run([
'ffmpeg', '-i', original,
'-c:v', 'libx264', '-preset', preset, '-crf', '23',
'-c:a', 'aac', '-b:a', '128k',
compressed, '-y'
], capture_output=True)
# 获取文件大小
size = os.path.getsize(compressed)
# 计算 VMAF
print(f"计算 VMAF: preset={preset}...")
vmaf_result = compute_vmaf(original, compressed)
results.append({
'preset': preset,
'vmaf': vmaf_result['vmaf'],
'harmonic_mean': vmaf_result['harmonic_mean'],
'size_mb': size / 1024 / 1024,
'ratio': vmaf_result['vmaf'] / (size / 1024 / 1024) if size else 0,
})
print(f" VMAF: {vmaf_result['vmaf']:.2f}, "
f"文件大小: {size/1024/1024:.1f} MB")
# 排序:按"画质/体积比"排序
results.sort(key=lambda r: r['ratio'], reverse=True)
print("\n=== 结果汇总(按性价比排序)===")
print(f"{'Preset':<12} {'VMAF':<8} {'体积(MB)':<10} {'性价比':<8}")
print("-" * 40)
for r in results:
print(f"{r['preset']:<12} {r['vmaf']:<8.2f} "
f"{r['size_mb']:<10.1f} {r['ratio']:<8.2f}")
return results
def plot_vmaf_per_frame(vmaf_json_path, output_image='vmaf_curve.png'):
"""绘制 VMAF 逐帧曲线图"""
try:
import matplotlib.pyplot as plt
except ImportError:
print("请安装 matplotlib: pip install matplotlib")
return
with open(vmaf_json_path, 'r') as f:
data = json.load(f)
frames = [f['frameNum'] for f in data['frames']]
vmaf_scores = [f['metrics']['vmaf'] for f in data['frames']]
plt.figure(figsize=(12, 4))
plt.plot(frames, vmaf_scores, linewidth=0.5)
plt.axhline(y=data['aggregate']['VMAF_score'],
color='r', linestyle='--',
label=f"平均 VMAF: {data['aggregate']['VMAF_score']:.1f}")
plt.xlabel('帧序号')
plt.ylabel('VMAF 分数')
plt.title('VMAF 逐帧曲线')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(output_image, dpi=150)
print(f"曲线图已保存: {output_image}")
if __name__ == '__main__':
# 对比不同 preset 的画质
results = benchmark_encoding_params(
'original.mp4',
'encoded_output/',
presets=['fast', 'medium', 'slow']
)
7.2 批量对比 CRF 值
def benchmark_crf_values(original, output_dir, crf_values=None):
"""对比不同 CRF 值下的画质"""
if crf_values is None:
crf_values = [18, 20, 22, 23, 25, 28, 30, 35]
results = []
for crf in crf_values:
compressed = os.path.join(output_dir, f'crf_{crf}.mp4')
subprocess.run([
'ffmpeg', '-i', original,
'-c:v', 'libx264', '-preset', 'medium', '-crf', str(crf),
compressed, '-y'
], capture_output=True)
size = os.path.getsize(compressed)
vmaf = compute_vmaf(original, compressed)
results.append({
'crf': crf,
'vmaf': vmaf['vmaf'],
'size_mb': size / 1024 / 1024,
})
print(f"CRF={crf}: VMAF={vmaf['vmaf']:.1f}, "
f"体积={size/1024/1024:.1f}MB")
return results
八、合规与温馨提示
- VMAF 是 Netflix 开源的合法工具,可以自由使用
- 本文讨论的画质评估方法适用于你自己制作的视频内容或已获得授权的素材
- 用 VMAF 对比不同来源的视频(比如下载的版本和官方版本)可能涉及版权问题,仅限个人学习研究
画质评估这件事,我个人的经验是:日常用 SSIM 快速验证,做最终决策用 VMAF。 PSNR 虽然粗糙,但在 CI/CD 流水线里做快速回归测试还是有用的——如果某次代码改动让 PSNR 突然跌了 5 dB,那肯定有问题。三个指标不是互相替代的关系,而是各有各的用武之地。
本文由 VidDown 技术博客原创发布。VidDown 在视频转码功能中内置了 VMAF 画质评估——你可以在转码后直观看到画质分数,帮助你在文件体积和画质之间找到最佳平衡点。访问 VidDown 了解更多。