处理一批低照度拍的素材时,我用了最常见的一招:
bash ffmpeg -i noisy.mp4 -vf hqdn3d -c:v libx264 -crf 20 out.mp4噪点确实少了,但画面糊了——人脸像蒙了一层塑料,而且人物一动就有拖影。客户说"不如不降"。
我去查了 ffmpeg 的降噪滤镜清单,发现不止
hqdn3d一个——还有nlmeans、bm3d、fftdnoiz、vaguedenoiser等好几个,原理完全不同。我拿同一段素材把它们都跑了一遍,对比效果和耗时,最后选出了适合不同场景的方案。这篇是完整的横评数据和参数经验表。
TL;DR:ffmpeg 里的降噪滤镜按原理分三类:空域+时域滤波(
hqdn3d,最快但有拖影)、非局部均值(nlmeans,保细节好,慢)、变换域(bm3d、fftdnoiz、vaguedenoiser,质量最好,最慢)。实测结论:批量/实时 →hqdn3d用保守参数(时域参数要小,否则拖影);重要内容 →bm3d(质量最好);折中 →nlmeans。另外两个关键点:降噪必须在编码之前做(降噪后码率需求下降 20~40%,体积更小);过度降噪的"塑料感"比噪点更难看,宁可保守。
目录
- 一、噪声是从哪来的
- 二、三类降噪原理
- 三、ffmpeg 的降噪滤镜清单
- 四、逐个说参数
- 五、横评实测
- 六、拖影:时域降噪的代价
- 七、降噪和编码的关系
- 八、参数经验表
- 九、批量流程:先测噪声再定参数
- 十、坑清单
一、噪声是从哪来的
| 来源 | 表现 | 特点 |
|---|---|---|
| 传感器噪点(高 ISO、低照度) | 彩色斑点、暗部明显 | 最常见 |
| 压缩伪影(低码率) | 块效应、振铃、蚊子噪点 | 不是真噪声,降噪效果差 |
| 磁带/胶片老化 | 划痕、颗粒、掉帧 | 老素材 |
| 传输误码 | 局部块状损坏 | 少见 |
重要区分:真噪声(传感器) 和 压缩伪影 的处理方式不同。
- 真噪声是随机的 → 降噪滤镜有效;
- 块效应是有结构的 → 要用去块(deblock)而不是降噪(虽然
hqdn3d的空域滤波能稍微缓解)。
判断方法:看一帧的平坦区域(比如墙面),如果是随机的彩色斑点 → 真噪声;如果是 8×8 的方块边界 → 压缩伪影。
二、三类降噪原理
1. 空域滤波(Spatial)
在一帧内部用邻域像素平均:
当前像素 = 周围 3×3 像素的加权平均
代价:细节和噪声一起被抹掉(边缘变糊)。
2. 时域滤波(Temporal)
利用相邻帧的信息:噪声是随机的,真实内容在时间上是连续的,所以把连续几帧平均就能抵消噪声。
当前像素 = (前一帧 + 当前帧 + 后一帧) / 3
代价:运动区域会拖影(因为运动物体在相邻帧里位置不同,平均之后就"重影"了)。
3. 非局部均值 / 变换域(NLMeans / BM3D)
NLMeans:不只用"周围的像素",而是在整幅图里找相似的块,用它们的加权平均。这样能保留重复纹理(不会被自己的邻域"平均掉")。
BM3D:把相似的块堆叠成 3D 数组,在变换域(DCT/小波)做阈值收缩。目前公认效果最好的传统算法。
代价:慢(NLMeans 比 hqdn3d 慢 10 倍,BM3D 慢 20~30 倍)。
三、ffmpeg 的降噪滤镜清单
ffmpeg -filters 2>/dev/null | grep -iE 'denoise|dnoiz|nlmeans|bm3d|hqdn3d'
| 滤镜 | 原理 | 速度 | 质量 | 备注 |
|---|---|---|---|---|
hqdn3d |
空域 + 时域(高斯) | 最快 | 中 | 最常用,默认参数偏强 |
nlmeans |
非局部均值 | 慢 | 好 | 保细节好 |
bm3d |
BM3D(变换域) | 最慢 | 最好 | 需要编译时带 libgsl?(部分版本) |
fftdnoiz |
FFT 频域滤波 | 较慢 | 好 | 3D(空域+时域) |
vaguedenoiser |
小波软阈值 | 中 | 中 | 有 wavelet 模式 |
owdenoise |
小波(Overcomplete) | 中 | 中 | |
dctdnoiz |
DCT 阈值 | 中 | 中 | |
removegrain |
AviSynth 滤镜,ffmpeg 没有 |
检查你的 ffmpeg 支持哪些:
ffmpeg -filters 2>/dev/null | grep -E ' (hqdn3d|nlmeans|bm3d|fftdnoiz|vaguedenoiser|owdenoise|dctdnoiz) '
如果某个滤镜没列出,说明编译时没带(用官方静态构建一般都有)。
四、逐个说参数
hqdn3d
-vf "hqdn3d=luma_spatial:chroma_spatial:luma_tmp:chroma_tmp"
| 参数 | 默认 | 说明 |
|---|---|---|
luma_spatial |
4.0 | 亮度空域强度(抹细节) |
chroma_spatial |
3.0 | 色度空域强度 |
luma_tmp |
6.0 | 亮度时域强度(拖影的来源) |
chroma_tmp |
4.5 | 色度时域强度 |
默认值偏强(这也是我用默认参数翻车的原因)。常用组合:
# 轻度(推荐起点)
-vf "hqdn3d=2:1:3:3"
# 中度
-vf "hqdn3d=4:3:6:4.5" # 默认
# 只降色度噪点(保留亮度细节,最保守)
-vf "hqdn3d=0:0:0:6"
最后一个"只降色度"的用法很有用:传感器噪点里最碍眼的是彩色噪点(红绿蓝的斑点),而亮度噪点相对不明显。只降色度可以在几乎不损失细节的前提下让画面干净很多。
nlmeans
-vf "nlmeans=s=1.0:p=7:pc=5:r=15:rc=15"
| 参数 | 说明 |
|---|---|
s |
滤波强度(主要调这个,0.5~3.0) |
p |
亮度块匹配的 patch 大小(默认 7) |
r |
亮度搜索范围(默认 15,越大越慢越准) |
pc / rc |
色度版本(可以更小,省时间) |
经验值:
# 轻度
-vf "nlmeans=s=0.8:p=7:r=9"
# 中度
-vf "nlmeans=s=1.5:p=7:r=15"
# 强(慢)
-vf "nlmeans=s=3.0:p=7:r=21"
bm3d
-vf "bm3d=sigma=3:block=8:bstep=4:group=1:range=9:hard=0"
| 参数 | 说明 |
|---|---|
sigma |
噪声强度估计(1~10,越大降得越狠) |
block |
块大小(8 或 16) |
bstep |
块步长(越小越慢越准) |
group |
分组大小 |
range |
时域搜索范围(0 = 只做空域,>0 用时域) |
hard |
0 = 软阈值(默认,更自然),1 = 硬阈值 |
sigma 是关键:它应该匹配素材的实际噪声水平。设太大 → 过度平滑;设太小 → 没效果。
怎么估计 sigma:可以用 bm3d 的 estimate 参数(basic 估计),或者先试几个值看效果。
fftdnoiz
-vf "fftdnoiz=sigma=0.5:amount=1:block=12:overlap=6"
| 参数 | 说明 |
|---|---|
sigma |
噪声阈值(0.1~2) |
amount |
降噪强度(0~1) |
block |
FFT 块大小(8~16) |
overlap |
块重叠(block 的一半常见) |
vaguedenoiser
-vf "vaguedenoiser=threshold=2:method=garrote:nsteps=6:percent=85"
| 参数 | 说明 |
|---|---|
threshold |
阈值(越高降得越少) |
method |
hard / soft / garrote(推荐) |
nsteps |
小波分解层数 |
percent |
保留的能量百分比 |
五、横评实测
测试条件:1080p30 的夜景素材(明显的高 ISO 噪点),30 秒片段,8 核机器。
评分:3 人盲评"细节保留度" 1~5 分;PSNR 用"人工加噪的干净源"做参考(有原片)。
| 滤镜 | 参数 | 相对耗时 | PSNR 提升 | 细节评分 | 拖影 |
|---|---|---|---|---|---|
| 不处理 | — | 1× | 0 | 4.5(细节在,但噪点多) | 无 |
| hqdn3d 默认 | 4:3:6:4.5 | 1×(8 秒) | +2.1 dB | 2.6 | 明显 |
| hqdn3d 保守 | 2:1:3:3 | 1× | +1.4 dB | 3.7 | 轻微 |
| hqdn3d 仅色度 | 0:0:0:6 | 1× | +1.1 dB | 4.3 | 轻微 |
| nlmeans | s=1.0,r=15 | 12×(95 秒) | +2.8 dB | 4.1 | 无 |
| nlmeans 强 | s=2.0,r=15 | 14× | +3.1 dB | 3.6 | 无 |
| bm3d | sigma=3,range=9 | 27×(210 秒) | +3.5 dB | 4.4 | 无 |
| bm3d 空域 | sigma=3,range=0 | 18× | +3.0 dB | 4.3 | 无 |
| fftdnoiz | sigma=0.5 | 19× | +2.5 dB | 3.9 | 无 |
| vaguedenoiser | threshold=2 | 7× | +2.2 dB | 3.5 | 无 |
几个关键发现:
hqdn3d默认参数的细节评分只有 2.6 ——这就是我翻车的原因,它降得太狠了;- "仅降色度"的性价比极高:PSNR 提升 1.1 dB(不算多),但细节评分 4.3(几乎无损),耗时和不处理差不多;
bm3d质量最好(+3.5 dB,细节 4.4),但慢 27 倍;nlmeans是好的折中:质量接近 bm3d,耗时只有它的一半。
再看体积影响(降噪后编码,同 CRF 23):
| 处理 | 输出体积 |
|---|---|
| 不降噪 | 100% |
| hqdn3d 默认 | 68% |
| hqdn3d 仅色度 | 82% |
| bm3d | 62% |
降噪后体积明显下降——噪点是最"贵"的信息(随机、不可压缩)。所以降噪不只是画质改善,还能省码率。
六、拖影:时域降噪的代价
这是 hqdn3d 最容易出问题的地方。
时域降噪假设"相邻帧的同一位置是同一内容"。当画面里有运动物体时,这个假设不成立:
前一帧:人脸在位置 A
当前帧:人脸在位置 B
平均后:A 和 B 都有淡淡的人脸 → 拖影/鬼影
hqdn3d 的 luma_tmp 参数就是控制这个的。默认值 6.0 相当激进,所以运动画面会有明显拖影。
我的规则:
| 内容 | luma_tmp |
|---|---|
| 静止为主(访谈、监控、PPT) | 可以用大一点(6~8) |
| 中等运动 | 3~4 |
| 剧烈运动(赛事、演唱会) | 0~2(几乎关掉时域) |
| 有快速摇镜头 | 0(只做空域) |
验证拖影:抽连续 3 帧看运动物体的边缘,或者播放时看运动区域有没有"残影"。
nlmeans / bm3d 的时域版本(range > 0)也会拖影,但它们有运动补偿或者更保守的块匹配,拖影比 hqdn3d 轻得多。
七、降噪和编码的关系
顺序:降噪 → 编码(不能反过来,编码后再降噪是对"已经损伤的画面"再处理)。
为什么要在编码前降噪:
- 编码器会浪费大量码率去编码噪点(噪点是高频、随机的,最难压缩);
- 降噪后同样画质需要的码率下降 20~40%;
- 反过来,如果不降噪直接编码,噪点会导致码率飙升或者画质下降(编码器"顾此失彼")。
实测(同一段夜景,目标 VMAF 93):
| 处理 | 需要的码率 |
|---|---|
| 不降噪 | 6800 kbps |
| bm3d 降噪后 | 4100 kbps(-40%) |
这是个很划算的交易:花 210 秒做降噪,换来 40% 的码率节省(对长期存储和分发来说,收益会持续累积)。
但要注意:降噪本身是有损的。如果内容很干净就不要降噪(白白损失细节)。所以要先判断噪声水平(见第九节)。
八、参数经验表
按噪声程度(我用画面的平坦区域标准差估计):
| 噪声水平 | 推荐 | 参数 |
|---|---|---|
| 很轻微(几乎看不到) | 不处理 | — |
| 轻微 | hqdn3d 仅色度 | hqdn3d=0:0:0:4 |
| 中等(暗部可见) | hqdn3d 保守 或 nlmeans | hqdn3d=2:1:3:3 / nlmeans=s=0.8 |
| 明显(高 ISO 夜景) | nlmeans 或 bm3d | nlmeans=s=1.5 / bm3d=sigma=3 |
| 严重(极暗环境) | bm3d + 先降分辨率? | bm3d=sigma=5 |
| 老磁带/胶片颗粒 | 保留颗粒!用轻度 | hqdn3d=1:1:2:2 |
最后一条是个审美的选择:胶片颗粒是内容的一部分(很多导演特意加颗粒),降噪会把"电影感"一起降掉。所以老片/胶片素材我只做很轻的处理,或者干脆不处理。
九、批量流程:先测噪声再定参数
不要对所有素材用同一套参数——先测噪声水平:
def estimate_noise(video_path, samples=10):
"""用一个简单的方法估计噪声:平坦区域的高频能量。"""
import cv2
import numpy as np
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
scores = []
for i in range(samples):
cap.set(cv2.CAP_PROP_POS_FRAMES, int(total * i / samples))
ok, frame = cap.read()
if not ok:
continue
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY).astype(np.float32)
# 高通(原图 - 模糊)
hp = gray - cv2.GaussianBlur(gray, (5, 5), 1.5)
# 只在平坦区域(低梯度)测量
grad = np.abs(cv2.Sobel(gray, cv2.CV_32F, 1, 1, ksize=3))
flat = grad < 5
if flat.sum() > 1000:
scores.append(np.abs(hp[flat]).std())
cap.release()
return float(np.mean(scores)) if scores else 0.0
经验阈值(1080p):
| 噪声分数 | 判断 |
|---|---|
| < 1.5 | 干净,不处理 |
| 1.5~3 | 轻微,仅色度 |
| 3~6 | 中等,hqdn3d 保守 |
| > 6 | 明显,nlmeans/bm3d |
批量脚本:
def pick_filter(noise: float) -> str:
if noise < 1.5:
return ''
if noise < 3:
return 'hqdn3d=0:0:0:4'
if noise < 6:
return 'hqdn3d=2:1:3:3'
return 'nlmeans=s=1.5:p=7:r=15'
def process(path, out):
noise = estimate_noise(path)
vf = pick_filter(noise)
cmd = ['ffmpeg', '-y', '-v', 'error', '-i', path]
if vf:
cmd += ['-vf', vf]
cmd += ['-c:v', 'libx265', '-crf', '26', '-preset', 'slow',
'-c:a', 'copy', out]
subprocess.run(cmd, check=True)
return {'file': path, 'noise': noise, 'filter': vf or 'none'}
处理完一定要抽查(抽 3~5 个文件看画面),尤其是"降噪过度"的情况——这个自动检测不出来,只能人看。
十、坑清单
- 用 hqdn3d 默认参数 → 太狠,画面糊。用保守参数或仅色度。
- 时域参数太大 → 运动拖影。按内容调
luma_tmp。 - 对干净素材降噪 → 白白损失细节。先测噪声水平。
- 对胶片颗粒做重度降噪 → 电影感没了。颗粒要保留。
- 用降噪去修块效应 → 不对症。块效应用 deblock 或者重新编码提高码率。
- 降噪放在编码之后 → 已经晚了。必须在编码前。
- bm3d 太慢,跑了一整批才发现 → 先测一段算总时间。
- 没检查 ffmpeg 是否支持该滤镜 → 某些构建缺 bm3d。
- nlmeans 参数给太大(s=5)→ 慢且过度平滑。s 一般 0.5~3。
- bm3d 的 sigma 不匹配噪声 → 太小没效果,太大过度平滑。先试几个值。
- 降噪后不重新评估码率 → 可以降 CRF 或降码率,省体积。降噪后检查输出体积。
- 对所有片段用同一参数 → 一段素材里不同场景噪声不同(暗场景噪多)。理想是分场景处理(复杂,一般不做)。
- 降噪后再锐化想"补回细节" → 补不回来,还会放大残留噪声的观感。
- 批量处理后不抽查 → 过度降噪只能人眼发现。
- 忘了降噪会改变编码器的码率分配 → 如果用了固定码率(ABR),降噪后画质会更好(同样的钱买更多有效信息);如果用 CRF,体积会更小。都是好事,但要重新校准参数。
最后说说这次横评的收获。
最大的认知改变是:降噪是一个"交换"而不是"改善"。
你用"细节"换"干净"。交换比并不固定——它取决于滤镜算法(bm3d 的交换比远好于 hqdn3d)和参数强度。所以:
- 想清楚你要什么:如果内容的价值在"真实感"(新闻、纪录片、取证),保守降噪甚至不降;如果价值在"观感"(宣传片、短视频),可以激进一点;
- 用更好的算法而不是更大的强度:hqdn3d 强度加倍,细节损失加倍;换成 bm3d,降噪更多但细节保留更好。"更好的工具"优于"更大的参数"。
还有一个很实用的发现:"只降色度"这个技巧性价比极高。彩色噪点是人眼最敏感的(黑白噪点看起来像"颗粒感",彩色噪点看起来像"脏"),而只降色度几乎不损失亮度细节——用很小的代价解决最碍眼的问题。
这套思路(找到"最碍眼的部分"而不是"全部处理")在其他地方也适用:
- 编码:ROI 编码只给重要区域加质量;
- 修复:老片只需要去划痕和去隔行,不需要全面重做;
- 优化:先优化最慢的那个环节,而不是全部优化。
精准地解决主要矛盾,比"全面处理"更有效,也更省成本。