视频从 448×768 输出到 1080×1920,文件尺寸、帧数和音轨都正确,能否宣布"增强成功"?还不能。分辨率是文件属性;人物轮廓是否自然、远处草木是否被补出假纹理、字幕边缘是否发毛、连播时细节是否跳动,是另一组问题。如果把"拉普拉斯方差更高"或"文件大了六倍"直接当画质分,最后交付的可能只是更锐的错误。
本文用同一条 24 帧/秒、623 帧的竖屏短片做验收。原片为 448×768,逐帧增强成片为 1080×1920。我们另做一份普通双三次插值基线,与增强版在相同的 1080×1920 坐标中比较第 620 帧的三个局部区域,并检查第 116~126 帧短静止段的帧间变化。这里没有拍摄得到的高分辨率真值,也没有完成盲评,结论只能是"发现了哪些需要复核的信号",不能写成"增强版画质提高了多少"。
目录
- 验收对象:文件正确不等于画质正确
- 先建立同画幅的普通插值基线
- [第 620 帧:高频细节增加了什么](#第 620 帧:高频细节增加了什么)
- [第 116~126 帧:怎样检查时间稳定性](#第 116~126 帧:怎样检查时间稳定性)
- 哪些指标能用,哪些结论不能下
- 把人工观看变成可执行的验收记录
- 交付选择与方法边界
一、验收对象:文件正确不等于画质正确
上篇谈的是怎样把低分辨率源片处理成指定尺寸,并保住画幅、帧率和音轨。这篇只讨论处理完以后怎么判断值不值得交付。验收至少有四层:
| 层次 | 能自动检查什么 | 仍需看什么 |
|---|---|---|
| 文件结构 | 宽高、解码帧数、帧率、时长、音轨 | 实际播放和音画同步 |
| 空间细节 | 同一区域的边缘、纹理变化 | 边缘是否自然,纹理是否可信 |
| 时间稳定 | 对齐帧的邻帧差值、异常区间 | 连播是否闪烁、脸和文字是否跳变 |
| 内容正确 | 可标出镜头和时间点 | 人物、动作、字幕是否符合叙事 |
本案例的两份文件均为 623 帧、24 帧/秒,增强版达到 1080×1920,音轨也仍在。它们满足结构对照的前提,却没有回答"同一张脸是否稳定""树枝细节是否真实"。为避免输出尺寸越大就被默认越好,本文先固定比较条件:同一帧号、同一裁切、同一显示尺寸、同一批局部区域。

图1:文件结构、空间细节、时间稳定与内容正确是四个不同的验收问题;后面三项不能由 1080p 属性推导出来。
二、先建立同画幅的普通插值基线
原片宽高比为 448/768≈0.5833,目标为 1080/1920=0.5625。如果把原片直接拉到目标尺寸,人物会被横向压窄。这里按高度等比放大到 1120×1920 ,再左右各裁 20 像素,得到 1080×1920。插值基线和增强版随后拥有同样的坐标系统,局部区域才可直接比较。
python
import cv2
def interpolation_baseline(frame):
if frame.shape[:2] != (768, 448):
raise ValueError("输入必须是本例的 448×768 BGR 帧")
enlarged = cv2.resize(frame, (1120, 1920), interpolation=cv2.INTER_CUBIC)
result = enlarged[:, 20:1100]
assert result.shape[:2] == (1920, 1080)
return result
这份基线使用 OpenCV 的 INTER_CUBIC,目的是给已输出文件建立一个可复核的普通插值对照;它不是原超分处理脚本内部张量插值中间态的逐像素复刻。不同库的双三次插值核、边界处理,以及增强版的再次编码都可能带来差异。因此,后面的差值解释为"这两份可见结果有多不同",不能全归因于模型推理。
下面取两份视频各自的第 620 帧,把原片先按上述规则放大裁切。图中两侧在同一输出尺寸展示,人物和字幕的位置一致;不会因为增强版原始像素更多,就让它在对照图里天然占更大面积。

案例图1:第 620 帧全貌。左侧是普通插值基线,右侧是增强成片;两者同为 1080×1920 坐标,需继续检查局部与连播。
三、第 620 帧:高频细节增加了什么
全貌看构图,局部才看得到边缘处理。第 620 帧选三个固定区域:人物和衣服、远处草木、片尾字幕。区域坐标都基于 1080×1920 的左上角,格式为 (左, 上, 右, 下);同一个框从插值版和增强版取图,不因哪一侧更"好看"而临时改框。
| 区域 | 裁切框 | 插值版拉普拉斯方差 | 增强版拉普拉斯方差 | 应回看什么 |
|---|---|---|---|---|
| 人物与衣服 | (300,960,850,1330) |
109.6 | 188.4 | 发际线、脸部轮廓、袖口是否自然 |
| 远处草木 | (240,420,840,780) |
37.3 | 491.7 | 枝叶是否变成不稳定的细碎纹理 |
| 字幕 | (30,1720,1050,1850) |
435.2 | 3889.8 | 笔画、描边、黑白交界是否发毛 |
拉普拉斯算子对亮度的快速变化敏感。方差从 37.3 到 491.7,说明这一块的高频响应大幅增加,不说明原场景真的有这么多可恢复的草叶细节 。锐化、纹理补造、字幕边缘和压缩噪声都可能抬高它。尤其字幕已经烧录在原片里,本次也跟着画面一起处理;所以字幕区域数值变化很大,并不能误写成"远景细节改善"。OpenCV 的拉普拉斯算子文档说明了它用于检测图像强度的二阶变化,而非直接给出主观画质分。
python
import cv2
import numpy as np
def laplacian_variance(frame, box):
left, top, right, bottom = box
height, width = frame.shape[:2]
if not (0 <= left < right <= width and 0 <= top < bottom <= height):
raise ValueError("裁切框为空或越界")
patch = frame[top:bottom, left:right]
gray = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
return float(cv2.Laplacian(gray, cv2.CV_64F).var())
face_box = (300, 960, 850, 1330)
# baseline 和 enhanced 分别是同一帧、同一尺寸的 BGR 数组
print(round(laplacian_variance(baseline, face_box), 1)) # 109.6
print(round(laplacian_variance(enhanced, face_box), 1)) # 188.4
这段代码提供的是定位优先级。例如远处草木的变化幅度远大于人物区域,就值得先放大看枝叶是不是形成了新的"网纹";字幕变化较大,就要在手机尺寸与原始像素尺寸各看一遍。不能仅凭表中的三个方差大小,把三个区域排出"质量第一、第二、第三"。

案例图2:三个固定区域均从同一帧、同一坐标裁取。人物区域显示了脸和衣服;字幕是单独的检查对象,不能混进背景纹理评价。

图2:高频量是告警线索;数值变大后,应回看具体的脸、枝叶和字幕边缘,而非给整条视频一个"清晰度分"。
四、第 116~126 帧:怎样检查时间稳定性
静帧质量过关仍不等于视频质量过关。逐帧增强的风险在于:同一块草地在第 N 帧被补出一种纹理,第 N+1 帧又被补出另一种;单帧都清楚,正常速度连播却可能跳动。但直接拿高速运动区的邻帧差做"闪烁率"会把人物运动、镜头运动和光照变化算进去。
因此先找一个近静止的对照区。上一轮卡顿定位已确认 v10 成片第 116~126 帧主画面变化很小。对其中 10 个相邻帧对逐一计算灰度平均绝对差(MAE),原片按原尺寸比较;插值基线与增强片按 1080×1920 比较。数值量纲为 8 位灰度的 0~255,不是百分比。
| 第 116~126 帧,10 个邻帧对 | 邻帧灰度 MAE 均值 |
|---|---|
| 448×768 原片 | 0.0093 |
| 1080×1920 普通插值 | 0.0111 |
| 1080×1920 增强成片 | 0.0343 |
增强版在这段的像素变化高于同尺寸插值基线,说明处理和再次编码后的结果并非完全时间不变;但 0.0343 在 0~255 灰度尺度上依然很小,单靠这个数值不能断言观众一定看见闪烁,更不能判定是哪一步造成。正确动作是带着这个区间原速连播,并对脸、草木、字幕分别看。它是待核验信号,不是"不合格"结论。
python
def gray_mae(previous, current):
if previous.shape != current.shape:
raise ValueError("邻帧尺寸不同,先对齐再比较")
a = cv2.cvtColor(previous, cv2.COLOR_BGR2GRAY)
b = cv2.cvtColor(current, cv2.COLOR_BGR2GRAY)
return float(np.mean(cv2.absdiff(a, b)))
# frames 是按帧号取出的 116~126 帧,enhanced_frames 同理。
baseline_frames = {n: interpolation_baseline(frames[n]) for n in range(116, 127)}
baseline_changes = [gray_mae(baseline_frames[n], baseline_frames[n + 1])
for n in range(116, 126)]
enhanced_changes = [gray_mae(enhanced_frames[n], enhanced_frames[n + 1])
for n in range(116, 126)]
print(round(float(np.mean(baseline_changes)), 4)) # 0.0111
print(round(float(np.mean(enhanced_changes)), 4)) # 0.0343
对照第 220→221 帧可看出为何不能混用运动段:普通插值邻帧 MAE 为 2.3308 ,增强版为 2.3899,比静止段高两个数量级,主要反映画面在变化,不宜直接解释为"增强引起闪烁"。若要评估运动中的局部闪烁,通常还要估计运动、对齐相同物体后比较残差,并人工复核遮挡、出画和估计失败。本文没有对这条短片做可靠的运动补偿,不拿运动段差值报"闪烁程度"。

图3:静止段比较能减少物体运动的干扰;增强版邻帧差较高是复核信号,仍受编码噪声影响。
五、哪些指标能用,哪些结论不能下
没有高分辨率真值时,指标的用途要说清楚。PSNR 要把输出与参考视频逐帧比较;把低清原片放大当"参考",得到的是"有多接近插值",不是"有多接近真实高清"。FFmpeg 的 PSNR 滤镜文档也明确要求主输入与参考输入分辨率、像素格式一致并逐帧对应。SSIM 等全参考指标同样不该在缺少可信真值时被包装成真实画质得分。超分辨率评价研究也专门讨论了"缺少高分辨率真值"与主观感知之间的落差,这篇无参考超分质量研究可作为进一步阅读。
本案例实际测了三类量:文件属性确认可比较,固定区域拉普拉斯方差提示空间高频变化,静止段邻帧 MAE 提示时间变化。它们都有用,但每项都有误报来源:
| 信号 | 真正能说明什么 | 不能替代什么 |
|---|---|---|
| 1080×1920、623 帧 | 输出规格和帧数满足约束 | 真实细节、脸部自然程度 |
| 拉普拉斯方差增加 | 某一区域高频响应变强 | 纹理真实性和整体观感 |
| 静止段邻帧差增加 | 该区间像素变化增加 | 观众是否察觉闪烁、变化来源 |
| 固定帧的同图对照 | 精确查看某处边缘与构图 | 一整段的时间稳定性 |
还要控制一个容易漏掉的变量:增强版经历了重新编码。即使把模型关掉,只做插值后重新压成 H.264,像素也可能与内存中的插值基线不同。要把"模型影响"和"编码影响"拆得更细,应再导出一个与增强版相同编码参数的插值 MP4,并复测差值。本文的基线是内存中的 OpenCV 插值帧,没有制作这份编码一致的插值成片,所以只比较可见结果,不做严格的因果归因。
六、把人工观看变成可执行的验收记录
"人工看一下"如果没有采样范围,很容易只盯着一张好看的封面。建议对每个候选版本先固定时间点,再记录区域、现象、证据帧号、是否影响交付。本例可用下面的检查单;其中"待连播"是当前事实状态,不是假装已通过:
| 样本 | 检查对象 | 自动信号 | 人工结论栏 |
|---|---|---|---|
| 第 620 帧 | 人物、衣服 | 高频量 109.6→188.4 | 放大看脸和袖口;待连播 |
| 第 620 帧 | 远处草木 | 高频量 37.3→491.7 | 检查枝叶与网纹;待连播 |
| 第 620 帧 | 已烧录字幕 | 高频量 435.2→3889.8 | 手机尺寸及原尺寸看笔画;待连播 |
| 第 116~126 帧 | 近静止画面 | 邻帧 MAE 0.0111→0.0343 | 原速看是否可见闪烁;待连播 |
| 第 220~221 帧 | 运动片段 | 运动使差值升至约 2.3 | 不把整帧差当闪烁分;待连播 |
不必要求每个复核人写主观长评,但至少用统一条件:原速播放、相同显示设备与画面尺寸、先全貌后局部;出现问题时记录帧号和区域,另一位复核者可定位到同一处。若字幕本来就已烧录,特别要检查它是否被模型处理出白边、黑边或短暂闪动。若字尚未烧录,通常应把字幕合成留到画面增强后,再对字幕单独验收。
分析工具本身也要测边界:原始帧形状不对时不悄悄拉伸,比较的两帧尺寸不同时拒绝,目标框越界或空框不能给出"0 分"。这比只对一条成功样片打印数字可靠。以下断言对应前面的基线与 MAE 核心函数:
python
import numpy as np
black = np.zeros((2, 2, 3), dtype=np.uint8)
gray20 = np.full((2, 2, 3), 20, dtype=np.uint8)
assert gray_mae(black, gray20) == 20.0
blank_source = np.zeros((768, 448, 3), dtype=np.uint8)
assert interpolation_baseline(blank_source).shape == (1920, 1080, 3)
try:
interpolation_baseline(np.zeros((720, 1280, 3), dtype=np.uint8))
except ValueError:
pass
else:
raise AssertionError("错误尺寸必须被拒绝")

图4:固定样本和观看条件,让"看起来更清楚"变成可定位、可复查的具体反馈。
七、交付选择与方法边界
最终选择不是"模型一定比插值高级"。如果平台允许原尺寸、增强版出现可见伪纹理,保留原片是合理结果;如果必须输出 1080×1920,但模型让脸或字幕不稳定,普通插值可能更稳;如果同帧局部和连续回放均能接受,增强版才是候选交付文件。若人物或动作在源片里本来就错了,三种版本都不能靠"变清楚"修好叙事。
本案例现阶段能确认的是:两版 623 帧可逐帧对照;第 620 帧三个区域的高频响应均增加;第 116~126 帧增强版的邻帧变化高于插值基线。它不能 证明新增纹理是真实的,也不能仅凭数字宣布存在可见闪烁或画质已达交付标准。下一步是按上表原速回放并记录接受/拒绝点;如需严格归因,再补一份相同编码条件下的插值版成片。

图5:选择交付版本时同时看输出规格、局部细节和连续播放;发现源片内容错误时应返回上游。
参考资料:OpenCV:拉普拉斯算子、FFmpeg:PSNR 滤镜及参考输入要求、Learning a No-Reference Quality Metric for Single-Image Super-Resolution。