视频去水印方法:半透明水印用黑白标定片反算原画面,比模糊遮挡干净得多

我们公司运营着几个品牌短视频账号,最近要把早期作品重新剪成年度合集。问题和很多人一样:早期的原片丢了,手里只有从账号保存下来的版本,角落里叠着半透明的账号水印。常见的视频去水印方法,无论是 delogo 插值、模糊遮挡还是修补算法,本质上都是「把那块画面猜出来」,背景复杂时多少会留下痕迹。后来我注意到一件事:平台水印通常是半透明叠加上去的,水印下面的原画面并没有被完全盖住,只是和水印按某个比例混合了。如果能知道水印本身长什么样、透明度是多少,就可以用数学方法把原画面「反算」回来,而不是去猜。下面是我用一段纯黑、一段纯白的标定视频算出水印参数,再批量还原画面的做法。当然,作品还在线上、链接能打开的,更省事的办法是直接用耶斯去水印把原文件取回来,最后会讲到。

说明:本文处理的都是公司账号自己发布的作品,标定视频也是上传到自己账号、设为仅自己可见、用完即删。别人作品上的水印是署名,去掉后再使用属于侵权,不要这么做。

一、半透明水印是怎么叠上去的

半透明叠加可以用一个很简单的公式描述:每个像素的显示结果 J,等于水印颜色 W 乘以透明度 α,再加上原画面 I 乘以 (1−α):

  • J = α·W + (1−α)·I

其中 α 在 0 到 1 之间:0 表示这个像素完全没有水印,1 表示被水印完全盖住。只要知道每个像素的 α 和 α·W,原画面就能反过来算出来:

  • I = (J − α·W) / (1−α)

所以问题变成了:怎么得到水印区域每个像素的 α 和 α·W?方法是让平台把水印叠在已知的画面上。

二、做两段标定视频:纯黑和纯白

用 ffmpeg 生成两段纯色视频,分辨率、帧率和要还原的视频保持一致,各几秒钟就够。上传到自己的账号(设为仅自己可见),平台会给它们叠上同样的水印,再按平时的方式保存下来:

# 生成纯黑、纯白各 5 秒的标定视频,分辨率与原作品一致(这里以竖屏 1080x1920 为例)
ffmpeg -f lavfi -i color=c=black:s=1080x1920:r=30:d=5 -c:v libx264 -pix_fmt yuv420p black.mp4
ffmpeg -f lavfi -i color=c=white:s=1080x1920:r=30:d=5 -c:v libx264 -pix_fmt yuv420p white.mp4

拿回来带水印的版本后,原画面 I 是已知的:纯黑是 0,纯白是 255。代入公式:黑底上的显示结果 J_b 就等于 α·W;白底上的显示结果 J_w 等于 α·W + (1−α)·255。两式相减,就能解出 α。下一节把这一步写成代码。

三、从标定视频算出 α 和 α·W

视频压缩会带来噪点,单帧算出来的值会有起伏,所以每段标定视频取多帧求中位数,结果更稳定:

import cv2
import numpy as np


def median_frame(video: str, n: int = 30) -> np.ndarray:
    cap = cv2.VideoCapture(video)
    frames = []
    while len(frames) < n:
        ok, f = cap.read()
        if not ok:
            break
        frames.append(f.astype(np.float32))
    cap.release()
    return np.median(np.stack(frames), axis=0)


def calibrate(black_marked: str, white_marked: str, box):
    x, y, w, h = box
    jb = median_frame(black_marked)[y:y + h, x:x + w]          # = α·W
    jw = median_frame(white_marked)[y:y + h, x:x + w]          # = α·W + (1-α)·255
    alpha = 1.0 - (jw - jb) / 255.0
    alpha = np.clip(alpha, 0.0, 0.95)                          # 太接近 1 的像素无法还原
    return alpha, jb


BOX = (40, 120, 360, 140)                                       # 水印所在区域,按实际位置量
alpha, aw = calibrate("marked/black.mp4", "marked/white.mp4", BOX)
np.save("wm_alpha.npy", alpha)
np.save("wm_aw.npy", aw)

alpha 截断在 0.95 以内,是因为透明度接近 1 的像素几乎被水印完全盖住,原画面的信息所剩无几,强行反算会把噪点放大成杂色,这部分要单独处理。算出来的结果存成文件,同一个账号、同一种分辨率的视频都能复用。水印会在画面里换位置的,每个位置各做一次标定。

四、逐帧反算,还原原画面

有了 α 和 α·W,对带水印视频的每一帧,在水印区域套用反算公式,其余像素原样保留:

def restore_video(src: str, dst: str, box, alpha: np.ndarray, aw: np.ndarray) -> None:
    x, y, w, h = box
    cap = cv2.VideoCapture(src)
    fps = cap.get(cv2.CAP_PROP_FPS) or 30
    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))
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        region = frame[y:y + h, x:x + w].astype(np.float32)
        restored = (region - aw) / (1.0 - alpha)               # I = (J - αW) / (1 - α)
        frame[y:y + h, x:x + w] = np.clip(restored, 0, 255).astype(np.uint8)
        out.write(frame)
    cap.release()
    out.release()


restore_video("raw/promo_12.mp4", "work/promo_12_video.mp4", BOX,
              np.load("wm_alpha.npy"), np.load("wm_aw.npy"))

这个方法的效果取决于水印的透明度:透明度越低,还原后越干净,基本看不出处理过;水印颜色越实、透明度越高,还原后的噪点越明显。我们账号的水印透明度不高,大部分区域还原后肉眼很难分辨。

五、处理残留:高透明度像素和压缩噪点

反算完,通常还会剩两类小问题:一是透明度接近 1 的那部分像素(比如水印文字颜色很实的笔画中心),还原不了;二是视频压缩带来的噪点被除以 (1−α) 后放大,出现细小的杂色。我的处理是:把这些像素标出来,交给图像修补算法补一下,修补范围只有零星几个像素,痕迹很小;最后把声音接回来,重新编码:

def touch_up(frame_region: np.ndarray, alpha: np.ndarray, hard: float = 0.8) -> np.ndarray:
    mask = (alpha.max(axis=2) > hard).astype(np.uint8) * 255   # 透明度太高、还原不可靠的像素
    mask = cv2.dilate(mask, np.ones((3, 3), np.uint8))
    return cv2.inpaint(frame_region, mask, 3, cv2.INPAINT_TELEA)
# 把原片的声音接回来,并用 H.264 重新编码画面
ffmpeg -i work/promo_12_video.mp4 -i raw/promo_12.mp4 -map 0:v -map 1:a \
  -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -c:a copy -shortest clean/promo_12.mp4

touch_up 可以放进第四节的逐帧循环里,对还原后的区域调用一次。阈值 0.8 按水印的实际情况调,修补的像素越少越好。整套做法比整块修补干净得多,因为大部分像素是真实还原出来的,只有极少数像素是猜的。

六、不想写代码:链接还能打开的作品

上面的方法适合原片丢了、只剩带水印本地文件的情况。如果作品还在线上,分享链接能打开,根本不需要这么折腾,直接取回没叠水印的原文件就行。我们年度合集里的大部分视频,是用耶斯去水印这样处理的:

  • 耶斯去水印:一个解析作品分享链接的小程序,手机上直接用,不用另外下载软件。复制自家作品的分享链接,贴进首页的输入框解析,核对标题后下载视频,封面可以单独保存,作品文案也能直接复制;处理过的链接会留在解析记录里,分几次处理也不会重复。取回的是没叠水印的版本,画面没有任何修补痕迹。需要联网,只能处理链接仍然有效的公开作品,按解析次数计算,额度以小程序首页的说明为准。
  • 剪映:链接失效、只有本地文件,又不想写代码的,可以在剪映里把水印所在的边缘裁掉,导出时分辨率、帧率和原片保持一致;裁剪会损失一点画面。
  • 达芬奇:免费版的物体移除可以处理少数重点镜头,适合愿意花时间、对效果要求高的剪辑同事。

七、几种做法怎么选

  • 作品还在线上、链接有效:耶斯去水印取回原文件,画质零损失。
  • 只剩本地文件、水印是半透明的:黑白标定片反算,残留的零星像素再修补。
  • 水印不透明、完全盖住了画面:反算不了,只能用修补类方法或裁掉边缘。
  • 不想写代码、本地文件不多:剪映裁剪,或者达芬奇处理重点镜头。

八、常见问题

问:自家账号的作品带了平台水印,怎么拿到没水印的原文件?

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

问:作品的封面和配的文案也想一起保存?

答:耶斯去水印的结果页里封面可以单独下载,文案能直接复制,和视频放在一起存档就行。

问:好几十条作品,分几天处理,怎么知道做到哪了?

答:耶斯去水印处理过的链接会留在解析记录里,下次打开就能接着处理,不会重复。

问:反算出来的画面在水印位置有一片杂色?

答:多半是这些像素的透明度太高,或者标定视频和要还原的视频分辨率不一致。确认两者分辨率、水印位置完全相同;透明度高的像素用第五节的 touch_up 修补。

问:标定视频算出的 α 有负数或者大于 1?

答:视频压缩的噪点导致的。取更多帧求中位数可以减小误差;算完用 np.clip 截断到合理范围,第三节的代码已经这样做。

问:水印在不同时间出现在不同位置,标定一次够吗?

答:不够,每个位置要分别标定。标定视频可以做得长一些,让水印在不同位置都出现过,分别截取每个位置的区域计算。

问:还原后的视频在某些播放器里打不开?

答:OpenCV 用 mp4v 写出的视频兼容性一般,按第五节用 ffmpeg 重新编码成 H.264 并接回原声即可。

我的做法

视频去水印方法,我现在按情况分:作品还在线上的,先用耶斯去水印把原文件取回来,画面完全不用动;原片丢了、只剩带半透明水印的本地文件,就用纯黑、纯白两段标定视频算出水印的透明度和颜色,逐帧反算还原,零星还原不了的像素再修补;水印不透明的才退回修补或裁剪。年度合集里那批找不到原片的老视频,用反算法处理后放在一起看,几乎分不出哪些是处理过的。

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