简介这是一套面向开发者与建站者的去水印系统源码主打全开源、所有去水印功能均在本地实现无需依赖第三方接口即可完成解析与处理适合希望自建去水印工具、研究本地解析逻辑或进行二次开发的技术人员参考使用。资源包共165个文件压缩后约21.36MB以76个php文件为核心业务逻辑配合35个json配置与数据文件、26个html页面模板、7个js脚本、6个sql数据库脚本及若干txt说明、css样式与配置文件整体结构完整覆盖前端页面、后端接口与数据存储各环节。内容预览中可见针对抖音、拼多多等平台的分享页与笔记测试页面说明项目已按多平台场景做了模块划分便于按需裁剪与扩展。目前已有50人学习下载读者可从中获取一套可直接部署运行的本地去水印方案理解各平台解析流程、接口组织方式与前后端协作思路并在此基础上替换解析规则、调整页面样式或接入自有业务减少从零搭建的时间成本。1. 去水印系统源码全开源本地跑通这件事比你想的更有价值很多人第一次搜「去水印系统源码/全开源/所有去水印功能在本地实现」其实是被在线工具逼出来的——上传一张图等半天回来一看水印没去干净画质还糊了一层更别提把素材传到别人服务器上那种不踏实感。本地实现的核心价值就一句话图片、视频、PDF 不出本机处理链路自己掌控批量跑也不受在线服务的次数和限速约束。这套方案适合三类人需要批量处理素材的自媒体运营、想二次开发做内部工具的后端工程师、以及拿它当图像修复入门项目的学生。它不承诺「一键完美」但能让你把去水印这件事拆开、调参、复现而不是对着一个黑匣子反复试。2. 去水印到底在做什么从掩膜到修复的完整链路2.1 去水印不是「擦掉」而是「补全」先把认知摆正。水印覆盖的区域原始像素信息已经永久丢失了任何去水印算法做的都是图像修复inpainting——根据水印周围的像素推测被遮挡区域原本长什么样。所以「去水印」这个叫法本身有点误导它更接近「内容感知填充」。这就决定了整个系统的技术骨架只有两大块水印定位找出水印在画面中的位置生成一张掩膜mask白色代表要修复的区域黑色代表保留。图像修复把原图和掩膜一起送进修复算法输出补全后的图像。视频去水印则是把这两步逐帧执行再叠加时域一致性处理否则会出现帧间闪烁。PDF 去水印稍微特殊——如果水印是独立图层或注释对象可以直接删除对象如果水印已经和内容栅格化合并就得走图像修复那条路。理解了这个链路你就明白为什么有些工具「去不干净」要么掩膜没找准要么修复算法对大面积遮挡无能为力。本地实现的好处是这两步你都能看到中间结果能调。2.2 本地实现的技术选型为什么我选 OpenCV PyTorch全开源、本地实现意味着不依赖任何云 API。常见的技术栈组合有这么几种方案定位方式修复方式适用场景依赖体积OpenCV 传统法阈值/形态学cv2.inpaint (Telea/NS)小面积、纯色水印极小OpenCV PyTorch阈值/检测模型LaMa / MAT 深度模型大面积、复杂背景中等纯深度学习端到端检测网络生成网络研究向、需训练大我一般推荐混合方案用 OpenCV 做快速掩膜生成和简单场景兜底用 PyTorch 加载 LaMa 这类开源修复模型处理复杂情况。理由是 LaMaLarge Mask Inpainting对大面积掩膜的修复效果在开源模型里属于第一梯队而且有现成权重可以直接用不需要自己训练。选型时要注意一个容易被忽略的点修复模型对掩膜的质量极其敏感。掩膜多圈了一个像素修复结果就可能把水印边缘的残影也「学」进去掩膜少圈了水印残留直接暴露。所以掩膜生成这一步值得花大力气调。2.3 环境搭建三条命令跑起来本地部署第一步是把环境弄干净。我习惯用 conda 隔离避免和系统里的其他包打架。# 创建独立环境python 3.10 兼容性最稳 conda create -n watermark-removal python3.10 -y conda activate watermark-removal # 安装核心依赖 pip install opencv-python numpy pillow torch torchvision # 如果要用 LaMa额外装它的推理依赖 pip install omegaconf easydict参数说明opencv-python是主库numpy做数组运算pillow处理图片读写torch提供深度模型推理。omegaconf和easydict是 LaMa 推理脚本读取配置用的不装会在加载模型时报ModuleNotFoundError。装完后验证一下import cv2 import torch print(OpenCV:, cv2.__version__) print(PyTorch:, torch.__version__) print(CUDA available:, torch.cuda.is_available())如果CUDA available是FalseCPU 也能跑只是 LaMa 推理一张 1080p 图大概要 3-5 秒GPU 上不到 0.5 秒。批量处理时这个差距会被放大建议有独显就配好 CUDA。3. 掩膜生成去水印成败的八成在这里3.1 用颜色阈值法定位纯色水印大多数平台水印是半透明白色或固定颜色的文字/logo这类水印用颜色空间阈值就能定位得不错。核心思路是把图像转到 HSV 空间因为 HSV 对亮度和颜色的分离比 RGB 更直观。import cv2 import numpy as np def generate_mask_by_color(image_path, lower, upper): 基于 HSV 颜色范围生成水印掩膜 lower/upper: HSV 下界和上界例如白色水印用 [0,0,200]-[180,30,255] img cv2.imread(image_path) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 按颜色范围提取得到初步掩膜 mask cv2.inRange(hsv, np.array(lower), np.array(upper)) # 形态学闭运算把断裂的文字笔画连起来 kernel np.ones((3, 3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) # 膨胀一点确保覆盖水印边缘的半透明过渡带 mask cv2.dilate(mask, kernel, iterations1) return img, mask逻辑说明inRange把落在颜色范围内的像素标为 255其余为 0。闭运算先膨胀后腐蚀能填补文字笔画间的空隙避免掩膜碎成一段一段。最后再膨胀一次是因为半透明水印的边缘像素颜色是渐变的阈值往往抓不全多扩一圈让修复算法有足够上下文。参数怎么调白色水印的 HSV 上界一般设[180, 30, 255]下界[0, 0, 200]。如果水印偏灰把下界的 V 值降到 150 左右。调参时把掩膜可视化出来看比盲猜快得多cv2.imwrite(mask_debug.png, mask)3.2 半透明水印的掩膜修正别让阈值骗了你半透明水印是最常见的坑。它在深色背景上偏白在浅色背景上几乎看不见单一阈值不可能同时抓住两种情况。我的做法是分区域处理 手动框选兜底。先看一个反直觉的现象对半透明水印直接用颜色阈值生成的掩膜往往「中间实、边缘虚」修复后水印中间干净了边缘留一圈鬼影。原因是边缘像素是水印和背景的混合颜色不纯阈值抓不住。解决办法是对掩膜做一次基于梯度的边缘扩展def refine_mask_by_gradient(img, mask, expand5): 在掩膜边缘沿梯度方向扩展覆盖半透明过渡带 expand: 扩展像素数半透明水印建议 3-8 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Sobel 算梯度水印边缘梯度大 grad_x cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize3) grad_y cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize3) grad cv2.magnitude(grad_x, grad_y) grad cv2.normalize(grad, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # 梯度大的地方且靠近原掩膜纳入新掩膜 _, grad_bin cv2.threshold(grad, 40, 255, cv2.THRESH_BINARY) near_mask cv2.dilate(mask, np.ones((expand*21, expand*21), np.uint8)) refined cv2.bitwise_or(mask, cv2.bitwise_and(grad_bin, near_mask)) return refined这段代码的逻辑是只在原掩膜附近near_mask限定范围寻找梯度大的像素把它们补进掩膜。这样既扩展了半透明边缘又不会把画面里无关的强边缘比如物体轮廓误吞进来。expand参数控制搜索范围半透明水印一般 3-8 够用太大容易误伤。3.3 视频去水印的掩膜传播视频如果逐帧独立生成掩膜会出现掩膜抖动修复后画面闪烁。常见做法是只在第一帧精细生成掩膜后续帧用光流跟踪传播。def propagate_mask_via_optical_flow(video_path, first_mask, output_path): 用 Farneback 光流把首帧掩膜传播到后续帧 cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) w int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*mp4v), fps, (w, h)) ret, prev cap.read() mask first_mask.copy() while ret: ret, curr cap.read() if not ret: break # 计算前后帧光流 flow cv2.calcOpticalFlowFarneback( cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY), cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY), None, 0.5, 3, 15, 3, 5, 1.2, 0) # 按光流把掩膜 warp 到当前帧 h_grid, w_grid np.mgrid[0:h, 0:w].astype(np.float32) map_x (h_grid flow[..., 1]).astype(np.float32) map_y (w_grid flow[..., 0]).astype(np.float32) mask cv2.remap(mask, map_y, map_x, cv2.INTER_NEAREST) # 这里把 mask 和 curr 送入修复函数略 prev curr cap.release() writer.release()参数说明calcOpticalFlowFarneback的pyr_scale0.5是金字塔缩放levels3是层数winsize15是窗口大小iterations3是迭代次数。窗口越大对快速运动越鲁棒但越慢。remap用最近邻插值因为掩膜是二值的线性插值会产生灰色边缘。注意光流传播在镜头切换或物体快速运动时会失效掩膜会漂移。工程上一般每隔 30-60 帧重新检测一次掩膜做校正或者检测到光流误差过大时重置。4. 图像修复从 OpenCV 传统法到 LaMa 深度模型4.1 OpenCV inpaint小面积水印的快速兜底OpenCV 自带两个修复算法Telea 和 Navier-Stokes都是基于扩散的传统方法。import cv2 def inpaint_opencv(image_path, mask, methodtelea, radius3): method: telea 或 ns radius: 修复半径越大越平滑但越慢 img cv2.imread(image_path) flag cv2.INPAINT_TELEA if method telea else cv2.INPAINT_NS result cv2.inpaint(img, mask, radius, flag) return result逻辑说明cv2.inpaint会从掩膜边界向内逐层扩散像素值radius决定每次扩散参考的邻域大小。Telea 适合细线条和小区域NS 适合稍大区域但容易产生模糊。参数怎么调radius3是默认值水印笔画细可以降到 2水印块大可以升到 5。但别指望它处理大面积水印——扩散法在大面积上会产生明显的涂抹感因为它是从边缘往里「糊」中间区域完全没有信息依据。适用边界水印面积小于画面 5%、背景纹理简单纯色墙、天空、虚化背景时OpenCV inpaint 效果够用且速度极快。超过这个范围就该上深度模型。4.2 LaMa 模型推理大面积水印的主力LaMa 的核心优势是用了快速傅里叶卷积FFC感受野能覆盖整张图所以对大掩膜也能利用远处信息做合理推测。本地部署 LaMa 的流程是下载开源权重 → 加载模型 → 预处理 → 推理 → 后处理。import torch import cv2 import numpy as np from PIL import Image def inpaint_lama(image_path, mask, model, devicecuda): 用 LaMa 模型修复图像 model: 已加载的 LaMa 模型 img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 预处理归一化到 [0,1] 并转 tensor img_t torch.from_numpy(img).float().permute(2, 0, 1) / 255.0 mask_t torch.from_numpy(mask).float() / 255.0 mask_t (mask_t 0.5).float() # 掩膜为 1 的地方需要修复 img_t img_t.unsqueeze(0).to(device) mask_t mask_t.unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): # LaMa 要求输入尺寸是 8 的倍数 h, w img_t.shape[2], img_t.shape[3] pad_h (8 - h % 8) % 8 pad_w (8 - w % 8) % 8 img_t torch.nn.functional.pad(img_t, (0, pad_w, 0, pad_h), modereflect) mask_t torch.nn.functional.pad(mask_t, (0, pad_w, 0, pad_h), modereflect) output model(img_t, mask_t) output output[:, :, :h, :w] # 后处理转回 numpy 和 BGR result output.squeeze(0).permute(1, 2, 0).cpu().numpy() result (result * 255).clip(0, 255).astype(np.uint8) result cv2.cvtColor(result, cv2.COLOR_RGB2BGR) return result逻辑说明LaMa 的输入是归一化后的图像张量和二值掩膜输出是修复后的图像。关键细节是尺寸必须是 8 的倍数因为模型内部有多次下采样不 pad 会在推理时报维度错误。用reflect模式 pad 而不是补零避免边缘产生黑边影响修复。参数说明mask_t 0.5是把掩膜二值化如果你的掩膜是 0/255除以 255 后正好是 0/1。推理时torch.no_grad()关掉梯度计算省显存。如果显存不够可以把图切成小块分别修复再拼回去但要注意块之间要有重叠否则接缝处会有痕迹。4.3 修复结果的融合别让修复区域「贴」上去深度模型修复出来的区域颜色和周围往往有轻微偏差直接贴回去会看到一块「补丁」。常见做法是在掩膜边缘做羽化融合。def blend_result(original, inpainted, mask, feather7): 在掩膜边缘羽化融合修复结果 feather: 羽化宽度越大过渡越柔和 # 对掩膜做高斯模糊得到 0-1 的权重图 mask_blur cv2.GaussianBlur(mask, (feather*21, feather*21), 0) alpha mask_blur.astype(np.float32) / 255.0 alpha alpha[..., None] # 扩展到三通道 # 加权融合 result original.astype(np.float32) * (1 - alpha) \ inpainted.astype(np.float32) * alpha return result.clip(0, 255).astype(np.uint8)逻辑说明高斯模糊把硬边掩膜变成渐变权重掩膜中心权重接近 1用修复结果边缘权重过渡到 0用原图。这样修复区域和原图之间没有硬接缝。参数说明feather控制过渡带宽度太小接缝还在太大修复区域会被原图「稀释」导致水印残留。一般 5-9 之间根据水印大小调。水印大就调大水印小就调小。5. 避坑与排查本地去水印最容易翻车的五个地方5.1 修复后水印位置出现「鬼影」轮廓现象水印主体去掉了但原水印位置能看到一圈淡淡的轮廓或颜色偏移。原因掩膜没有完全覆盖水印的半透明边缘。半透明水印的边缘像素是水印和背景的混合颜色阈值抓不住修复算法只处理了掩膜内的区域边缘残留就被保留下来。解决用 3.2 节的梯度扩展法把掩膜往外扩 3-8 像素或者在掩膜生成后统一做一次膨胀。判断标准是把掩膜叠加到原图上可视化确保水印的每一个像素包括最淡的边缘都在掩膜内。5.2 LaMa 推理报维度错误现象RuntimeError: The size of tensor a (x) must match the size of tensor b (y)或者下采样时的尺寸不匹配错误。原因输入图像尺寸不是 8 的倍数。LaMa 内部有 3 次下采样每次除以 2所以尺寸必须能被 8 整除。解决按 4.2 节的代码做 pad推理完再裁回原尺寸。注意 pad 用reflect模式不要用默认的补零否则边缘会出现黑边被模型学进修复结果。5.3 视频去水印帧间闪烁现象单帧看没问题连续播放时修复区域忽明忽暗像在闪。原因逐帧独立生成掩膜或独立推理相邻帧的掩膜有微小差异修复结果不一致。解决掩膜用光流传播3.3 节保证帧间掩膜连续。如果掩膜已经稳定但修复结果还闪可以在时域上对修复区域做平滑——把连续 3-5 帧的修复结果做加权平均再输出。代价是快速运动场景会有拖影需要权衡。5.4 批量处理时显存溢出现象处理前几张图正常跑到后面报CUDA out of memory。原因PyTorch 的缓存没有及时释放或者图片尺寸越来越大导致单次推理显存需求累积。解决每处理完一张图手动torch.cuda.empty_cache()或者把推理包在with torch.no_grad():里确保不建计算图。如果单张图就超显存把图切成 512x512 的块分别处理块间重叠 64 像素最后按羽化融合拼回去。5.5 PDF 去水印后文字变模糊现象PDF 里的水印去掉了但正文文字也跟着糊了。原因把整个 PDF 页面栅格化成图片再修复修复算法不分青红皂白把文字区域也「平滑」了。解决先判断水印类型。如果水印是独立的文本对象或图片对象用 PDF 解析库能读出来直接删除对象不动内容层。只有当水印和内容已经栅格化合并时才走图像修复并且掩膜要精确到只覆盖水印不能碰到文字。这一步用 PDF 解析库提取对象树是关键别一上来就栅格化。6. 进阶技巧把去水印做成可复用的本地服务单次跑通脚本只是起点真正提升效率的是把它封装成一个本地 HTTP 服务让其他工具能调用。我用 FastAPI 搭一个最小服务接收图片和掩膜参数返回修复结果。from fastapi import FastAPI, UploadFile, File from fastapi.responses import Response import cv2 import numpy as np app FastAPI() app.post(/remove_watermark) async def remove_watermark( file: UploadFile File(...), lower_v: int 200, expand: int 5 ): # 读取上传的图片 content await file.read() img cv2.imdecode(np.frombuffer(content, np.uint8), cv2.IMREAD_COLOR) # 生成掩膜复用第 3 章的逻辑 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, np.array([0, 0, lower_v]), np.array([180, 30, 255])) kernel np.ones((3, 3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) mask cv2.dilate(mask, kernel, iterationsexpand) # 修复这里用 OpenCV 兜底生产环境换成 LaMa result cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA) # 编码返回 _, buf cv2.imencode(.png, result) return Response(contentbuf.tobytes(), media_typeimage/png)逻辑说明lower_v暴露成参数让调用方根据水印深浅调整阈值。expand控制掩膜膨胀次数。返回用 PNG 避免 JPEG 二次压缩损失。启动命令uvicorn main:app --host 127.0.0.1 --port 8000调用示例curl -X POST http://127.0.0.1:8000/remove_watermark?lower_v180expand6 \ -F filetest.jpg -o result.png参数怎么调lower_v从 200 往下调水印越淡调越低但太低会把浅色背景也吞进掩膜。expand从 5 开始试水印边缘有残留就加修复区域出现明显色块就减。一个我踩过的坑服务并发调用时如果多个请求同时加载 LaMa 模型显存会瞬间翻倍。正确做法是模型在服务启动时加载一次全局复用请求处理时只做推理。用 FastAPI 的lifespan事件在启动时初始化模型别在路由函数里加载。最后说个习惯我每次调完参数都会把掩膜和修复结果并排存一份文件名带上参数值。这样回头对比不同参数的效果时不用重新跑也方便定位是哪组参数出的问题。去水印这件事没有万能参数靠的就是这种笨办法一点点试出适合自己素材的组合。希望帮到你。本文还有配套的精品资源点击获取