提示

返回博客列表

没有原片怎么判断画质:无参考质量评估(NR-VQA)能做什么

有次客户发来一批视频(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+),所以它的定位是快速筛查,不是精确判断。

目录

一、三类质量评估

类型 需要什么 代表指标 准确度 场景
全参考(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 的做法:

  1. 计算图像的 MSCN 系数(减去局部均值、除以局部标准差);
  2. 提取这些系数的统计特征(高斯拟合参数 + 相邻系数的相关性);
  3. 用一个训练好的 SVM 回归模型把这些特征映射成质量分。

输出:0~100,越低越好(0 = 高质量,100 = 很差)。

需要模型文件:因为是监督学习,需要训练好的模型(OpenCV 提供 brisque_model_live.yml 和 brisque_range_live.yml)。

NIQE(Natural Image Quality Evaluator)

思路:完全不需要训练数据(所以叫 "completely blind")。

  1. 同样计算 MSCN 系数;
  2. 拟合一个多元高斯模型(MVG)来描述这张图的统计特性;
  3. 把这个模型跟"从大量高质量自然图像中统计出的先验模型"比较,算距离;
  4. 距离越大 → 越偏离"好图"的统计特性 → 质量越差。

输出:通常 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——能反映趋势,但不能用于精确的优劣判断;
  • 针对单一劣化的检测(模糊、块效应)相关性更低,但诊断价值高(它能明确告诉你"这是模糊")。

所以无参考指标的正确用法是:

  1. 排序:把 300 个视频按分数排序,重点看最差的那 10%;
  2. 筛异常:设定阈值,把"分数明显偏离群体"的挑出来人工看;
  3. 不要用于:判断"这个比那个好 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 小时,这个效率提升是决定性的。

十、坑清单

  1. 以为无参考能替代 VMAF → 准确度差一大截。有原片就用 VMAF。
  2. 用 BRISQUE/NIQE 评估屏录/动画 → 这些不是"自然图像",指标会误判。
  3. 只看均值不看分位数 → 漏掉"个别很糟的片段"。看 P95/P05。
  4. 给客户出精确的质量分数 → 无参考的精度不支持这个。给"有没有问题"和分布。
  5. 阈值不标定 → 不同内容的分数基准不同。在自己的数据上跑一遍看分布再定阈值。
  6. 忘了装 opencv-contrib → cv2.quality 不存在。
  7. BRISQUE 模型文件路径不对 → 报错或者返回奇怪的值。确认模型文件下载正确。
  8. 对每一帧都算 → 慢。1 fps 抽样足够。
  9. 不排除转场/黑场帧 → 片头的黑场会被判成"低质量"。跳过片头片尾。
  10. 噪声检测把颗粒感当质量问题 → 老片/胶片素材会误报。结合内容类型判断。
  11. 冻结帧检测把静止画面误判 → PPT 类内容本来就不动。按内容类型调阈值。
  12. 不人工复核 → 自动筛查一定有误差,标记出来的要人看一眼确认。
  13. 用单一指标下结论 → 综合看多个指标(模糊 + 块效应 + 噪声 + 冻结帧)。
  14. 忽略音频 → 音频也可能有问题(杂音、爆音、静音)。可以加简单的音频检测。
  15. 不做基线对比 → 单个文件的绝对分数意义不大,跟同一批的其他文件比才有意义。

最后说说我对无参考质量评估的定位。

它是一个"筛子",不是一个"尺子"。

尺子(VMAF)能告诉你"A 比 B 好 2.3 分",这个数字是可信的、可复现的。筛子(BRISQUE/NIQE + 针对性检测)只能告诉你"这批里这 30 个可能有问题,你看看"——它的价值在于把 300 个视频的筛查工作量压缩到 30 个。

搞清楚这个定位,才不会用错:

  • 调优编码参数 → 用尺子(VMAF);
  • 检查入库素材有没有明显问题 → 用筛子(无参考);
  • 给客户出质量报告 → 用筛子给"有没有问题",用尺子给"具体数值"(如果有的话)。

还有一点体会:针对性检测比综合指标更受欢迎(在团队内部)。因为"BRISQUE = 42.3"这种数字没人知道该怎么办,而"这 9 个文件模糊、这 5 个有冻结帧"是可以直接行动的。

工程上,"能指导行动的输出"比"精确但抽象的输出"更有价值。 这也是为什么我最后交付给客户的是一份"问题清单"(每个问题有类型、时间位置、截图),而不是一个"质量评分表"。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部