做视频这行的,几乎都绕不开"画面里有一块不属于自己的东西"这种尴尬:素材是从别处拿来的授权片段、带了台标;自己几年前拍的片子,压了一行日期;客户给的成片,角落挂着早就下线的二维码。这时候在线去水印站点的第一反应都是"上传一下就好",但真到 4K、10 秒以上、需要帧间稳定的时候,在线工具要么限时长、要么二次压缩糊成一片、要么干脆给你打个更大的水印当"广告位"。VSR 这类视频修复工具链之所以在圈内被反复提起,就是因为它把"逐帧修补"变成了"时序一致重建",处理完的片段不会出现一帧糊一帧清的鬼影。这篇东西不是产品说明书,是我自己在一台普通工作站上把 VSR 从零装起来、跑通、调参、踩坑、再优化的一整套记录,适合手里有显卡、愿意折腾命令行、想把活儿留在本地跑的人看。安装过程本身不难,难的是那几个"文档里不写、报错也不告诉你"的版本匹配问题,下面一点点拆。
1. 先想清楚:为什么去水印这件事要放在本地跑
很多人对"本地部署"有误解,觉得那是技术人员为了炫技。实际不是。决定要不要把 VSR 装在自己机器上,本质上是一道成本题,只不过成本不止钱这一项。
1.1 在线工具的三笔隐性账
第一笔是画质账。绝大多数在线去水印走的是"上传—服务端处理—下载"的链路,中间至少要经历一次有损转码。你传上去的是 20 Mbps 的 4K 素材,下来可能变成 8 Mbps 的 H.264,水印是没了,但整段画面的细节也一起没了。VSR 走本地处理,输入输出可以都保持无损或者可控的编码参数,中间不经过任何人的服务器,画质损耗只来自你主动选择的编码设置。这一条对做商业交付的人几乎是决定性的,客户不会关心你用什么工具,但一定会盯着画面里的噪点和色带。
第二笔是时长账。在线服务基本都有限制,免费额度通常是 10 秒到 30 秒,超过就要付费,而且付费之后往往还有单文件体积上限。一段 3 分钟的宣传片,你要切成十几段分别上传,切点处的画面衔接还得自己处理,折腾的时间比装一套工具长得多。VSR 本地跑,理论上只受你的硬盘和耐心限制,一晚上挂着处理几十个文件是常规操作。
第三笔是素材账,也是最容易被忽略的一笔。素材上传到别人的服务器,等于把内容交给了不可控的第三方。有些素材涉及未公开的产品、还没上线的片子、客户明确要求保密的画面,这种时候"上传"这个动作本身就不该发生。我不是说在线工具一定有问题,而是说当你手里有不该外传的素材时,本地处理不是可选项,是唯一选项。
提示:去水印这件事本身有边界。只处理你自己拍摄的、已经获得授权的、或者明确允许二次编辑的素材。别人的成片、平台的付费内容、带版权标识的商业物料,把水印抹掉再传播,性质就变了。工具是中性的一把刀,怎么用取决于拿刀的人,这条线得自己守住。
1.2 VSR 解决的核心问题:为什么它比逐帧修补更像"修复"
要理解 VSR 值不值得装,得先明白它和传统做法的差别在哪。
最原始的做法是逐帧涂抹:在每一帧上框选水印区域,用周围像素做填充。单帧看没什么问题,一连起来播就露馅了——因为每帧的填充结果都是独立计算的,帧与帧之间的纹理不连续,肉眼看起来就是一块区域在"抖动""沸腾",专业点叫闪烁。水印越大、背景越复杂,这种闪烁越明显,观感上比留着水印还难受。
好一点的做法的做法是引入时序信息,也就是让模型在处理第 N 帧时,同时参考第 N-1 帧和第 N+1 帧。VSR 这类工具链的核心价值就在这儿:它不是把视频当成"一堆图片",而是当成"一段连续的运动"。模型在时序窗口内做特征对齐,把前一帧已经修好的纹理"搬运"到当前帧的对应位置,再结合当前帧自身的邻域信息做融合。结果是修复区域在整个片段里保持纹理一致,播放起来不会跳。
这个差别在什么场景下最明显?半透明水印压在运动的物体上、水印覆盖区域的背景有细密纹理(草地、布料、皮肤)、水印横跨画面明暗交界处。这三种情况用逐帧法基本做不出来,用 VSR 跑一遍,多数能到"不仔细看发现不了"的程度。当然它也不是万能的,后面讲参数的时候会说到它的失效场景。
1.3 谁适合照着这篇装
说清楚适合谁,能省掉很多无效折腾。如果你符合下面几条里的两条以上,那本地部署是划算的:
| 你的情况 | 本地部署是否划算 | 原因 |
|---|---|---|
| 每月处理超过 10 段素材 | 划算 | 在线服务的付费额度累计起来远超折腾成本 |
| 素材分辨率 2K 及以上 | 划算 | 在线工具对大分辨率普遍降质严重 |
| 素材涉密或未公开 | 必须本地 | 上传动作本身不可接受 |
| 偶尔处理一段 10 秒素材 | 不太划算 | 装环境的两小时比处理本身长 |
| 机器无独立显卡 | 慎重 | 纯 CPU 跑视频修复慢到怀疑人生 |
| 只想点两下就出结果 | 不适合 | 这套东西是命令行工作流,不是开箱即用软件 |
显存这一条得单独说。视频修复是显存杀手,因为它要同时把多帧的特征图放在显存里做对齐。8 GB 显存能舒服跑 1080p 的短片段,12 GB 以上可以碰 2K,4K 基本得 24 GB 起步,或者靠分块处理硬扛。买卡之前先看看自己现在这张卡还剩多少余量,这个数字决定了后面所有参数的上限。
2. 装之前的三张底牌:显卡、驱动、运行时版本
装这类工具翻车,九成不是装错了包,而是环境本身就不对。所以先把三件事查清楚,比后面看一百条报错有用。
2.1 先量一下显存和算力的真实余量
打开终端,先确认系统认不认你的卡。命令行输入:
# Linux 环境 nvidia-smi # Windows 环境(在 PowerShell 里) nvidia-smi这个命令会输出一张表,重点看三个数:显卡型号、显存总量(Total)、当前占用(Used)。很多人忽略"当前占用"这一项,结果装完之后跑起来就显存不足——因为桌面环境、浏览器、剪辑软件已经先占走了一大块。
这里有个经验值:把显存总量减去当前占用,再打个七折,才是你实际可用的数字。因为 VSR 在处理过程中会有峰值占用,中间某些时刻(比如时序窗口滑动、特征图拼接)的瞬时占用可能比稳态高 30% 左右。
# 想持续观察占用变化,可以循环刷 nvidia-smi --query-gpu=memory.used,memory.total,utilization.gpu --format=csv -l 1另外看一个隐藏指标:算力等级(Compute Capability)。它在显卡规格里查得到,通常是一个"X.Y"格式的数字。这个数字决定了你能装哪个版本的并行计算框架——算力太老的卡,新版本框架直接不支持,装了也跑不起来。我见过有人拿着一张三年前的入门卡硬装最新框架,报错信息是"no kernel image is available for execution on the device",翻译成人话就是"你的卡太老,这个版本我不认"。
2.2 驱动版本和运行时版本根本不是一回事
这是新手最容易混淆的地方,而且混淆的后果很隐蔽——装的时候一切正常,跑的时候报一个跟显卡毫无关系的错。
打个比方:驱动是显卡的"操作系统",是厂商提供的底层接口;运行时(CUDA Runtime)是跑在驱动之上的"应用程序接口",是深度学习框架调用的那一层。你的驱动版本必须大于等于运行时版本,反过来不行。比如驱动支持到 12.x,你可以装 11.8 的运行时,可以装 12.1 的运行时,但不能装 12.4 的。
查驱动支持的运行时上限:
nvidia-smi输出右上角的CUDA Version: 12.x这个数字,不是你已安装的运行时版本,而是"当前驱动能够支持的最高运行时版本"。这一点极其重要,很多人把它误认为已装版本,结果装框架时装了个超版本的运行时,跑起来报错才发现。
查实际装了什么运行时:
nvcc --version如果这个命令不存在,说明你没装完整的工具包(只装了驱动),这在只跑推理的场景下通常没关系——因为现代深度学习框架的 pip 包已经自带了运行时库,不需要系统全局安装。只有当你需要编译自定义算子的时候,才需要完整的工具包。
我的建议很直接:只跑推理、不编译算子的话,别去装完整的工具包。让 pip 包自己带运行时,版本匹配由包的构建方保证,能省掉大量版本冲突。
2.3 Python 版本这一关,比你想象的难过
Python 版本是这类工具安装里最容易翻车的一环,而且翻得毫无征兆。
原因是这样的:深度学习框架的预编译包,是按 Python 版本 + 运行时版本 + 操作系统三元组发布的。某个版本组合可能只发布了 Python 3.9 和 3.10 的包,Python 3.12 的包还没有。你装了最新的 Python 3.12,pip 找不到匹配的预编译包,就会去下源码包自己编译。源码编译在 Linux 上可能花一小时,在 Windows 上大概率直接失败,报一堆 C++ 编译错误——而这些错误跟你的代码完全没关系。
所以装之前先做一件事:查一下你要装的框架版本,官方支持哪些 Python 版本,然后反向选 Python,而不是先装 Python 再挑框架。
# 查看当前 Python 版本 python --version python3 --version # 查看系统里都装了哪些版本(conda 环境下) conda search python我个人的稳定组合是 Python 3.10,这个版本在目前的生态里兼容性最好,既不太老(大部分包都有 3.10 的轮子),也不至于太新(没有包还没跟上)。3.11 也可以,3.12 就要看运气了。
还有一个坑:多个 Python 版本共存时的优先级问题。系统里同时有 3.9 和 3.11,python命令指向哪个、pip命令又指向哪个,很容易错位。装完包之后跑起来报 "No module named xxx",八成就是这个原因——你把包装到了 A 环境,用 B 环境跑的。
# 一条命令确认 pip 装到哪去了 python -c "import sys; print(sys.executable)" pip -V这两行的输出路径必须指向同一个环境。如果不一致,后面所有操作都是在给自己挖坑。
3. 从零到跑通的完整安装链路
前面三张底牌查清楚,就可以动手了。这一节按顺序来,每一步都说明为什么这么做,不建议跳步。
3.1 目录规划与环境隔离:为什么一定要建虚拟环境
先说结论:绝对不要在系统默认环境里装这套东西。
理由不是洁癖。视频修复工具链的依赖是出了名的"挑版本"——它需要特定版本的深度学习框架、特定版本的图像处理库、特定版本的视频编解码绑定。这些依赖一旦和系统里其他项目的依赖打架,你会在两个项目之间反复重装,一晚上就这么没了。
建一个独立环境,最省事的方式:
# 创建独立环境,指定 Python 版本 conda create -n vsr python=3.10 -y # 激活 conda activate vsr # 确认当前环境 which python # 应该输出类似 .../envs/vsr/bin/python不用 conda 的话,Python 自带的 venv 也够用:
python -m venv ~/envs/vsr source ~/envs/vsr/bin/activate # Linux / macOS # Windows: ~\envs\vsr\Scripts\activate再说目录规划。我习惯这样组织:
~/vsr-workspace/ ├── envs/ # 虚拟环境(如果用 venv) ├── repo/ # 工具源码目录 ├── weights/ # 模型权重,单独放,方便备份 ├── inputs/ # 待处理素材 ├── outputs/ # 处理结果 ├── masks/ # 遮罩图 └── logs/ # 运行日志为什么要把权重单独放?因为权重文件动辄几百 MB 到几 GB,而且是不常变动的资产。放在代码目录里,一旦你重新拉代码或者切换分支,权重就跟着丢了或者被覆盖,重新下载一次可能就是一小时。独立目录 + 配置文件里写绝对路径,是最省心的做法。
提示:Windows 用户特别注意路径里的中文和空格。这套工具链里有些底层组件对非 ASCII 路径的处理不完善,放在
C:\Users\张三\我的视频\这种路径下,可能出现"文件存在但读不到"的诡异问题。统一用全英文、无空格的路径,能避免掉一大类玄学报错。
3.2 依赖安装的先后顺序为什么不能乱
这里必须讲清楚顺序,因为顺序错了会出现"装完了但跑不起来"的情况。
第一步:先装框架本体,并且指定运行时版本。
# 先确认驱动支持的运行时上限,再决定装哪个版本 # 假设驱动支持到 12.x,我们装 11.8 的运行时(更稳) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118为什么用官方索引而不是默认源?因为默认源上的包可能是 CPU 版本,装完跑起来只有几帧每秒,你还以为是参数没调好。用官方索引能确保拿到对应运行时版本的预编译包。
装完立刻验证:
python -c "import torch; print(torch.__version__); print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"输出里cuda.is_available()必须是True。如果是False,先别往下走,八成是三个原因之一:装成了 CPU 版本、驱动版本太低、运行时版本和驱动不匹配。这时候往下装都是白装。
第二步:装视频编解码相关的绑定。
视频读写是这套流程里最容易被忽略的一环。深度学习框架能处理张量,但读不了 MP4。你需要一个能解码视频帧的库。常见的选择是 FFmpeg 的 Python 绑定:
# 方式一:pip 装(部分平台需要系统里有 ffmpeg 可执行文件) pip install ffmpeg-python # 方式二:直接用系统包管理器装 ffmpeg 本体 # Ubuntu/Debian sudo apt install ffmpeg # macOS brew install ffmpeg # Windows 用官方构建好的可执行文件,解压后加进 PATH装完验证:
ffmpeg -version这条命令能输出版本信息,说明可执行文件在 PATH 里,Python 绑定才能正常调用。
第三步:装工具本身的依赖。
cd ~/vsr-workspace/repo pip install -r requirements.txt到这一步如果有人告诉你"先装 requirements 再装框架",那是错的。因为 requirements 里通常只写torch而不带版本约束,pip 会去装最新版,可能和你的驱动不匹配。先手动指定框架版本装好,再装 requirements 时 pip 会发现依赖已满足,直接跳过,这样才稳。
装完做个依赖冲突检查:
pip check这条命令会列出所有版本冲突。如果能输出 "No broken requirements found",环境就算干净了。
3.3 拿一段 3 秒的自测片段验证全链路
装完之后直接上正式素材是最亏的做法——跑十分钟报个错,你都不知道是环境问题还是素材问题。
我的习惯是先造一段 3 秒的测试片段,把"读视频—处理—写视频"这三段链路全走一遍:
# 从任意一段素材里截 3 秒出来,重编码成小尺寸,处理速度快 ffmpeg -ss 00:00:05 -t 3 -i input.mp4 -vf scale=640:-2 -c:v libx264 -crf 18 test_clip.mp4这段命令的意思是:从第 5 秒开始取 3 秒,缩放到宽 640,用高质量参数重新编码。为什么缩放?因为小尺寸能快速暴露问题,处理一段 640 宽的视频可能只要十几秒,而 4K 要几分钟。等链路跑通了,再上大尺寸。
然后跑一次最小化的处理:
python run.py \ --input inputs/test_clip.mp4 \ --output outputs/test_out.mp4 \ --mask masks/test_mask.png \ --window 5 \ --batch 2跑完检查三件事:输出文件是否存在、文件大小是否合理(不为 0、也不至于比输入小一半)、能不能用播放器正常打开。这三件事都过了,才说明环境是通的。这里--window指的是时序窗口大小,--batch指的是每批处理的帧数,具体含义后面详细说,现在只要知道它们是最影响显存的两个参数就行。
4. 模型权重与目录结构:跑通之后的第一道坎
环境跑通只是把"引擎"装好了,真正的"油"是模型权重。这一步的坑不在技术难度,而在信息混乱——网上流传的权重文件名五花八门,你不知道哪个对应哪个。
4.1 权重文件怎么认、放哪、怎么校验
权重文件一般有三种常见封装:
| 格式 | 后缀 | 特点 | 适用场景 |
|---|---|---|---|
| 完整检查点 | .pth/.ckpt | 包含模型结构 + 参数 + 训练状态,体积大 | 需要继续训练或微调 |
| 纯参数 | .pth/.bin | 只有参数,需要配合代码里的结构定义 | 常规推理,体积小 |
| 通用交换格式 | .onnx | 与框架解耦,可跨运行时 | 部署到不同设备 |
只做推理的话,优先找纯参数版本,体积小、加载快。完整检查点动辄两三个 GB,加载时还要占额外内存。
权重放好之后,一定要做一次完整性校验。下载中断是最常见的隐形故障,文件看着在,其实是残缺的,加载时报的是"unexpected end of file"或者莫名其妙的张量形状不匹配。
# 看文件大小是否和官方公布的数值一致 ls -lh ~/vsr-workspace/weights/ # 如果有官方提供的哈希值,做一次比对 sha256sum ~/vsr-workspace/weights/model.pth我在这一点上吃过亏。有一次权重下载到 98% 断了,浏览器显示"下载完成",文件大小差了几十 MB,但由于没有校验,我在后面折腾了两个小时,怀疑过驱动、怀疑过显存、怀疑过参数,最后才发现是文件残缺。
4.2 配置文件里的路径写法,决定了你能不能一键切换
多数这类工具用一个 YAML 或 JSON 配置文件描述"用哪个权重、走哪个流程"。这个文件里的路径写法有讲究。
# config.yaml model: name: vsr_default weights: /home/user/vsr-workspace/weights/model.pth # 绝对路径 device: cuda:0 io: input_dir: /home/user/vsr-workspace/inputs output_dir: /home/user/vsr-workspace/outputs mask_dir: /home/user/vsr-workspace/masks inference: window_size: 5 batch_size: 2 tile_size: 0 # 0 表示不分块 fp16: true为什么坚持写绝对路径?因为相对路径的基准是"你从哪个目录执行的命令",而不是"配置文件在哪"。你在repo/目录下跑和在~/下跑,相对路径解析出来的结果完全不同,排查起来极其费劲。用绝对路径,虽然看着不优雅,但省心。
fp16: true这一项值得单独提一句。它表示用半精度浮点做推理,显存占用能降到大约一半,速度还能快 20% 到 40%。代价是极少数情况下会出现数值精度问题,表现为画面某些区域出现异常亮点。一般视频修复场景下,半精度是完全够用的,建议默认开着;只有在结果里看到莫名其妙的色块或亮斑时,才关掉它排查。
5. 参数怎么调:去水印效果的四个关键旋钮
环境和权重都齐了,剩下的问题就变成"怎么让它修得好看"。这一节是全篇最需要经验的部分,因为参数之间是相互牵制的,不是单调调优。
5.1 遮罩比坐标靠谱:为什么我不推荐用矩形框
很多工具支持用坐标定义一个矩形区域作为水印位置,写起来简单:
--region 1200,80,1600,180 # x1,y1,x2,y2但矩形框有两个硬伤。第一,它把框内所有内容都当成需要修复的,包括那些本来完好的画面——水印是斜着的、或者形状不规则的时候,框必须画大,导致整片区域被重绘,细节丢失。第二,矩形框无法表达"这一块要修、那一块不要修",而实际素材里经常出现水印压在关键主体上的情况。
遮罩图的思路是给一张和视频同尺寸的黑白图,白色表示需要修复,黑色表示保留原样。它的好处是精确到像素:
from PIL import Image import numpy as np # 生成一张全黑的遮罩,尺寸和视频帧一致 w, h = 1920, 1080 mask = np.zeros((h, w), dtype=np.uint8) # 把水印区域标白(这里给一个示例区域) mask[80:180, 1200:1600] = 255 Image.fromarray(mask).save("masks/test_mask.png")写这段代码的时候,遮罩图只存单通道灰度,不要存成 RGB。有些工具读到三通道遮罩会按第一通道处理,结果恰好是对的;有些会报维度错误。单通道是通用做法。
还有一个实用技巧:遮罩可以适当外扩 3 到 5 个像素。因为水印边缘往往有半透明的过渡区域(尤其是压缩过的视频,水印边缘会有"光晕"),只按精确边界做遮罩,修完之后边缘会留下一圈淡淡的痕迹。外扩几像素,把过渡区也包进去,结果会干净很多。
# 用膨胀操作把遮罩外扩 4 像素 import cv2 kernel = np.ones((9, 9), np.uint8) mask_dilated = cv2.dilate(mask, kernel, iterations=1) cv2.imwrite("masks/test_mask_dilated.png", mask_dilated)5.2 批大小和显存占用:一个可以直接抄的换算
批大小(batch size)是一次同时处理多少帧。它和显存的关系近似线性,但不是完全线性,因为还有模型本身占用的固定开销。
我实测出来的经验公式大致是这样:
显存占用 ≈ 模型固定开销 + batch_size × 单帧开销(分辨率)对于 1080p 素材,单帧开销大约在 0.6 到 1.0 GB 之间(取决于模型规模),模型固定开销大约 1.5 GB。所以:
| 分辨率 | 批大小 | 预估显存占用 | 适合的显卡显存 |
|---|---|---|---|
| 720p | 1 | 约 2.5 GB | 6 GB 起 |
| 720p | 4 | 约 5.5 GB | 8 GB 起 |
| 1080p | 1 | 约 3.0 GB | 8 GB 起 |
| 1080p | 4 | 约 7.5 GB | 12 GB 起 |
| 1080p | 8 | 约 13 GB | 16 GB 起 |
| 2K | 4 | 约 14 GB | 24 GB 起 |
这张表是估算,实际会因为模型规模和帧内容(复杂画面特征图更多)有上下浮动。保守做法是从 batch=1 开始,跑通了再往上加,每次翻倍,观察显存占用,接近上限的 85% 就停。
批量大有什么好处?主要是吞吐效率。批处理能更好地利用显卡的并行能力,同样一段视频,batch=4 比 batch=1 可能快一倍多。所以只要显存够,就往上加,加到快撑不住了为止。
显存不够又不想降分辨率的办法是分块(tiling):
--tile 512意思是把每一帧切成 512×512 的小块分别处理,再拼回去。好处是显存占用大幅下降,坏处是块与块之间可能出现接缝——因为每块独立处理,边缘区域的上下文信息不完整。缓解办法是让块之间重叠一部分,多数实现里会有个tile_overlap参数,设成 64 或 128,重叠区取两次处理结果的平均值或者用羽化过渡。
5.3 时序窗口:窗口越大越稳,也越容易拖影
时序窗口(window size)是这个工具最有特色的参数,也是最难调的一个。
它的含义是:处理当前帧时,向前后各看几帧作为参考。窗口设成 5,就是看前后各 2 帧,加上自己共 5 帧。
窗口大的好处是时序稳定性更好——模型有更多信息来判断"这块区域在运动中应该是什么样子",修复结果在帧间更连贯。尤其在水印覆盖区背景有缓慢运动的时候(比如镜头平移、人物走动),大窗口的效果明显好于小窗口。
窗口大的坏处是两个。第一个是显存,窗口里的每一帧都要参与特征对齐,显存占用近似线性增长。第二个更麻烦:拖影。当画面里有快速运动或者场景切换的时候,窗口跨得太远,模型会把不相关的内容"搬"过来,表现为修复区域出现残影——某个人物的轮廓、某个物体的边缘,糊在本来不该有东西的地方。
我的调节经验是这样的:
| 画面特征 | 建议窗口 | 理由 |
|---|---|---|
| 静态机位、背景不动 | 7 到 9 | 内容稳定,大窗口收益高 |
| 缓慢平移或人物慢走 | 5 | 平衡稳定性和拖影 |
| 手持晃动、快速运动 | 3 | 运动剧烈,大窗口必定拖影 |
| 有场景切换 | 3 或更小 | 切换点两侧内容无关,窗口必须小 |
| 多镜头剪辑的成片 | 3,并在切点处分段 | 分段处理比调窗口更有效 |
最后一条值得展开说。如果素材是剪辑过的成片,镜头之间有硬切,那么无论窗口设多小,切点附近都会有问题——因为切点前后的画面内容完全无关,模型硬要把它们对齐,必然出错。正确做法是先用镜头检测找出所有切点,然后按切点把视频切成独立片段,分别处理,最后再拼回去。
# 用 ffmpeg 的场景检测找出切点时间戳 ffmpeg -i input.mp4 -vf "select='gt(scene,0.3)',showinfo" -f null - 2>&1 | grep showinfo输出的信息里会有每处切点的时间,把它们记下来,作为分段处理的边界。这个预处理动作看起来麻烦,但比起处理完发现切点处一塌糊涂要重跑,还是划算太多。
5.4 一个容易忽略的参数:输出编码质量
处理完的帧要重新编码成视频,这一步的编码参数直接决定你前面所有努力有没有白费。
# 错误的做法:用默认参数输出,可能只有 2 Mbps ffmpeg -i frames/%06d.png output.mp4 # 正确的做法:明确指定编码器和质量参数 ffmpeg -framerate 30 -i frames/%06d.png \ -c:v libx264 -preset slow -crf 16 \ -pix_fmt yuv420p \ output.mp4这里-crf 16是关键。CRF 是恒定质量模式,数值越小质量越高、文件越大。18 是"视觉无损"的常用值,16 接近无损。为什么不用默认值?因为默认的 CRF 通常是 23,在这个值下水印是去掉了,但整个画面会因为压缩出现色带和块效应,尤其在渐变色区域(天空、皮肤)特别明显。
-pix_fmt yuv420p这一项是为了兼容性。有些播放器和平台不接受 yuv444 或者 10 位色深的输出,指定成 yuv420p 能确保到处都能播。代价是色度信息有损,但在 8 位色深下肉眼看不出差别。
6. 报错现场还原:六个高频问题的排查链路
这一节不讲"标准答案",讲的是我实际遇到问题时怎么一步步缩小范围。直接给结论的教程,你遇到的问题永远不在那个列表里。
6.1 "Torch not compiled with CUDA enabled":先排除误装 CPU 版
这个报错很多人第一反应是"驱动坏了",其实八成是装错了包。
排查链路我是这么走的:
第一步,确认装的是不是 GPU 版本:
python -c "import torch; print(torch.version.cuda)"输出如果是None,那就是 CPU 版本,实锤。原因是从默认源装包时,pip 下的是 CPU 构建。解决办法是卸载重装,走官方索引:
pip uninstall torch torchvision -y pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118第二步,如果torch.version.cuda有值但is_available()还是 False,那就查驱动:
nvidia-smi如果这条命令本身报错或者找不到,那是驱动没装好,或者显卡根本没被系统识别。这种情况在虚拟机和某些远程环境里很常见,因为显卡没有直通给这个环境。
第三步,如果前两步都正常,检查设备序号:
python -c "import torch; print(torch.cuda.device_count())"多卡机器上如果指定了cuda:1但只有一张卡,就会失败。这种情况在有集成显卡的笔记本上也可能出现——设备 0 是核显,独显是设备 1。
6.2 显存不足的报错,未必真的是显存不够
"CUDA out of memory" 这个报错最坑的地方在于,它可能是真的不够,也可能是被别的进程占着,还可能是显存碎片化。
排查顺序:
# 第一步:看当前谁在占显存 nvidia-smi # 第二步:如果看到 Python 进程残留,杀掉 # 找到进程号后 kill -9 <PID>我遇到过最诡异的一次:所有相关进程都杀干净了,nvidia-smi显示显存占用是 0,但跑起来还是 OOM。最后发现是有个后台的图形界面程序占着显存不放(桌面环境的合成器),显示不出来但确实占着。解决办法是关掉那个程序,或者切换到命令行模式跑。
如果确实不够,按这个顺序降配,每一步降一个档:
- 打开半精度
--fp16 - 批大小减半
--batch 1 - 开启分块
--tile 512 - 时序窗口减小
--window 3 - 分辨率降一半(最不推荐,画质损失大)
按这个顺序降的好处是,画质损失从 0 到明显是递增的,能保住尽量好的结果。
6.3 输出视频只有一帧或者打不开:编码链路的典型断点
处理跑完了,输出文件 200 字节,打不开。这种问题基本出在编码环节。
最常见的原因是帧文件名排序错了。如果你的中间帧存成frame_1.png到frame_1000.png,按字符串排序会变成frame_1, frame_10, frame_100...,顺序完全乱掉。解决办法是用固定位数的序号:
# 用零填充,保证字典序等于数值序 filename = f"frame_{idx:06d}.png"第二个原因是帧率和输入不匹配。输入是 30 fps,输出用默认的 25 fps,结果是播放速度变了。这一点必须在编码命令里明确:
ffmpeg -framerate 30 -i frames/%06d.png -c:v libx264 -crf 16 -pix_fmt yuv420p out.mp4第三个原因是音轨丢失。帧序列里没有音频,直接编码出来的视频是无声的。需要把原视频的音轨单独抽出来再合并:
# 抽出音轨 ffmpeg -i input.mp4 -vn -c:a copy audio.aac # 合并 ffmpeg -i out.mp4 -i audio.aac -c:v copy -c:a aac -shortest final.mp4-shortest是防止音视频长度不一致导致的末尾黑屏。
6.4 修完的水印区域发灰、发糊:不是模型不行,是遮罩或窗口有问题
这个现象很常见,画面里修过的区域像蒙了一层雾。原因通常有三个。
原因一:遮罩范围过大。遮罩画得比水印大一圈,导致大片正常区域被重绘。重绘区域越大,模型的"猜"的成分越多,猜不准就糊。解决是把遮罩收回到水印边缘 + 3 像素。
原因二:时序窗口太大导致平均化。大窗口下模型会把多帧信息做加权平均,运动区域平均之后就会糊。把窗口从 7 降到 3 试试,如果清晰度回来了,那就是这个原因。
原因三:半精度导致的精度损失。关掉--fp16跑一遍对比,如果清晰度有改善,说明这个模型的某些层对精度敏感,那就老老实实用全精度。
我一般的做法是拿一段 3 秒的片段,用四组参数各跑一遍,横着对比:
| 组别 | 窗口 | 精度 | 遮罩外扩 | 观察点 |
|---|---|---|---|---|
| A | 3 | fp16 | 3 px | 基线 |
| B | 3 | fp32 | 3 px | 精度影响 |
| C | 7 | fp16 | 3 px | 窗口影响 |
| D | 3 | fp16 | 8 px | 遮罩影响 |
对比的时候不要用播放器看,逐帧暂停看,特别看修复区域的边缘和纹理细节。播放时人眼会被运动掩盖细节,容易误判。
6.5 处理速度慢到不可接受:分清楚瓶颈在哪
一段 10 秒 1080p 的视频跑了两小时,这个速度肯定有问题。先定位瓶颈,别盲目换硬件。
# 看处理过程中显卡利用率 nvidia-smi --query-gpu=utilization.gpu,memory.used --format=csv -l 1如果 GPU 利用率长期低于 40%,说明瓶颈不在显卡,而在数据读取或写盘。常见原因是把中间帧存成 PNG 再读回来——PNG 的编解码很慢,尤其在高分辨率下。解决办法是改成无损的二进制格式,或者干脆让流程在内存里串起来,不落盘。
如果 GPU 利用率接近 100% 但速度还是慢,那是真算力不够,只能降配。降配顺序参考 6.2 节,优先开半精度,其次减小窗口。
还有一个容易被忽略的点:硬盘 IO。输入输出放在机械硬盘上,4K 素材的读写速度会成为明显瓶颈。把工作目录放到 SSD 上,速度提升可能比换显卡还明显。
6.6 处理到一半突然中断:先看日志,别急着重跑
长任务中断,第一反应是重跑,但重跑之前一定要看日志,否则同样的错误还会再犯一次。
# 把输出同时写到文件,方便事后查 python run.py ... 2>&1 | tee logs/run_$(date +%Y%m%d_%H%M).log日志里重点看中断前的最后几行。常见的三类:
- 显存不足:中断点前后的帧内容更复杂,导致峰值占用超了。解决是降批大小,或者开分块。
- 文件读取失败:素材里有个别帧损坏,读到就崩。解决是用 ffmpeg 先做一次完整性检查,把坏帧丢弃或补齐。
- 内存泄漏:跑得越久占用越高,最后被系统杀掉。这种情况多见于帧数据没有及时释放,解决办法是分段处理——把长视频切成 30 秒一段,独立进程跑,跑完释放,再跑下一段。
分段处理这个做法我后来变成了默认策略。它不只是防内存泄漏,还有个额外好处:某一段出问题不用全部重跑,几十秒的损失可以接受。
7. 批量处理与效率:把一晚上的活压到一小时
单段跑通之后,真正的工作量在批量。这一节讲的是怎么把零散的操作组织成可靠的流水线。
7.1 用脚本组织流程,而不是手工重复
手工一条条敲命令的问题不是慢,是不可复现。今天调了三个参数跑出来的效果,明天想再跑一遍,你得回忆当时敲了什么。
我的做法是写一个简单的 shell 脚本,把所有参数集中在一处:
#!/usr/bin/env bash set -euo pipefail # 一出错就停,不带着错误往下跑 WORKSPACE=~/vsr-workspace INPUT_DIR="$WORKSPACE/inputs" OUTPUT_DIR="$WORKSPACE/outputs" MASK="$WORKSPACE/masks/main_mask.png" WINDOW=5 BATCH=2 TILE=0 mkdir -p "$OUTPUT_DIR" "$WORKSPACE/logs" for f in "$INPUT_DIR"/*.mp4; do name=$(basename "$f" .mp4) echo "[$(date +%H:%M:%S)] 开始处理: $name" python "$WORKSPACE/repo/run.py" \ --input "$f" \ --output "$OUTPUT_DIR/${name}_fixed.mp4" \ --mask "$MASK" \ --window "$WINDOW" \ --batch "$BATCH" \ --tile "$TILE" \ --fp16 \ 2>&1 | tee "$WORKSPACE/logs/${name}.log" echo "[$(date +%H:%M:%S)] 完成: $name" doneset -euo pipefail这一行看着不起眼,作用很大。它保证任何一个文件处理失败时脚本立刻停下,而不是继续跑完剩下的然后给你一堆失败的输出。早停能让你及时发现问题是普遍的还是个例的。
7.2 用并行把显卡吃满,但别过头
批处理默认是串行的,一次一个文件。如果你的显卡在处理单个文件时利用率不高(比如只有 50%),那可以并行跑两个进程,把剩余的算力利用起来。
# 用 xargs 做简单的并行,-P 2 表示同时跑两个 find "$INPUT_DIR" -name "*.mp4" -print0 | \ xargs -0 -P 2 -I {} bash -c ' name=$(basename "{}" .mp4) python ~/vsr-workspace/repo/run.py \ --input "{}" \ --output ~/vsr-workspace/outputs/${name}_fixed.mp4 \ --mask ~/vsr-workspace/masks/main_mask.png \ --window 3 --batch 1 --fp16 '这里的关键是并行数乘以单进程显存占用不能超过总显存。如果单进程用 6 GB,总显存 12 GB,那并行 2 个刚好,并行 3 个必然 OOM。而且并行太多反而会变慢,因为显卡调度开销变大,显存频繁换入换出。
我的经验是并行数不超过 2,除非你用的小模型、小分辨率、显存余量很大。视频处理不是那种能靠堆并发提升吞吐的任务类型。
7.3 一个被低估的提速手段:预处理降噪
这个发现有点反直觉。水印区域如果本身噪点很多(夜景、高 ISO 拍摄的素材),模型的修复难度会大幅上升,因为它要在噪声里找纹理,处理时间和效果都会打折扣。
做法是在处理之前先做一次轻度降噪:
ffmpeg -i input.mp4 -vf "hqdn3d=1.5:1.5:3:3" -c:v libx264 -crf 16 preprocessed.mp4hqdn3d的前两个参数是空间降噪强度,后两个是时间降噪强度。1.5 是很轻的强度,肉眼几乎看不出细节损失,但能让模型少处理很多无用信息。实测下来,同样一段素材,预处理降噪之后处理速度能快 15% 到 25%,修复区域也更干净。
代价是这个预处理本身也要时间。如果素材本来就很干净,这一步就是纯浪费。我的判断标准是:放大到 200% 看画面,如果能明显看到颗粒感,就做降噪;如果画面干净得像塑料,就跳过。
7.4 结果验收:不要只看处理完的那一片
最后说一个验收方法,这是我自己吃过亏之后定下来的流程。
处理完之后,不要只盯着修复区域看,要整段完整播放一遍,重点检查四个地方:
- 修复区域与周边的过渡带:有没有一条明显的分界线,或者一圈颜色略异的"描边"
- 运动场景下修复区域是否跟随:水印压在移动物体上时,修复的纹理有没有跟着物体一起动
- 镜头切换点前后两帧:有没有突然的亮度跳变或者形状错位
- 画面整体色调:有没有偏色,尤其是修复区域和周边色调是否一致
这四项里,前两项是模型能力问题,后两项多半是参数或流程问题。过渡带有明显分界,通常是遮罩边缘太生硬,可以做一次羽化;色调不一致,往往是半精度或者多次编码累积造成的,关掉 fp16、改成一次编码能改善。
我现在的习惯是把原片和修复片并排播放,同样的时间轴同步走,一帧一帧对。看着费时间,但比交出去被打回来重做要划算得多。做这行久了会发现,真正的效率不来自跑得多快,而来自返工得少。