有次客户发来一批视频(300 多个),问:"帮我看看这些画质怎么样,有没有明显有问题的?"
我的第一反应是算 VMAF——然后立刻意识到VMAF 需要参考源(要有"原片"才能比较)。这批视频没有原片,而且它们本来就是"最终成品"。
那就只能人工看了。300 个视频,每个看 30 秒,也要两个半小时,而且人看久了会疲劳,判断标准会漂移。
我去查了资料,发现有一整套"无参考质量评估"(No-Reference / Blind Video Quality Assessment)的方法——不需要原片,直接从画面本身估计质量。这篇写我试过的几种方法、OpenCV 里的实现,以及它们到底有多准。
TL;DR:质量评估分三类:全参考(FR)= VMAF/PSNR/SSIM(最准,需要原片)、半参考(RR)、无参考(NR)= BRISQUE/NIQE(不需要原片,但准确度低得多)。OpenCV 的
cv2.quality模块提供 BRISQUE 和 NIQE(需要opencv-contrib-python+ 模型文件)。此外,针对具体劣化类型,自己写检测更实用:模糊(拉普拉斯方差)、块效应(块边界差分)、噪声(高频能量)、冻结帧(相邻帧差为零)、黑帧。无参考指标跟主观的相关性远不如 VMAF(相关系数大约 0.6~0.75 vs 0.9+),所以它的定位是快速筛查,不是精确判断。
目录
- 一、三类质量评估
- 二、无参考能测什么
- 三、BRISQUE 与 NIQE:原理简介
- 四、OpenCV 里怎么用
- 五、自己实现:针对具体劣化的检测
- 六、从帧级到视频级
- 七、和主观的相关性:别太当真
- 八、实用场景
- 九、实测数据
- 十、坑清单
一、三类质量评估
| 类型 | 需要什么 | 代表指标 | 准确度 | 场景 |
|---|---|---|---|---|
| 全参考(FR) | 原片 + 压缩后 | VMAF、PSNR、SSIM、MS-SSIM | 高(与主观相关性 0.9+) | 编码参数调优、质量回归 |
| 半参考(RR) | 原片的部分特征 | 一些传统指标 | 中 | 传输质量监测(少见) |
| 无参考(NR) | 只要待评估文件 | BRISQUE、NIQE、PIQE | 中低(0.6~0.75) | 入库质检、UGC 筛选、老素材评估 |
关键认知:无参考不是"全参考的替代品",而是"在没有原片时的唯一选择"。它的准确度天然受限——因为它不知道"原图长什么样",只能靠"这张图看起来像不像一张'正常的好图'"来判断。
二、无参考能测什么
无参考方法通常能识别这些具体劣化:
| 劣化 | 典型原因 | 检测方法 |
|---|---|---|
| 模糊 | 对焦失误、过度压缩、缩放 | 拉普拉斯方差、梯度能量 |
| 块效应 | 低码率编码 | 8×8 块边界的差分突变 |
| 噪声 | 高 ISO、传感器 | 高频能量估计 |
| 带状(banding) | 位深不足、渐变压缩 | 平缓区域的梯度突变 |
| 过曝/欠曝 | 曝光问题 | 亮度直方图分布 |
| 色彩偏移 | 白平衡错误 | 色彩统计量(灰度世界假设) |
| 冻结帧/卡顿 | 录制/编码问题 | 相邻帧差为零的持续段 |
| 黑帧 | 转场、录制中断 | 亮度极低 |
| 隔行残留 | 去隔行失败 | 梳齿状检测 |
| 分辨率上采样痕迹 | 低分辨率拉伸 | 频谱分析(较难) |
注意:BRISQUE/NIQE 这类"综合指标"会给出一个总分,但它们对不同类型的劣化敏感度不同(比如 BRISQUE 对模糊和噪声敏感,对色彩偏移不敏感)。所以:
综合指标用来排序和筛出异常,具体诊断要用针对性的检测。
三、BRISQUE 与 NIQE:原理简介
BRISQUE(Blind/Referenceless Image Spatial Quality Evaluator)
思路:自然图像(拍出来的好照片)的统计特性是有规律的——比如相邻像素的归一化亮度(MSCN 系数)服从某种分布(高斯-like)。劣化(压缩、模糊、噪声)会让这个分布"变形"。
BRISQUE 的做法:
- 计算图像的 MSCN 系数(减去局部均值、除以局部标准差);
- 提取这些系数的统计特征(高斯拟合参数 + 相邻系数的相关性);
- 用一个训练好的 SVM 回归模型把这些特征映射成质量分。
输出:0~100,越低越好(0 = 高质量,100 = 很差)。
需要模型文件:因为是监督学习,需要训练好的模型(OpenCV 提供 brisque_model_live.yml 和 brisque_range_live.yml)。
NIQE(Natural Image Quality Evaluator)
思路:完全不需要训练数据(所以叫 "completely blind")。
- 同样计算 MSCN 系数;
- 拟合一个多元高斯模型(MVG)来描述这张图的统计特性;
- 把这个模型跟"从大量高质量自然图像中统计出的先验模型"比较,算距离;
- 距离越大 → 越偏离"好图"的统计特性 → 质量越差。
输出:通常 1~15,越低越好(< 3 好,3~6 中等,> 7 差)。
NIQE 的优势:不需要训练模型(不需要标注数据),跨内容类型更通用。
NIQE 的劣势:它假设"好图"是自然摄影图像——对屏录、动画、医学图像这类非自然内容不适用(会把干净的 PPT 判成"质量差",因为它不符合自然图像的统计规律)。
四、OpenCV 里怎么用
安装(必须有 contrib 模块):
pip uninstall opencv-python -y
pip install opencv-contrib-python
BRISQUE:
import cv2
img = cv2.imread('frame.jpg')
# 需要下载模型文件(opencv_contrib 的 testdata 里有)
model = 'brisque_model_live.yml'
range_ = 'brisque_range_live.yml'
brisque = cv2.quality.QualityBRISQUE_compute(img, model, range_)
print(f'BRISQUE: {brisque[0]:.2f}') # 越低越好
模型文件可以从 opencv_contrib 仓库下载:
https://github.com/opencv/opencv_contrib/tree/master/modules/quality/testdata
NIQE:
niqe = cv2.quality.QualityNIQE_compute(img)
print(f'NIQE: {niqe[0]:.2f}') # 越低越好
验证你的 OpenCV 有没有 quality 模块:
import cv2
print(hasattr(cv2, 'quality')) # True 就说明装的是 contrib 版
踩过的坑:cv2.quality 在不同 OpenCV 版本里的 API 略有差异(有的版本 QualityBRISQUE_compute 需要三个参数,有的只需要图像)。如果报错,查对应版本的文档。
五、自己实现:针对具体劣化的检测
说实话,在实际项目里,针对性的检测比综合指标更好用——因为它们给的是"明确的诊断"("这一帧模糊了"),而不是一个你不知道该怎么解读的分数。
模糊检测
def blur_score(img) -> float:
"""拉普拉斯方差:越大越清晰。"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return float(cv2.Laplacian(gray, cv2.CV_64F).var())
参考值(480px 宽):< 50 明显模糊,> 300 清晰。
块效应检测
块效应是 8×8 编码块的边界出现不连续。检测方法:比较"块边界处"和"块内部"的像素差。
def blocking_score(img) -> float:
"""块效应强度:块边界的差分相对于内部的差分是否异常。"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32)
h, w = gray.shape
h -= h % 8
w -= w % 8
# 水平方向的差分
diff = np.abs(np.diff(gray[:h, :w], axis=1))
# 块内部边界(列 0..6)vs 块边界(列 7, 15, 23...)
inner = diff[:, [i for i in range(w - 1) if (i + 1) % 8 != 0]].mean()
boundary = diff[:, [i for i in range(w - 1) if (i + 1) % 8 == 0]].mean()
if inner < 1e-6:
return 0.0
return float(boundary / inner) # > 1.5 说明块效应明显
原理:正常情况下,相邻列之间的差分应该差不多;如果只在第 8 列的边界处出现大跳变,那就是块效应。
噪声估计
def noise_score(img) -> float:
"""用高通滤波后的能量估计噪声(简化)。"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32)
# 用一个简单的高通(原图 - 模糊图)
blurred = cv2.GaussianBlur(gray, (5, 5), 1.5)
high = gray - blurred
# 平坦区域(低方差)里的高频能量主要是噪声
local_std = cv2.blur(np.abs(high), (9, 9))
return float(local_std.mean())
带状(banding)检测
带状是渐变区域出现的阶梯。检测:找"平缓区域"(低梯度)里的"梯度突变"。这个比较难做好,我的简化版:
def banding_score(img) -> float:
"""渐变区域的二阶差分异常(简化,用于粗筛)。"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32)
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
mag = np.sqrt(gx ** 2 + gy ** 2)
# 带状表现为:大片极低梯度区域里出现规则的梯度线
low = mag < 2.0 # 平坦区域
if low.sum() < 1000:
return 0.0
# 统计平坦区域占比(过高说明可能有带状)
return float(low.mean())
(这个指标很粗糙,只能作为提示。banding 最好的检测方式是人眼看渐变区域,或者用专门的指标。)
冻结帧检测
def detect_frozen(video_path, min_frames=15, diff_thresh=0.5):
"""检测画面冻结(连续多帧几乎无变化)。"""
cap = cv2.VideoCapture(video_path)
prev = None
run = 0
frozen_ranges = []
idx = 0
while True:
ok, frame = cap.read()
if not ok:
break
small = cv2.resize(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), (64, 36))
if prev is not None:
d = np.abs(small.astype(np.float32) - prev.astype(np.float32)).mean()
if d < diff_thresh:
run += 1
else:
if run >= min_frames:
frozen_ranges.append((idx - run, idx, run))
run = 0
prev = small
idx += 1
cap.release()
if run >= min_frames:
frozen_ranges.append((idx - run, idx, run))
return frozen_ranges
六、从帧级到视频级
单个帧的分数要聚合成视频级:
def video_quality_report(video_path, sample_fps=1.0):
"""抽样评估整个视频,返回聚合报告。"""
frames, stamps = sample_frames(video_path, sample_fps=sample_fps)
scores = []
for f in frames:
scores.append({
'blur': blur_score(f),
'blocking': blocking_score(f),
'noise': noise_score(f),
'niqe': float(cv2.quality.QualityNIQE_compute(f)[0]),
})
arr = {k: np.array([s[k] for s in scores]) for k in scores[0]}
return {
'frames': len(frames),
'niqe_mean': arr['niqe'].mean(),
'niqe_p95': np.percentile(arr['niqe'], 95), # 最差的 5%
'blur_mean': arr['blur'].mean(),
'blur_p05': np.percentile(arr['blur'], 5), # 最模糊的 5%
'blocking_mean': arr['blocking'].mean(),
'noise_mean': arr['noise'].mean(),
'frozen': detect_frozen(video_path),
}
聚合的时候要看分位数,不只是均值:
- 均值好但 P95 差 → 有个别很糟的片段(可能是场景切换处、或者某段编码失败);
- 均值差 → 整体有问题。
我的报告里一定同时给"均值"和"最差 5%"。
七、和主观的相关性:别太当真
这是最重要的一节。
我在自己的数据上做过一次相关性分析:让 3 个人对 40 个视频片段打主观分(1~5),然后看各指标和主观分的相关性(Pearson 相关系数):
| 指标 | 与主观的相关系数 |
|---|---|
| VMAF(有参考) | 0.91 |
| NIQE | -0.68(负相关,因为越低越好) |
| BRISQUE | -0.71 |
| 拉普拉斯方差(模糊) | 0.62 |
| 块效应分数 | -0.55 |
结论:
- VMAF 明显更准(0.91)——有原片的时候一定用 VMAF;
- 无参考指标大约 0.6~0.75——能反映趋势,但不能用于精确的优劣判断;
- 针对单一劣化的检测(模糊、块效应)相关性更低,但诊断价值高(它能明确告诉你"这是模糊")。
所以无参考指标的正确用法是:
- 排序:把 300 个视频按分数排序,重点看最差的那 10%;
- 筛异常:设定阈值,把"分数明显偏离群体"的挑出来人工看;
- 不要用于:判断"这个比那个好 0.5 分"(没有意义)、给客户出"质量评分"(会被质疑)。
我给客户的报告写法:
300 个文件中,自动筛查发现 23 个存在明显的质量问题(模糊/块效应/冻结帧),已单独列出。其余文件的自动评分分布如下(分数越低越好),整体处于正常区间。
给的是"有没有问题"和"分布",不是"精确到小数的质量排名"。
八、实用场景
无参考评估在这些场景很有用:
1. 入库质检(最常用)
新入库的素材先跑一遍自动筛查,有明显问题的标记出来。我们前面那篇 ffprobe 质检流水线讲的是"元数据层 + 解码层",无参考评估是第三层(画质层)——三者结合才完整。
2. 老素材评估
一批几十年前的老录像,没有"原片"概念,用无参考评估能快速分出"哪些还有修复价值"。
3. 编码异常监控
批量转码后跑一遍,如果某个文件的分数明显偏离其他文件,说明这次编码出了问题(比如参数错误、源损坏)。
4. UGC 内容筛选
用户上传的内容质量参差,用无参考分数过滤掉明显太差的(比如提示"这个视频分辨率太低/太模糊")。
5. 传输质量监测
边缘节点转码后的内容,用无参考分数做抽查(不需要回传原片比对)。
九、实测数据
对 300 个交付视频的自动筛查结果:
| 问题类型 | 检出数量 | 处理方式 |
|---|---|---|
| 明显模糊(拉普拉斯 < 80) | 9 | 确认是原始素材问题,告知客户 |
| 块效应(> 1.8) | 6 | 重新编码(提高码率) |
| 冻结帧(> 2 秒) | 5 | 源文件问题,标记 |
| 黑帧(> 3 秒) | 3 | 源文件问题,标记 |
| 噪声过高 | 11 | 老素材,不做处理(告知客户) |
| 时长与帧数不符 | 4 | 时间戳问题,重新封装 |
| 合计标记 | 31(10.3%) |
人工复核这 31 个:确认 26 个确实有问题,5 个是误报(主要是"噪声过高"这一项,因为有些内容本来就有颗粒感)。
准确率 84%,误报率 16%——对筛查工具来说可以接受(宁可多标记几个让人看一眼,也不要漏掉真有问题的)。
耗时:300 个视频(平均 20 分钟),1 fps 抽样 + 8 进程并行,总共 26 分钟。相比人工看 100 小时,这个效率提升是决定性的。
十、坑清单
- 以为无参考能替代 VMAF → 准确度差一大截。有原片就用 VMAF。
- 用 BRISQUE/NIQE 评估屏录/动画 → 这些不是"自然图像",指标会误判。
- 只看均值不看分位数 → 漏掉"个别很糟的片段"。看 P95/P05。
- 给客户出精确的质量分数 → 无参考的精度不支持这个。给"有没有问题"和分布。
- 阈值不标定 → 不同内容的分数基准不同。在自己的数据上跑一遍看分布再定阈值。
- 忘了装 opencv-contrib →
cv2.quality不存在。 - BRISQUE 模型文件路径不对 → 报错或者返回奇怪的值。确认模型文件下载正确。
- 对每一帧都算 → 慢。1 fps 抽样足够。
- 不排除转场/黑场帧 → 片头的黑场会被判成"低质量"。跳过片头片尾。
- 噪声检测把颗粒感当质量问题 → 老片/胶片素材会误报。结合内容类型判断。
- 冻结帧检测把静止画面误判 → PPT 类内容本来就不动。按内容类型调阈值。
- 不人工复核 → 自动筛查一定有误差,标记出来的要人看一眼确认。
- 用单一指标下结论 → 综合看多个指标(模糊 + 块效应 + 噪声 + 冻结帧)。
- 忽略音频 → 音频也可能有问题(杂音、爆音、静音)。可以加简单的音频检测。
- 不做基线对比 → 单个文件的绝对分数意义不大,跟同一批的其他文件比才有意义。
最后说说我对无参考质量评估的定位。
它是一个"筛子",不是一个"尺子"。
尺子(VMAF)能告诉你"A 比 B 好 2.3 分",这个数字是可信的、可复现的。筛子(BRISQUE/NIQE + 针对性检测)只能告诉你"这批里这 30 个可能有问题,你看看"——它的价值在于把 300 个视频的筛查工作量压缩到 30 个。
搞清楚这个定位,才不会用错:
- 调优编码参数 → 用尺子(VMAF);
- 检查入库素材有没有明显问题 → 用筛子(无参考);
- 给客户出质量报告 → 用筛子给"有没有问题",用尺子给"具体数值"(如果有的话)。
还有一点体会:针对性检测比综合指标更受欢迎(在团队内部)。因为"BRISQUE = 42.3"这种数字没人知道该怎么办,而"这 9 个文件模糊、这 5 个有冻结帧"是可以直接行动的。
工程上,"能指导行动的输出"比"精确但抽象的输出"更有价值。 这也是为什么我最后交付给客户的是一份"问题清单"(每个问题有类型、时间位置、截图),而不是一个"质量评分表"。