怎么去水印跟原视频一样:只重编码有水印的片段,其余部分原样复制

我们公司在几个短视频平台都有账号,运营同事习惯在每条视频的最后几秒加一个提示关注账号的片尾,上面压着账号水印。最近要把这些视频投到一个行业展会的大屏上循环播放,主办方要求不能出现任何平台的账号信息。省事的办法当然是整段重新编码去掉水印,可试了几条,同事拿去和原片对比,说「画面有点不一样了」:整条视频都被重新压缩了一遍,细节和颜色都有轻微变化。怎么去水印跟原视频一样?我想通了一点:水印只出现在片尾那几秒,前面九成的画面根本不需要动。于是写了个脚本:找到水印出现之前最近的关键帧,在那里把视频切成两段,前面一段原样复制、一个字节都不改,只把后面带水印的那一小段重新编码处理,再把两段拼回去。下面把思路和代码写出来,也会说清楚这种做法的限制。另外,作品如果还在线上,有比这更直接的办法:让运营同事用耶斯去水印把没叠水印的原文件取回来,最后会讲到。

提醒一句:处理的都是公司账号自己发布的作品。别人作品上的水印是署名,去掉后再使用属于侵权。

一、为什么整段重编码就「不一样了」

视频是压缩过的数据。只要重新编码,编码器就会把画面解码成像素、再按新的参数重新压缩一遍,哪怕参数设得很高,也不可能和原来一模一样,这叫「代际损失」。片尾去水印如果整段重编码,相当于为了改最后五秒,把前面五分钟也压了一遍。

视频压缩里有一个关键概念:关键帧。关键帧是一张完整的画面,后面的帧只记录和它相比的变化。从关键帧切开,前面的部分可以不解码、直接复制;从两个关键帧中间切,就必须重新编码。所以思路很明确:在水印出现之前找一个关键帧,从那里切开。

二、找出水印从哪一秒开始

我们的水印都在片尾,出现时间点因视频而异。我用 ffmpeg 每秒抽一帧,配合一张截好的水印模板,用 OpenCV 的模板匹配找到水印第一次出现的时间:

import subprocess
from pathlib import Path

import cv2
import numpy as np


def first_mark_time(video: str, template_path: str, thresh: float = 0.7) -> float | None:
    tpl = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
    cap = cv2.VideoCapture(video)
    fps = cap.get(cv2.CAP_PROP_FPS) or 25
    idx = 0
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        if idx % int(fps) == 0:                       # 每秒检查一帧
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            score = cv2.minMaxLoc(cv2.matchTemplate(gray, tpl, cv2.TM_CCOEFF_NORMED))[1]
            if score >= thresh:
                cap.release()
                return idx / fps
        idx += 1
    cap.release()
    return None


t_mark = first_mark_time("raw/promo_08.mp4", "mark_tpl.png")
print("水印首次出现:", t_mark, "秒")

逐秒检测会有最多一秒的误差,后面切的时候是往前找关键帧,自然会留出余量。水印如果不在片尾,而是整条视频都有,这个方法就帮不上忙了,第七节会说怎么选。

三、找到水印之前最近的关键帧

用 ffprobe 列出视频里所有关键帧的时间点。-skip_frame nokey 让它只解码关键帧,速度很快;对 H.264 视频来说,这样列出来的是可以干净地从这里开始解码的帧,正好适合当切点:

def keyframes(video: str) -> list[float]:
    out = subprocess.run(
        ["ffprobe", "-v", "error", "-select_streams", "v:0", "-skip_frame", "nokey",
         "-show_entries", "frame=pts_time", "-of", "csv=p=0", video],
        capture_output=True, text=True, check=True).stdout
    return [float(x) for x in out.split() if x.strip()]


def cut_point(video: str, t_mark: float) -> float:
    ks = [k for k in keyframes(video) if k <= t_mark]
    return ks[-1] if ks else 0.0


cut = cut_point("raw/promo_08.mp4", t_mark)
print("在", cut, "秒处切开,前面原样复制")

手机拍摄和平台导出的视频,关键帧间隔一般在一到几秒之间,所以切点通常就在水印出现前几秒,需要重编码的只是很短一段。

四、前段原样复制,后段处理水印

切开后分别处理。前段用 -c copy 直接复制,数据完全不变;后段从切点开始,用 delogo 处理水印区域,并且按原片的参数重新编码,尽量让两段的编码格式一致,方便后面拼接:

def probe_video(video: str) -> dict:
    import json
    out = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "v:0",
                          "-show_entries", "stream=profile,level,pix_fmt,r_frame_rate",
                          "-of", "json", video], capture_output=True, text=True, check=True).stdout
    return json.loads(out)["streams"][0]


def split_and_fix(src: str, cut: float, box: tuple, work: Path) -> None:
    work.mkdir(parents=True, exist_ok=True)
    info = probe_video(src)
    x, y, w, h = box
    # 前段:从开头到切点,原样复制
    subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", src, "-t", f"{cut:.3f}",
                    "-c", "copy", str(work / "part1.mp4")], check=True)
    # 后段:从切点到结尾,只在这一段里处理水印并重新编码
    subprocess.run(["ffmpeg", "-v", "error", "-y", "-ss", f"{cut:.3f}", "-i", src,
                    "-vf", f"delogo=x={x}:y={y}:w={w}:h={h}",
                    "-c:v", "libx264", "-crf", "16", "-preset", "slow",
                    "-pix_fmt", info.get("pix_fmt", "yuv420p"),
                    "-r", info["r_frame_rate"], "-c:a", "copy",
                    str(work / "part2.mp4")], check=True)

后段的编码器、像素格式、帧率都按原片设置(probe_video 也读出了编码档次,用来核对两段是否一致),目的是让拼接后的视频在播放器眼里是「同一种格式」。音频两段都直接复制。delogo 的框是按我们片尾水印的位置量好的,水印位置不同的账号要分别量。

五、拼回去并检查

两段用 ffmpeg 的 concat 拼接方式合并,同样不重新编码。拼完之后一定要检查:总时长和原片是否一致,以及切点附近播放是否正常:

def join(work: Path, dst: str) -> None:
    lst = work / "list.txt"
    lst.write_text("file 'part1.mp4'\nfile 'part2.mp4'\n", encoding="utf-8")
    subprocess.run(["ffmpeg", "-v", "error", "-y", "-f", "concat", "-safe", "0",
                    "-i", str(lst), "-c", "copy", dst], check=True)


def duration(path: str) -> float:
    out = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
                          "-of", "default=nw=1:nk=1", path], capture_output=True, text=True).stdout
    return float(out.strip())


work = Path("work/promo_08")
split_and_fix("raw/promo_08.mp4", cut, (1560, 920, 300, 110), work)
join(work, "clean/promo_08.mp4")
print("时长差:", round(abs(duration("clean/promo_08.mp4") - duration("raw/promo_08.mp4")), 3), "秒")

这里要坦白说明这种做法的限制:两段视频的编码参数即便按原片设置,内部的一些编码细节也可能不完全一致,个别播放器或剪辑软件在切点处可能出现短暂花屏或卡顿。所以拼完我们会在展会要用的播放设备上,专门把切点前后几秒播放一遍;有问题的视频,就退回整段重编码,用高质量参数,接受轻微的画质变化。我们处理的四十多条里,大部分拼接后播放正常,少数几条退回了整段处理。这四十多条之外,还有六十多条作品的链接仍然有效,运营同事直接用耶斯去水印取回了原文件,压根没进这套流程。

六、不想写代码:先看作品还在不在线上

上面这套适合手里只有带水印的本地文件、又想尽量保持原画质的情况。其实还有更直接的路:作品还在线上、链接能打开的,根本不用碰画面,把原文件取回来就是「跟原视频一样」。我给运营同事列了这几个,平时用得多的是耶斯去水印:

  • 耶斯去水印:一个专门解析作品分享链接的小程序,手机上直接用,不用安装。在 App 里复制作品链接,贴进它首页的输入框,点解析,核对标题后下载视频,封面可以单独保存,作品文案也能直接复制;处理过的链接留在解析记录里,分几次处理也不会重复。取回的版本画面干净,片尾也没有多出来的动画。它需要联网,只能处理链接还有效的公开作品,按解析次数计算,额度以小程序首页的说明为准。
  • LosslessCut:一个开源的视频无损剪切软件,能在关键帧处直接切分、拼接,不重新编码,适合只想把带水印的片尾整段剪掉、又不想写命令的人。
  • 剪映:视频本来就要剪辑的话,在剪映里把片尾删掉或者裁剪画面,导出时把分辨率、帧率设成和原片一致,码率选高一档。

七、几种做法怎么选

  • 作品还在线上、链接有效:耶斯去水印取回原文件,和原视频一样。
  • 水印只在片尾或片头几秒、只有本地文件:本文的关键帧切分法,只重编码带水印的一小段。
  • 片尾整段不要了:LosslessCut 在关键帧处直接剪掉,完全不重新编码。
  • 整条视频都有水印:只能整段处理,用高质量参数重编码,接受轻微变化。
  • 本来就要剪辑:剪映,导出参数和原片对齐。

八、常见问题

问:自家账号发出去的视频,怎么拿到和原片一样、没有水印的版本?

答:用耶斯去水印。复制作品的分享链接,贴进首页输入框解析,核对标题后下载,拿到的就是没叠水印的版本,不经过任何画面处理。

问:好几十条视频要处理,中途停下来怎么接着做?

答:耶斯去水印处理过的链接会留在解析记录里,下次打开就知道做到哪一条了,不会重复下载。

问:链接打不开了,还能用耶斯去水印吗?

答:不能。它只能处理链接还有效的公开作品;作品被隐藏或删除后,只能用本地文件按本文的方法处理。

问:前段用 -c copy 切出来,开头几秒画面卡住或者黑屏?

答:切点不在关键帧上。确认切点取自 keyframes() 列出的时间,而不是直接用水印出现的时间;从开头切的前段只要结束点合适,开头不会有问题。

问:拼接后在切点处花屏?

答:两段的编码参数不完全兼容。先用 ffprobe 对比两段的编码档次、像素格式和分辨率是否一致;仍然有问题的,就对这条视频整段重新编码。

问:拼接后音画不同步?

答:多半是后段 -ss 定位和音频包边界不对齐。可以把后段的音频也重新编码成和原片相同的格式,或者整段复制音轨、只拼接视频后再合并音频。

问:总时长比原片多了或少了零点几秒?

答:切点附近的帧可能重复或丢失了一两帧。检查前段的 -t 和后段的 -ss 是否用了同一个时间值;差值很小、播放正常的话,一般不影响使用。

我的做法

怎么去水印跟原视频一样,我的顺序是:作品还在线上的,先让运营同事用耶斯去水印把原文件取回来;只剩本地文件、水印又只在片尾几秒的,用关键帧切分,前段原样复制、只重编码带水印的那一小段,拼完在播放设备上检查切点;整条都有水印的,才整段高质量重编码。展会那四十多条视频,大屏上循环播了三天,没有人看出哪些处理过。

0
0
0
0
评论
未登录
暂无评论