有次要处理一批老素材:480p 的 DV 录像,客户要求出 1080p 版本。
我用了最直白的命令:
ffmpeg -i old.avi -vf scale=1920:1080 -c:v libx264 -crf 20 out.mp4。出来的画面……确实变大了,但糊得像隔着一层雾。客户不满意,我自己看也不满意。然后我做了两件事:换缩放算法(默认 bilinear → lanczos),以及处理色度上采样。改完之后同样的源,主观清晰度提升明显。
这篇写这次学到的东西:缩放算法有哪些、各自适合什么、4:2:0 色度到底意味着什么、为什么放大到 1080p 也不可能"真的变清晰",以及怎么用客观指标验证。
TL;DR:
-vf scale=W:H:flags=<算法>,缩小用area或bicubic,放大用lanczos或spline,实时用bilinear。三个容易忽略的点:先确认 SAR(有些 1440×1080 的视频显示时就是 1920×1080,不该按像素尺寸算);4:2:0 的色度分辨率只有亮度的一半,缩放时色度要用足够好的算法(加full_chroma_int修正半像素偏移);放大不能凭空增加细节(要真提升画质只能用 AI 超分,那是另一回事)。追求质量用zscale(需要 libzimg),它支持分别指定亮度和色度算法,还能做正确的色彩空间转换。
目录
- 一、两类缩放,问题完全不同
- 二、先搞清楚这个视频"显示时多大"(SAR/DAR)
- 三、ffmpeg 的缩放算法清单
- 四、4:2:0 意味着什么:色度只有一半分辨率
- 五、色度半像素偏移与 full_chroma_int
- 六、zscale:更专业的选择
- 七、缩放前后该做什么处理
- 八、尺寸对齐与"非标准"分辨率
- 九、实测对比
- 十、放大到 1080p 的现实预期
- 十一、坑清单
一、两类缩放,问题完全不同
降采样(缩小):1080p → 720p。信息是"多余"的,问题在于怎么丢弃。
- 丢得不好 → 摩尔纹、锯齿、细节消失;
- 核心是抗混叠(抗锯齿):缩小前要先把高频滤掉,否则采样会产生伪影。
升采样(放大):480p → 1080p。信息是"不足"的,问题在于怎么插值。
- 插值不好 → 模糊(bilinear)或者振铃/锯齿(过度锐化);
- 放大不可能恢复已经丢失的细节,只能让已有的细节看起来更平滑自然。
这两类要用不同的算法,用错了质量差别很大。
二、先搞清楚这个视频"显示时多大"(SAR/DAR)
这是我踩的第一个坑:像素尺寸不等于显示尺寸。
很多老视频(DV、DVD、某些电视源)用的是非方形像素:
1440×1080 的 DV 视频,像素宽高比 SAR = 4:3
显示尺寸 = 1440 × (4/3) : 1080 = 1920 : 1080 ← 其实是 16:9
如果你按像素尺寸缩放,等于把它当成 4:3 处理,画面会被横向压扁。
检查:
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,sample_aspect_ratio,display_aspect_ratio \
-of default=noprint_wrappers=1 input.avi
# width=1440
# height=1080
# sample_aspect_ratio=4:3
# display_aspect_ratio=16:9 ← 这才是显示比例
正确处理(把非方形像素"拉正"):
# 方法 1:缩放到显示尺寸,并把 SAR 设为 1(方形像素)
ffmpeg -i input.avi -vf "scale=1920:1080,setsar=1" -c:v libx264 ... out.mp4
# 方法 2:通用写法(自动按 SAR 计算)
ffmpeg -i input.avi -vf "scale=iw*sar:ih,setsar=1" -c:v libx264 ... out.mp4
setsar=1 一定要加——否则容器里还是标记着 SAR=4:3,播放器会再拉伸一次,画面就错了。
我现在的习惯:处理任何来源不明的视频之前,先看一眼 SAR。这一步只要 5 秒,能避免"画面比例不对"这类返工。
三、ffmpeg 的缩放算法清单
scale 滤镜的 flags 参数(swscale 的算法):
| 算法 | 类型 | 质量 | 速度 | 适合 |
|---|---|---|---|---|
fast_bilinear |
双线性(快速版) | 差 | 最快 | 实时预览 |
bilinear |
双线性 | 较差 | 很快 | ffmpeg 默认(降采样时) |
bicubic |
双三次 | 中 | 中 | 通用、放大缩小都还行 |
bicublin |
亮度双三次+色度双线性 | 中 | 中 | 折中 |
area |
面积平均 | 好(缩小) | 中 | 降采样首选 |
gauss |
高斯 | 中 | 中 | 特殊用途 |
sinc |
Sinc 窗 | 好(缩小) | 慢 | 高质量缩小 |
lanczos |
Lanczos(3-tap) | 好(放大) | 慢 | 升采样首选 |
spline |
样条 | 好(放大) | 慢 | 放大,比 lanczos 少一点振铃 |
neighbor |
最近邻 | 差(有锯齿) | 最快 | 像素艺术(保持硬边缘) |
point |
同 neighbor |
我的选择规则:
缩小(降采样)→ area 或 bicubic
放大(升采样)→ lanczos 或 spline
实时/批量吞吐优先 → bilinear 或 fast_bilinear
缩放到整数倍 → 效果最好(1/2、1/4 这种)
# 缩小
-vf scale=1280:720:flags=area
# 放大
-vf scale=1920:1080:flags=lanczos
# 也可以全局设置(影响所有隐式缩放)
-sws_flags lanczos
一个反直觉的点:bilinear 是 ffmpeg 的默认算法,但它在缩小时质量很差(因为它只用了 2×2 的邻域采样,没有做充分的抗混叠)。缩小时用 area 明显更好——这是很多人"ffmpeg 缩放质量差"印象的来源,其实只是用错了默认算法。
四、4:2:0 意味着什么:色度只有一半分辨率
绝大多数视频是 YUV 4:2:0:
亮度(Y):全分辨率 1920×1080
色度(U/V):1/2 × 1/2 960×540
也就是说:颜色信息的分辨率只有亮度的一半(水平和垂直都是)。
这是"色度子采样"(chroma subsampling),利用了人眼对亮度敏感、对色彩细节不敏感的特性。
对缩放的影响:
- 缩放时,亮度和色度要分别处理;
- 如果色度用太差的算法(或者处理不当),会出现彩色边缘锯齿、颜色渗色;
- 放大时色度的插值更重要(因为要插值出 4 倍的色度像素)。
ffmpeg 的默认行为:亮度和色度用同一个 flags(比如都 lanczos)。大多数情况下够用。
进阶控制(分别指定):
# 用 zscale 分别指定(见下节)
-vf zscale=1920:1080:f=lanczos:c=lanczos
五、色度半像素偏移与 full_chroma_int
这是个比较专业但真实存在的问题。
背景:在 4:2:0 里,色度采样点相对于亮度有一个半像素的偏移(chroma siting)。不同标准的规定不同:
- MPEG-2:色度水平对齐(left siting);
- H.264/HEVC(常见的):色度居中(center siting)。
如果缩放时不考虑这个偏移,色度会被"错开"半像素——表现是彩色边缘出现轻微的渗色或者错位,在大面积高对比色彩边缘(比如红色文字)尤其明显。
修复:给 swscale 加 full_chroma_int(用更精确的色度插值)和 full_chroma_inp(考虑输入色度定位):
ffmpeg -i in.mp4 -vf "scale=1280:720:flags=lanczos+full_chroma_int+full_chroma_inp" out.mp4
或者全局:
ffmpeg -sws_flags "+full_chroma_int+full_chroma_inp+accurate_rnd" -i in.mp4 -vf scale=1280:720 out.mp4
这几个 flag 的含义:
| flag | 作用 |
|---|---|
full_chroma_int |
色度插值用全精度的整数计算(而不是近似的快速路径) |
full_chroma_inp |
考虑输入色度的子采样位置 |
accurate_rnd |
使用精确的舍入 |
代价:慢一些(大约 20~40%)。只在对画质要求高的场景用。
我第一次看到这几个 flag 的时候完全不知道是干什么的,直到有一次处理一批带红色字幕的老片,缩放后字幕边缘出现明显的彩色毛边,加上 full_chroma_int 之后消失——这才明白它解决的是什么问题。
六、zscale:更专业的选择
zscale 是基于 zimg 库的高质量缩放/色彩转换滤镜,ffmpeg 需要编译时带 --enable-libzimg。
它比 swscale 强在哪:
- 分别指定亮度和色度算法(
f=是亮度,c=是色度); - 正确的色彩空间转换:支持指定 matrix/primaries/transfer,能做真正的色域转换(比如 BT.709 → BT.2020);
- 正确的位深处理与 dither:高位深转低位深时做抖动,避免色带;
- 色度定位(chroma location)可指定。
# 高质量缩小(亮度 lanczos,色度 bicubic)
-vf zscale=1280:720:f=lanczos:c=bicubic
# 完整色彩参数(HDR → SDR 的 tone mapping 也用它)
-vf zscale=1920:1080:f=spline36:c=lanczos:min=709:pin=709:tin=709:rin=tv:m=709:p=709:t=709:r=tv
参数含义:
| 参数 | 含义 |
|---|---|
w / h |
目标尺寸 |
f |
亮度(luma)缩放算法 |
c |
色度(chroma)缩放算法 |
min/pin/tin |
输入的 matrix / primaries / transfer |
m/p/t |
输出的 matrix / primaries / transfer |
rin / r |
输入/输出的色彩范围(tv=limited, pc=full) |
d / di |
dither 方式(位深降低时用) |
什么时候值得用 zscale:
- 做 HDR → SDR 转换(tone mapping);
- 高质量母版处理;
- 跨色域转换(BT.709 ↔ BT.2020);
- 对色度质量特别敏感的内容。
日常缩放用 swscale(scale 滤镜)就够了——差别在大多数内容上看不出来。
检查你的 ffmpeg 有没有 zscale:
ffmpeg -filters 2>/dev/null | grep zscale
七、缩放前后该做什么处理
顺序很重要,做错了会放大问题:
| 处理 | 应该在缩放前还是后 | 理由 |
|---|---|---|
| 降噪 | 前 | 噪点会被缩放"抹匀"变成更难看的块状;先降噪再缩放更干净 |
| 锐化 | 后 | 缩放后锐化才有意义(缩放前的锐化会被插值冲掉) |
| 去隔行 | 前 | 隔行素材必须先去隔行,否则缩放会把两场混在一起(梳齿状伪影) |
| 裁剪 | 前(先裁后缩放) | 缩放后再裁等于浪费了处理量 |
| 色彩转换 | 后 | 在空间域处理完再做色彩映射 |
一个典型的完整处理链(老素材修复):
ffmpeg -i old.avi \
-vf "cropdetect-safe,yadif=1,hqdn3d=2:1:3:3,scale=1920:1080:flags=lanczos+full_chroma_int,unsharp=5:5:0.8:5:5:0.0" \
-c:v libx264 -crf 20 -pix_fmt yuv420p \
-c:a aac -b:a 128k out.mp4
顺序是:去隔行 → 降噪 → 缩放 → 锐化。
关于锐化:缩放后轻微锐化能显著改善"糊"的观感,但过度锐化会产生白边(halo),看起来更假。我一般只在放大时用,参数很保守(unsharp=5:5:0.5)。
八、尺寸对齐与"非标准"分辨率
编码器对尺寸有要求:
- H.264/H.265 在 4:2:0 下要求宽高都是偶数(因为色度是 1/2);
- 某些硬件编码器要求 16 的倍数;
- 4:2:2 的水平方向要求 2 的倍数。
ffmpeg 的错误提示:
[libx264 @ 0x...] width not divisible by 2 (1281x720)
解决办法:
# 方法 1:自动向下取偶数(-2 而不是 -1)
-vf scale=1280:-2
# 方法 2:用 crop 裁掉奇数那一行/列
-vf crop=floor(iw/2)*2:floor(ih/2)*2
# 方法 3:用 pad 补(不推荐,会加黑边)
-2 的含义:自动计算另一个维度,并保证结果是偶数。-1 只保证比例不保证偶数——这个区别很多人不知道,导致"明明用了 -1 还是报错"。
需要 16 的倍数时(某些硬件编码器):
-vf "scale=trunc(iw/16)*16:trunc(ih/16)*16"
或者用 pad 补到 16 的倍数(加上黑边,但保持画面比例):
-vf "scale=1280:720,pad=ceil(iw/16)*16:ceil(ih/16)*16"
九、实测对比
降采样 1080p → 720p(同一源,VMAF 以"用 lanczos 缩放到同样尺寸再编码"为参考)
| 算法 | 相对耗时 | 主观评价 | 说明 |
|---|---|---|---|
| bilinear(默认) | 1.0× | 细节丢失明显,边缘有锯齿 | 默认就是它,难怪大家觉得 ffmpeg 缩放差 |
| area | 1.4× | 干净,无明显伪影 | 推荐 |
| bicubic | 1.5× | 略锐于 area,边缘有小振铃 | 也不错 |
| lanczos | 2.1× | 锐利,但有轻微振铃 | 缩小用它有点"过" |
| sinc | 3.0× | 最锐,振铃最明显 | 一般不用 |
升采样 480p → 1080p
| 算法 | 相对耗时 | 主观评价 |
|---|---|---|
| bilinear | 1.0× | 糊,明显 |
| bicubic | 1.6× | 尚可,稍糊 |
| lanczos | 2.3× | 清晰,边缘锐利,轻微振铃 |
| spline | 2.5× | 清晰,振铃最少 |
| neighbor | 0.8× | 块状锯齿(不推荐,除非像素艺术) |
结论:
- 缩小:
area(性价比最高); - 放大:
lanczos或spline(spline 的振铃更少,看内容选)。
耗时的影响:在"转码本来就要几秒/帧"的场景里,缩放多花 2 倍时间(缩放本身只占总耗时的 5~15%)影响不大。只有在实时转码(直播)时才需要考虑速度,那时候用 bilinear。
十、放大到 1080p 的现实预期
必须说清楚的一件事:放大不能凭空增加细节。
480p 的源里没有的细节,放大到 4K 也不会有。缩放算法能做的是:
- 让已有的边缘更平滑(减少锯齿感);
- 让插值更"自然"(避免块状、模糊);
- 配合锐化让主观清晰度提升一点。
但它不能恢复:
- 已经丢失的纹理(衣服的织纹、皮肤细节);
- 被压缩掉的块状/模糊(源本身是低码率 480p 的话,放大只会放大压缩伪影);
- 隔行、噪点这些损伤(要在缩放前单独处理)。
如果客户期待"放大后像高清一样清晰":
- 技术上只能靠 AI 超分(Real-ESRGAN 等,项目里有专门一篇讲);
- 而且超分也有代价:可能产生幻觉细节、处理极慢(实时倍数 0.1~2x)、对老片效果不稳定。
我的交付话术:"放大到 1080p 后画面会比原来更平滑、在电视上没有拉伸感,但清晰度受限于原始素材。如果要接近原生高清的观感,需要做 AI 超分,耗时是普通转码的 20~50 倍,我可以先做一集样片给您看效果。"
给客户一个样片永远比解释有用。 我现在遇到这类需求,都是先出 30 秒样片(普通缩放 vs AI 超分),让客户自己对比选择。
十一、坑清单
- 用默认的 bilinear 缩小 → 细节丢失、锯齿。用
area。 - 放大用 bilinear → 糊。用
lanczos/spline。 - 忽略 SAR → 1440×1080 的视频被当成 4:3 处理,画面变形。
- 忘了
setsar=1→ 播放器再拉伸一次。 - 用
-1而不是-2自动计算尺寸 → 奇数尺寸报错。 - 缩放不处理色度质量 → 彩色边缘毛边。加
full_chroma_int。 - 先缩放再降噪 → 噪点被抹成块状。先降噪。
- 先缩放再去隔行 → 梳齿伪影被放大。先去隔行。
- 缩放前锐化 → 白等着被插值冲掉,白费功夫。缩后锐化。
- 过度锐化 → 白边(halo),看着更假。保守参数。
- 期待放大能变清晰 → 物理上不可能。要跟客户说清楚,或者做 AI 超分样片。
- 缩放后再裁剪 → 浪费算力,且画质更差(二次采样)。
- 缩放和编码的分辨率不一致 → 缩放 720p 但编码器按 1080p 的码率给,浪费;或者反过来画质不够。
- 高位深转 8bit 不做 dither → 渐变区域出现色带。用 zscale 的 dither,或者保持 10bit。
- HDR 素材直接缩放 → 需要 tone mapping(项目里色彩空间那篇讲过),直接缩放会得到发灰的画面。
- 缩放后画质变差却以为是编码器问题 → 先单独输出缩放结果(不编码)看一眼,定位到底是哪一步的问题。
最后说说这次之后养成的习惯。
处理任何"画质不满意"的问题时,先把流程拆开单独验证:
# 1. 只缩放,不编码(输出 raw 或者无损),看缩放本身有没有问题
ffmpeg -i in.mp4 -vf scale=1920:1080:flags=lanczos -c:v libx264 -crf 0 -an scale_only.mp4
# 2. 用同样的源,只改编码参数,看编码的影响
ffmpeg -i in.mp4 -vf scale=1920:1080:flags=lanczos -c:v libx264 -crf 23 -an encode_test.mp4
# 3. 对比
很多时候"画质差"是多个环节叠加的结果(缩放算法差 + 码率不够 + 色度处理粗糙),单独看每一环才能定位。
还有一个体会:"默认参数"不等于"合适的参数"。ffmpeg 的缩放默认是 bilinear,是因为它快、通用,不是为了质量。类似的情况还有:AAC 默认码率、CRF 默认值、preset 默认 medium——这些都是"能用"的起点,不是"最优"的答案。
所以每当我要用某个工具的默认参数做正式产出时,都会问一句:"这个默认值是为了什么场景选的?我的场景是什么?" 这一问,经常能发现可以改进的地方。