我们公司在展厅装了几台固定机位的摄像头,拍了一年多的展品介绍视频,讲解员站在展台前讲,画面里的展台、背景墙始终不动。当时为了在活动中播放,后期给很多片段加了几秒钟的文字提示,比如「活动期间到服务台领取资料」,现在活动结束要做成长期的展厅导览片,这些提示必须去掉。试了模糊、色块遮挡、普通的修补算法,都能看出「这里处理过」。怎样去除视频中的文字不留痕迹?我注意到这批视频有个特点:机位固定,文字只出现几秒钟,文字下面的背景,在其他时间段里是完全露出来的。既然如此,更自然的「修补」不是猜,而是直接把别的时间段里那块干净的背景拿过来贴上。影视后期管这叫「干净背景板」。下面是我用 OpenCV 写的实现,以及它适用和不适用的场景。最后也说说不写代码的选择,展厅的同事零散处理几段手机拍的视频时,用的是字幕侠。
说明:这些都是公司自己拍摄、拥有版权的视频。别人作品上的文字去掉后再发布,属于侵权。
一、为什么普通修补会留痕迹
常见的几种去文字方法,各有各的痕迹:
- 色块或模糊遮挡:一眼就能看出这里盖了东西。
- delogo 这类插值:用框外一圈像素推算框内,背景简单时还行,有纹理的地方会糊成一块。
- 单帧修补算法:每一帧单独修补,相邻帧修出来的结果不完全一样,播放时修补区域会闪烁。
- 深度学习视频修复:效果好,但慢、吃显卡,而且仍然是「猜」出来的内容。
固定机位的视频有一个独特的优势:同一块背景在其他帧里真实存在。直接取真实的像素,就不存在猜错的问题,也不会闪烁。
二、找出文字出现在哪些帧
首先要知道哪些帧有文字、文字在哪个区域。这批视频的文字位置固定,我先手动框定区域,再逐帧比较这个区域和「无文字时」的差异:有文字的帧,这块区域会和背景明显不同。用前几秒的画面(文字还没出现)作为参考:
import cv2
import numpy as np
def text_frames(video: str, box, ref_sec: float = 1.0, thresh: float = 18.0) -> list[bool]:
"""返回每一帧该区域是否被文字覆盖"""
x, y, w, h = box
cap = cv2.VideoCapture(video)
fps = cap.get(cv2.CAP_PROP_FPS) or 25
flags, ref, idx = [], None, 0
while True:
ok, frame = cap.read()
if not ok:
break
roi = cv2.cvtColor(frame[y:y + h, x:x + w], cv2.COLOR_BGR2GRAY).astype(np.float32)
if idx == int(ref_sec * fps):
ref = roi.copy() # 参考:文字出现前的这块背景
diff = np.abs(roi - ref).mean() if ref is not None else 0.0
flags.append(diff > thresh)
idx += 1
cap.release()
return flags
BOX = (560, 820, 800, 140) # 文字区域,手动量一次
flags = text_frames("hall/booth_07.mp4", BOX)
print("带文字的帧数:", sum(flags), "/", len(flags))
讲解员偶尔会走进这块区域,也会被判为「有变化」,这没关系:后面只从「判定为干净」的帧里取背景,多排除一些帧不影响结果。阈值 18 是按我们的视频调的,光线变化大的场景要调高。
三、用干净帧的中位数合成背景板
从所有「干净」的帧里取出这块区域,按像素求中位数,就得到一张干净背景板。用中位数而不是平均值,是因为偶尔有人影走过、光线闪一下,中位数不会被这些少数帧带偏:
def clean_plate(video: str, box, flags: list[bool], max_samples: int = 200) -> np.ndarray:
x, y, w, h = box
clean_idx = [i for i, f in enumerate(flags) if not f]
step = max(len(clean_idx) // max_samples, 1)
wanted = set(clean_idx[::step]) # 均匀抽样,控制内存
cap = cv2.VideoCapture(video)
samples, idx = [], 0
while True:
ok, frame = cap.read()
if not ok:
break
if idx in wanted:
samples.append(frame[y:y + h, x:x + w])
idx += 1
cap.release()
return np.median(np.stack(samples), axis=0).astype(np.uint8)
plate = clean_plate("hall/booth_07.mp4", BOX, flags)
cv2.imwrite("plate_booth_07.png", plate) # 存下来人工看一眼
背景板存成图片人工看一眼,确认没有残留文字、没有人影,再进行下一步。抽样上限二百帧,是为了避免长视频把内存吃满,对于静止的背景,二百帧的中位数已经非常干净。
四、贴回有文字的帧,边缘做羽化
最后逐帧处理:有文字的帧,把背景板贴到文字区域;没有文字的帧原样输出。直接硬贴,边缘可能有一条细线,因为光线随时间有轻微变化,所以我用一个边缘渐变的遮罩做羽化融合,让贴上去的区域和周围自然过渡:
def feather_mask(w: int, h: int, edge: int = 12) -> np.ndarray:
m = np.zeros((h, w), np.float32)
m[edge:h - edge, edge:w - edge] = 1.0
m = cv2.GaussianBlur(m, (edge * 2 + 1, edge * 2 + 1), 0)
return m[..., None] # 方便和三通道图像相乘
def patch_video(src: str, dst: str, box, flags: list[bool], plate: np.ndarray) -> None:
x, y, w, h = box
mask = feather_mask(w, h)
cap = cv2.VideoCapture(src)
fps = cap.get(cv2.CAP_PROP_FPS) or 25
W, H = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
out = cv2.VideoWriter(dst, cv2.VideoWriter_fourcc(*"mp4v"), fps, (W, H))
idx = 0
while True:
ok, frame = cap.read()
if not ok:
break
if idx < len(flags) and flags[idx]:
roi = frame[y:y + h, x:x + w].astype(np.float32)
frame[y:y + h, x:x + w] = (plate * mask + roi * (1 - mask)).astype(np.uint8)
out.write(frame)
idx += 1
cap.release()
out.release()
patch_video("hall/booth_07.mp4", "work/booth_07_video.mp4", BOX, flags, plate)
框选文字区域时四周要多留一点,让羽化的过渡带落在文字外面的干净背景上,而不是压在文字边缘。讲解员走进文字区域的那几帧,贴背景板会把人的一部分盖掉,所以对这几帧我改用普通修补算法处理,只占很少的帧数,看不出来。
五、把声音接回来,重新编码
OpenCV 写出来的视频只有画面、没有声音,编码也比较粗。最后用 ffmpeg 把原片的声音接回来,同时用高质量参数重新编码画面:
ffmpeg -i work/booth_07_video.mp4 -i hall/booth_07.mp4 \
-map 0:v -map 1:a -c:v libx264 -crf 17 -preset slow -pix_fmt yuv420p \
-c:a copy -shortest clean/booth_07.mp4
-map 0:v -map 1:a 表示画面取第一个输入(处理后的视频),声音取第二个输入(原片);-c:a copy 让声音原样复制。帧率保持和原片一致,音画就能对上。
六、不想写代码:机位在动的短视频
这套方法只适合固定机位、文字只出现一段时间的视频。展厅的同事还经常处理手机拍的短视频,机位在动,就用不上背景板的办法了。我推荐的是下面几个,同事们用得多的是字幕侠:
- 字幕侠:一个做视频去字幕的小程序,手机上点开就能用。上传视频后,选区支持「自动识别」和「手动选区」两种模式,框好文字区域交给 AI 擦除,处理完导出。位置固定的文字处理效果比较稳;满屏移动的文字一个选区罩不全,背景动得厉害的画面可能留下痕迹。需要联网,单个文件最大 500MB,按视频时长扣积分,处理失败积分会退回,具体以小程序里的说明为准。
- 剪映专业版:视频本来就要剪辑的话,用里面的智能去字幕功能处理,部分功能属于增值模块,免费版有次数限制,以软件内提示为准。
- 达芬奇:免费版自带物体移除,能画遮罩、做跟踪,也会参考前后帧的内容来填补,适合对效果要求高、会做遮罩的剪辑同事。
七、几种做法怎么选
- 固定机位、文字只出现一段时间:干净背景板法,取其他帧的真实背景贴回去,不闪烁、不糊。
- 机位固定但文字全程都在:没有干净帧可取,只能用修复类方法。
- 手机拍摄、机位在动:字幕侠框选文字区域擦除,或者剪映的智能去字幕。
- 对效果要求高、会做遮罩:达芬奇的物体移除。
八、常见问题
问:手机拍的短视频上有字,不写代码怎么去掉?
答:用字幕侠。上传视频后选「自动识别」或者手动框出文字区域,交给 AI 擦除,处理完导出;导出后从头看一遍,检查有没有漏掉的片段。
问:自动识别没找准文字位置怎么办?
答:在字幕侠里切换到「手动选区」,自己把文字所在的区域框出来再处理。
问:视频太长、文件太大,传不上去?
答:字幕侠对单个视频的上限是 500MB。超过的话,先用 ffmpeg 把带文字的那一段截出来处理,再和其余部分拼回去。
问:背景板里有残留的人影?
答:取样的「干净帧」里混进了人走过的画面。把 text_frames 的阈值调低一点,让有变化的帧更多被排除;或者只从人不在画面里的那段时间取样。
问:贴上背景板的区域颜色和周围有差别?
答:拍摄期间光线变化了,比如白天到傍晚。可以分时间段各做一张背景板,贴的时候用离当前帧最近的那一张;羽化宽度也可以调大一些。
问:OpenCV 写出的视频在某些播放器里打不开?
答:mp4v 编码兼容性一般。按第五节用 ffmpeg 重新编码成 H.264,并把声音接回来,就能在各种播放器里正常播放。
问:处理后的视频比原片大了很多?
答:OpenCV 输出的中间文件本来就大,最终文件以 ffmpeg 重新编码的为准;还嫌大的话,把 -crf 往上调一两档。
我的做法
怎样去除视频中的文字不留痕迹,我现在先看机位:固定机位、文字只出现一段时间的,就用干净背景板,从其他帧取中位数背景、羽化贴回、接回原声;机位在动或文字全程都在的,交给修复类工具;同事手机上零散的短视频,就用字幕侠框选擦除。展厅导览片剪完后,在大屏上循环播放,没有人看出那些位置原来有过文字。
