一段 AI 生成的竖屏短片,剪辑和字幕都完成了,导出时却发现源片只有 448×768。直接把编码尺寸改成 1080×1920,文件确实会变大,画面却不会因此获得真实细节;如果逐帧交给增强模型,又会遇到画幅变化、处理耗时、音轨保留和帧间闪烁。问题不是"怎样输出一个 1080p 文件",而是在可接受的成本下,交付一个尺寸正确、音画完整、没有明显伪细节的视频。
下面以一条 25.97 秒、623 帧、24 帧/秒的竖屏短片为例。输入为 H.264、448×768,含 AAC 音轨;一次逐帧增强后的输出为 H.264、1080×1920,仍为 623 帧、24 帧/秒,并保留 AAC 音轨。本文解释如何选择普通插值与模型增强、为什么要先算画幅、如何组织解码---推理---编码,以及最后该查什么。案例文件已经生成,但没有可追溯的同机复测日志和人工盲评记录,所以不宣称"提速百分之多少"或"画质提高多少分"。
目录
- 先确定要解决的是哪一种低清问题
- 插值、超分模型与重生成该怎么选
- [448×768 到 1080×1920,先处理画幅](#448×768 到 1080×1920,先处理画幅)
- 逐帧处理怎样接上音轨与编码器
- 怎样比较增强结果,避免只看像素数
- 用媒体探测与测试确认交付文件
- 失败边界与上线使用建议
一、先确定要解决的是哪一种低清问题
"不清楚"至少包含三种不同问题。第一种是尺寸不足 :在手机全屏或大屏播放时,448 像素宽的原片必须放大,边缘容易显得软。第二种是源画面本身缺信息 :人物眼睛、文字、衣服纹理在原片里已经模糊,放大只能估计它们,不能还原拍摄现场并不存在的"真值"。第三种是时间维度不稳定:同一张脸相邻帧时而锐利、时而改变纹理;单帧看起来清楚,连播时反而闪烁。
因此,这次处理的目标分两层:文件层要满足尺寸、帧数、帧率、时长和音轨要求;观看层要核对人物、字幕、细线条以及动作片段。文件层能写成自动校验,观看层仍需要同一位置的前后对照和连续回放。把这两层混为一个"输出 1080p 即通过"的指标,最容易把问题带进交付版。
| 已知输入 | 目标或约束 | 为什么重要 |
|---|---|---|
| 448×768、24 帧/秒、623 帧 | 1080×1920,帧率与帧数保持一致 | 避免无意加速、掉帧或重复帧 |
| 原片带 AAC 音轨 | 输出仍有可播放的音轨 | 逐帧图像管道不会自动带上声音 |
| 竖屏画幅约 0.5833 | 目标画幅 0.5625 | 不能无解释地把人物横向压窄 |
| 源文件约 6.58 MB | 输出文件约 40.26 MB | 清晰度、编码码率与存储成本要一并考虑 |

图1:这次处理先锁定帧数、画幅、帧率与音轨,再讨论画质;"像素增加"不是观看层的验收结论。
二、插值、超分模型与重生成该怎么选
如果只是需要一个指定尺寸的展示文件,双三次或 Lanczos 插值通常是第一条基线:快、行为可预测,也不凭空构造强纹理;缺点是原片模糊的脸和远景不会真正恢复。如果源片还有可辨认的边缘,但放大后明显发软,可以试超分辨率模型,再与普通插值做同帧对照。若原视频已经出现双人、错手、文字乱码或动作跳变,增强会把这些错误放大得更清楚;此时该回到生成或剪辑阶段,而不是继续调"锐化强度"。
| 处理方式 | 适用前提 | 常见代价或风险 | 这次怎么用 |
|---|---|---|---|
| 普通插值 | 主要缺显示尺寸,源片内容本身可接受 | 细节仍软,但不易产生模型式伪纹理 | 作为对照基线 |
| 图像超分模型逐帧处理 | 原片有一定结构信息,可接受较高处理成本 | 脸、草叶和字幕可能出现伪细节;跨帧可能抖动 | 对模型输出做尺寸调整和强度混合 |
| 视频时序增强模型 | 闪烁是主要问题,模型与硬件条件合适 | 时序窗口、显存、速度与边界帧处理更复杂 | 本次未使用,不能借其能力评价结果 |
| 返回源头重生成 | 主体、动作、文字本身已错 | 需要重新筛选镜头并回归时间线 | 画面错误时优先考虑 |
本案例选的是"逐帧模型输出 + 普通插值基线按强度混合"。它的价值是能在增强过头时降低模型影响,而不是保证结果一定自然。不同超分模型的训练数据和适用画面不同;动画、真人、文字密集画面不要共用一次主观判断。公开示例只解释管线,不把某个模型权重当成通用答案。
解码、模型推理、编码也是三段不同的耗时。即使推理跑在 GPU 上,读入原片、CPU/GPU 数据往返和 H.264 编码都还存在;看到 GPU 利用率不满,不能直接断定"模型没有加速"。先分别记录每段耗时、内存和磁盘吞吐,再决定优化批量、编码器还是解码策略。

图2:音轨来自原片的独立映射;逐帧图像流只负责视频,不会自动保存声音。
三、448×768 到 1080×1920,先处理画幅
这个尺寸变化藏着一个容易忽略的比例问题:448 / 768 ≈ 0.5833,而 1080 / 1920 = 0.5625。若直接把原帧拉成 1080×1920,人物会略微横向压缩。按高度等比放大到 1920 时,宽度应为 448 × (1920 / 768) = 1120。本案例的做法是先得到 1120×1920,再从左右各裁去 20 像素,形成 1080×1920。
这比直接拉伸更忠于人物比例,但裁切不等于零损失:画面两侧各有约 1.8% 的放大后宽度被移除。如果人物、字幕、产品或界面元素恰好贴边,应该改为留边、调整构图,或者接受另一种目标尺寸。横竖比不能只在处理脚本里写死;文章发布前要用边缘主体的画面再检查一次。
python
source_w, source_h = 448, 768
target_w, target_h = 1080, 1920
scaled_w = round(source_w * target_h / source_h) # 1120
crop_each_side = (scaled_w - target_w) // 2 # 20
assert scaled_w - 2 * crop_each_side == target_w
模型可能先输出更高分辨率,随后再用双三次插值调整到 1120×1920;普通插值基线也调整到同样尺寸,才能逐像素混合。不能把尺寸不同的两张图直接加权,或者先裁一张、后裁另一张。下面是本案例中决定输出画幅与增强强度的核心计算,model 为已加载且处于推理模式的超分模型,tensor 是形状为 [1, 3, 768, 448]、值域为 0~1 的 RGB 图像:
python
import torch
import torch.nn.functional as F
with torch.inference_mode():
enhanced = model(tensor)
enhanced = F.interpolate(
enhanced, size=(1920, 1120), mode="bicubic", align_corners=False
)[:, :, :, 20:1100]
baseline = F.interpolate(
tensor, size=(1920, 1120), mode="bicubic", align_corners=False
)[:, :, :, 20:1100]
strength = 0.7
output = torch.lerp(baseline, enhanced, strength).clamp(0, 1)
assert output.shape == (1, 3, 1920, 1080)
strength=0 是普通插值,strength=1 是当前模型输出,0.7 只是这次处理脚本采用的参数,不是通用最优值。应把同一帧在不同强度下并排比较,再连播人物移动和细密背景;尤其不要因为一张静帧更锐就无条件开到 1。

图3:按高度等比放大得到 1120×1920,再左右各裁 20 像素;主体靠边时应重新评估裁切方案。
四、逐帧处理怎样接上音轨与编码器
完整管线不是"打开图片,调用一次模型"那么简单。解码器把视频解成连续 rgb24 原始帧;Python 每次按 宽×高×3 字节读满一帧,转换为张量进行推理;编码器从标准输入读回处理后的 rgb24 帧。同时,它从原文件读取音频,将视频帧与音轨封装到新 MP4。原项目使用 FFmpeg 解码/编码、PyTorch 推理和 Spandrel 加载模型;下面只保留决定正确性的关键点:
python
frame_size = 448 * 768 * 3
count = 0
while True:
raw = read_exactly(decoder.stdout, frame_size)
if not raw:
break
if len(raw) != frame_size:
raise RuntimeError("输入在一帧中间结束,不能交付半帧视频")
tensor = rgb_bytes_to_tensor(raw) # [1, 3, 768, 448], 0~1
result = enhance_and_crop(tensor) # [1, 3, 1920, 1080]
encoder.stdin.write(tensor_to_rgb_bytes(result))
count += 1
encoder.stdin.close()
if encoder.wait() != 0 or count == 0:
raise RuntimeError("编码失败或没有产生视频帧")
这里的 read_exactly、张量转换和 enhance_and_crop 需要由实际程序实现;代码块刻意突出三件事:不能把管道一次 read 返回不足一帧误判为视频结束;不能在编码器失败时仍把文件标成完成;不能只看处理循环计数而不看最终封装文件。完整工程还应捕获解码器退出码、保存标准错误日志、清理失败的临时输出,并在验收通过后才将临时文件改为正式文件。
在 FFmpeg 侧,原项目将处理后的原始帧作为输入 0,再把源 MP4 作为输入 1,使用 -map 0:v:0 -map 1:a:0 -c:a copy 明确选择处理后视频和原音轨。FFmpeg 官方文档说明 -map 控制输出流选择;不显式映射时不能假设"原片有音轨,所以输出也有"。该命令要求输入含音轨;若产品还接受静音视频,应先探测音频流,再选择有声和无声两条封装路径,不能让 1:a:0 在无音频时直接报错。
另外,本案例原片是固定 24 帧/秒;原始帧管道本身不带每帧时间戳,编码器按 -r 24 重建时间轴。可变帧率视频不能直接照搬,否则可能改变节奏和音画关系。遇到手机拍摄的 VFR 素材,应先决定是否规范化为 CFR,并记录丢帧、补帧策略,或采用可保留时间戳的处理管线。
五、怎样比较增强结果,避免只看像素数
本案例的第 620 帧适合观察人物、树桩、衣服褶皱和枯草。先用同一帧号抽取原片和增强片,再以接近的显示尺寸观看;否则右侧 1080p 文件天然显示得更大,比较会失真。全貌图检查构图与字幕安全区,局部图检查面部、袖口、草叶和物体边缘。下面两张案例图来自同一帧,不是两次生成的不同画面。

案例图1:第 620 帧全貌对照。增强版像素尺寸更大,但交付前仍要检查边缘裁切和字幕位置。
同一张图的局部区域放到近似相同的屏幕占用面积后,才看得到"视觉更清楚"和"纹理是否可靠"之间的区别。脸部和衣服边缘可以更明确,但模型也可能把泥土、草叶的随机纹理修饰成不存在的细节。没有原始高分辨率真值图时,不能把看起来更锐等同于恢复了真实细节;PSNR、SSIM 这类有参考指标也缺少可靠的配对真值。此时更适合做固定区域的人工对照,记录具体接受或拒绝的理由。

案例图2:人物与树桩局部同帧、近似同屏尺寸对照;单帧不能证明相邻帧没有纹理闪烁。
实际复核至少选三类片段:静止人物看五官和衣物,运动人物看脸与手是否一帧一变,细纹理背景看草叶、瓦片和树枝是否闪烁。字幕若已经烧进原片,也会跟着进增强模型,必须看笔画是否发毛;如果字幕仍是独立轨道,通常应先增强画面、后合成字幕,以免字幕被模型"修复"。这不是对所有模型的绝对规定,而是由字幕是否已烧录决定的处理顺序。

图4:原片、普通插值和模型增强应在同帧、同区域下比较;模型结果还要连播检查时间稳定性。
六、用媒体探测与测试确认交付文件
人工看画面之前,先让程序检查可量化事实。使用 ffprobe -count_frames 读取实际解码帧数,而不只信容器里可能缺失或不准确的声明值;提取视频宽高、平均帧率、音频编码和时长。下面是这个案例的实际探测结果,字节数是文件大小,不是"画质得分":
| 字段 | 输入 | 输出 | 结论 |
|---|---|---|---|
| 画面尺寸 | 448×768 | 1080×1920 | 达到目标尺寸 |
| 解码帧数 | 623 | 623 | 未见帧数变化 |
| 平均帧率 | 24/1 | 24/1 | 保持固定帧率 |
| 音频编码 | AAC | AAC | 音轨存在、编码类型一致 |
| 容器时长 | 25.966 秒 | 25.958 秒 | 相差约 0.008 秒,小于一帧 |
| 文件大小 | 6,577,236 字节 | 40,262,752 字节 | 约增至 6.12 倍 |
"音轨存在"还不等于"人听起来同步"。结构校验通过后,至少试听开头、中间转场和结尾;字幕与旁白要按同一时间码复核。画面也要完整解码到最后一帧,不能只看播放器能打开首屏。
读者可以先分别对原片和增强片执行下面的命令。-count_frames 会遍历视频流,长视频上需要时间;验收阶段值得做,任务进度页却不应每刷新一次就重跑。把两份 JSON 中的帧数、帧率、宽高、音频流及容器时长代入后面的断言即可:
bash
ffprobe -v error -count_frames \
-show_entries stream=codec_type,codec_name,width,height,avg_frame_rate,nb_read_frames \
-show_entries format=duration,size -of json input.mp4
为了避免验收逻辑只覆盖成功路径,测试至少包含:输出少一帧、音轨缺失、尺寸错误三个拒绝案例。以下是结构校验核心,假设 source、output 已从探测结果整理为字典:
python
def audit(source, output):
checks = {
"target_size": (output["width"], output["height"]) == (1080, 1920),
"frame_count": output["frames"] == source["frames"],
"frame_rate": output["fps"] == source["fps"],
"audio_codec": output["audio"] == source["audio"],
"duration_delta_under_frame":
abs(output["duration_s"] - source["duration_s"]) <= 1 / 24,
}
return {"checks": checks, "pass": all(checks.values())}
拒绝路径同样应运行。例如原片和输出均为 623 帧、均有 aac 音轨时通过;把输出改为 622 帧或清空音频流,即应失败:
python
source = {"width": 448, "height": 768, "fps": "24/1", "frames": 623,
"audio": ["aac"], "duration_s": 25.966}
output = {"width": 1080, "height": 1920, "fps": "24/1", "frames": 623,
"audio": ["aac"], "duration_s": 25.958}
assert audit(source, output)["pass"]
assert not audit(source, output | {"frames": 622})["pass"]
assert not audit(source, output | {"audio": []})["pass"]
assert not audit(source, output | {"width": 1120})["pass"]
这段断言的时长容差以本案例 24 帧/秒为前提;通用脚本应从输入帧率计算一帧的时长。它能发现漏帧、丢音轨和尺寸不符,却不能发现"脸变假""动作卡顿"。结构正确是观看复核的起点,不是替代品。

图5:本次文件结构检查通过;主观细节、时间闪烁与音画同步仍要按片段人工复核。
七、失败边界与上线使用建议
第一,不要把输出尺寸当质量指标 。源片有严重压缩块、错误人脸、重影或错误动作时,超分只能制造更大的错误。第二,不要只估算 GPU 推理时间 。长视频的总耗时还包括解码、数据搬运、编码与文件写入;这次有成片文件,但缺少同环境可追溯的逐段计时,因此不把某个历史运行时间当作公开性能结论。第三,要给输出设置存储和失败处理边界。本例文件体积从约 6.58 MB 到 40.26 MB;批量处理时应限制并发、预留磁盘空间,失败时保留原片并清理未验收的临时文件。
真正可复用的工作顺序是:先核对源片问题属于尺寸、细节还是内容错误;为普通插值建立基线;计算画幅后再跑模型;同帧对照并连播;最后检查帧数、时长、音轨和文件大小。只有当结构检查和观看复核都过关,才把增强版作为交付文件。这个流程适用于短剧,也适用于低分辨率演示视频、历史素材和竖屏产品视频;不同素材要重新选模型、强度与裁切方式,不能直接继承本例参数。
参考资料:FFmpeg 文档:流选择与映射、PyTorch:推理模式、PyTorch:张量插值、Spandrel:模型加载。