最近在做 AI 小镇项目时,遇到一件让人哭笑不得的事:自己调了很久的角色立绘,被人用 AI 一键抠图工具直接抠走,换了个背景就当原创素材使用了。这种事单纯靠“整图对比”很难发现,原因是对方保留的是角色主体,背景已经被替换。
今天这篇文章,我想从技术侧把 AI 立绘、AI 抠图、图像指纹检测、数字水印这几个问题完整拆一遍。不光讲原理,还会给出一个能跑起来的“立绘指纹检测器”,用 pHash 和 ORB 特征匹配来识别“被抠图搬运”的素材,最后再聊聊隐蔽水印和版权保护的最佳实践。
读完之后,你可以掌握:
- AI 立绘从生成到使用的基本 pipeline;
- AI 抠图的底层原理与常见工具;
- 用 pHash 和 ORB 做图像相似度检测;
- 用简化频域水印给立绘加一层隐蔽标识;
- 实际项目中保护原创素材的工程建议。
1. 背景与核心概念
1.1 立绘是什么
立绘,一般指角色的全身像或半身像,在游戏、漫画、视觉小说、虚拟主播等领域非常常见。以前的立绘是画师一张张手绘的,现在很多项目开始用 AI 生成草稿再人工精修,尤其是独立游戏或 AI 交互项目,批量生成角色需求很大。
在 AI 小镇这种项目里,角色头像、角色立绘、场景素材都可能是 AI 生成后人工挑选出来的。一张能用的立绘并不是“点一下生成”就完事,需要不断调整提示词、切换模型、做多次图生图,甚至局部重绘,最后才能得到一张可以放进资源目录的图。
1.2 为什么会被“抠下来一块”
“抠图”本质上是图像分割和抠像,把目标区域从背景里分离出来。现在开源工具很多,U2-Net、RemBg、BackgroundMattingV2,以及各类一键去背景 SDK,都可以把人物或主体从原图中抠出来。如果原图主体清晰、背景相对规整,AI 抠图效果会非常好,最后直接输出一张透明背景的 PNG。
于是问题就来了:对方不需要搬运整张原图,只需要把角色主体“抠”出来,换一个背景、改一下色调,就变成了“新素材”。相比之下,如果对方直接复制整张原图,用感知哈希一查就能发现;但“抠图 + 换背景”会让传统整图相似度判断失效,这也是这个现象让人头疼的核心原因。
1.3 这篇文章解决的三个问题
- 如何理解 AI 立绘的生成成本,以及 AI 抠图为什么高效。
- 如何设计一套算法,在对方已经抠图、换背景的前提下,仍然识别出立绘主体来自同一张原图。
- 如何通过数字水印和发布策略,降低素材被“顺手抠走”的风险。
下面我们进入正题。
2. 环境准备与版本说明
本文代码以 Python 为主,建议环境如下:
- Python 3.8 或以上;
- OpenCV(opencv-python);
- NumPy;
- Pillow(可选,用于处理图片格式);
- DCT 计算直接使用 OpenCV 的
cv2.dct。
安装命令:
pip install opencv-python numpy pillow版本请根据实际环境调整,本文代码重点演示思路,不依赖特定小版本。如果你在 Windows 上使用,建议先在虚拟环境里安装依赖;macOS 或 Linux 同理。
3. AI 立绘生成与抠图原理拆解
3.1 一条典型的 AI 立绘生成 pipeline
现在生成立绘的主流方式,是基于扩散模型的开源或在线服务,比如 Stable Diffusion 等。一条典型链路是:
- 需求分析:确定角色风格、体型、服装、动作、视角。
- 提示词设计:把需求转成正向提示词和负向提示词。
- 模型推理:用文本生成图像模型生成初稿。
- 图生图 / 局部重绘:继续调整构图、面部、细节。
- 超分和修图:放大、去噪、调色。
- 抠图与合成:把立绘抠出来,合成到角色卡、UI、场景中。
这里值得强调的是,能进入“可用素材”阶段的立绘,已经经过多轮调试。外人只看到最终那张图,看不到背后调整了多久。这也是为什么被直接抠图搬运会更让人感到无奈。
3.2 AI 抠图到底做了什么
抠图在技术上分成几个层次:
- 语义分割(Semantic Segmentation):给每个像素打类别标签,比如人、背景、物品。
- 实例分割(Instance Segmentation):在语义分割的基础上,区分同类的不同实例。
- 抠图(Matting):不仅区分前景背景,还计算前景透明度 alpha,适合头发丝、半透明物体等细节。
现在常见的一键去背景工具,很多是基于深度模型的语义分割或抠图网络。比如:
- U2-Net:常用于显著性目标检测和去背景,很多一键抠图工具早期版本都基于 U2-Net。
- BackgroundMattingV2:更偏向视频抠图,适合实时场景。
- MODNet:适合人像快速抠图。
流程上,把图片输入模型,模型输出一个 alpha 遮罩,然后用 alpha 遮罩和原图合成透明图。对于主体清晰、边界明显的立绘,这个流程非常成熟,甚至不需要手动修正。
3.3 为什么“换背景”难以用整图比较查出来
如果对方只是整体复制,用 pHash、aHash 等感知哈希算法很容易发现。但对方先抠图、再合成到新背景,整张图的像素分布已经完全不同了,简单哈希会失效。
所以我们需要借助局部特征。立绘上的人脸、服饰轮廓、装饰元素、武器等区域,会被提取成大量局部特征点。即便背景被替换,角色主体上的特征点依然存在。用 ORB、SIFT 这类特征描述子做匹配,仍然能得到较高的匹配数量。
4. 识别搬运:图像指纹与相似度检测
4.1 感知哈希 pHash 的原理
pHash(Perceptual Hash)是一种对图像内容进行“摘要”的算法。它不像 MD5 那样对文件字节敏感,两张视觉上几乎一样的图,即使格式不同、压缩率不同、尺寸不同,pHash 也会很接近。
经典 pHash 的做法:
- 把图像缩放到固定尺寸,比如 32x32。
- 转为灰度图。
- 做 DCT(离散余弦变换)。
- 取左上角 8x8 的低频系数区域。
- 计算这 64 个系数的中位数。
- 每个系数大于中位数记为 1,小于中位数记为 0,得到 64 位特征串。
- 比较两个特征串的汉明距离,距离越小越相似。
这类方法对缩放、亮度变化、轻微压缩有较好的鲁棒性,适合判断“两张图是不是同一张图”。
4.2 ORB 特征匹配的原理
ORB(Oriented FAST and Rotated BRIEF)是一种快速局部特征提取算法。它的特点:
- 速度比 SIFT/SURF 快很多;
- 对旋转和尺度变化有一定鲁棒性;
- 适合在实时场景和工程中使用;
- 不需要额外安装 OpenCV contrib 模块。
ORB 先在图像中检测关键点,再为每个关键点计算二值描述子。两张图比较时,用汉明距离做最近邻匹配,再通过比例测试筛选可靠的匹配点。
当立绘被抠出来并换到新背景后,新背景新增的特征点不会影响原区域特征点的匹配,角色主体上的特征点仍然能在两张图中对应上。
4.3 匹配数量与阈值
ORB 匹配的“好匹配数量”取决于图像内容、清晰度和主体占比。通常可以设一个经验阈值,例如:
- 匹配数少于 15:基本不相似;
- 15 到 60:有一定相似,建议人工复核;
- 大于 60:很可能是同源素材。
但阈值不能一概而论,复杂立绘的特征点远多于简单立绘。实际项目里,建议先用一批已知正负样本标定阈值,再投入自动化流程。
5. 实战:立绘指纹检测器
下面实现一个简单的 Python 小工具,包含 pHash 和 ORB 两种检测方式。
5.1 项目结构
image_fingerprint/ ├── fingerprint.py # 核心算法 ├── detector.py # 命令行入口 ├── assets/ │ ├── original.png # 原始立绘 │ ├── stolen_new_bg.png # 被抠出来换背景的图 │ └── other.png # 无关的另一张图5.2 fingerprint.py:核心算法
# 文件路径:image_fingerprint/fingerprint.py import cv2 import numpy as np def read_gray(image_path): img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f"无法读取图片: {image_path}") return img def read_rgb_with_white_bg(image_path): """ 读取图片。如果是透明 PNG,先把透明背景合成到白色画布上, 避免透明区域在灰度图中变成黑色,干扰后续特征提取。 """ img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) if img is None: raise ValueError(f"无法读取图片: {image_path}") if len(img.shape) == 2: return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) if img.shape[2] == 4: alpha = img[:, :, 3] / 255.0 rgb = img[:, :, :3] white = np.ones_like(rgb) * 255 merged = rgb * alpha[..., None] + white * (1 - alpha[..., None]) return merged.astype(np.uint8) return img[:, :, :3] def phash(image_path, resize_size=32, low_size=8): """计算 pHash,返回 64 位二值数组""" img = read_gray(image_path) img = cv2.resize(img, (resize_size, resize_size)) img = np.float32(img) dct = cv2.dct(img) dct_low = dct[:low_size, :low_size] med = np.median(dct_low) hash_bits = (dct_low > med).flatten().astype(np.uint8) return hash_bits def hamming_distance(hash1, hash2): """计算两个哈希数组的汉明距离""" return int(np.count_nonzero(hash1 != hash2)) def phash_similarity(image_path1, image_path2): """ 返回相似距离,0 表示最相似,64 表示完全不同。 一般 10 以内可以认为是同一张图或高度相似。 """ h1 = phash(image_path1) h2 = phash(image_path2) return hamming_distance(h1, h2) def orb_features(image_path, max_features=1000): """提取 ORB 特征点与描述子""" img = read_gray(image_path) orb = cv2.ORB_create(nfeatures=max_features) keypoints, descriptors = orb.detectAndCompute(img, None) return keypoints, descriptors def orb_match_score(image_path1, image_path2, ratio=0.75): """返回经过比例测试后的匹配点数量""" _, des1 = orb_features(image_path1) _, des2 = orb_features(image_path2) if des1 is None or des2 is None: return 0 bf = cv2.BFMatcher(cv2.NORM_HAMMING) matches = bf.knnMatch(des1, des2, k=2) good = [] for m, n in matches: if m.distance < ratio * n.distance: good.append(m) return len(good)这里需要注意:
cv2.dct要求输入是浮点数,所以要先将灰度图转成np.float32。- pHash 的经典做法是缩小到 32x32,再取 8x8 低频区,所以
resize_size不能小于low_size。 BFMatcher.knnMatch返回的每个匹配对包含最近邻和次近邻,最近邻距离明显小于次近邻时,才认为是可靠匹配,这就是 Lowe 比例测试的基本思路。
5.3 detector.py:命令行入口
# 文件路径:image_fingerprint/detector.py import sys from fingerprint import phash_similarity, orb_match_score def main(): if len(sys.argv) != 3: print("用法: python detector.py <图片A> <图片B>") sys.exit(1) img_a = sys.argv[1] img_b = sys.argv[2] phash_dist = phash_similarity(img_a, img_b) orb_score = orb_match_score(img_a, img_b) print(f"pHash 汉明距离: {phash_dist} (0~64,越小越相似)") print(f"ORB 匹配点数: {orb_score}") if phash_dist <= 10: print("结论: 很可能是同一张/高度相似的图片") elif orb_score >= 60: print("结论: 局部特征高度相似,疑似从同一立绘抠图或裁剪") elif orb_score >= 15: print("结论: 存在一定相似特征,建议人工复核") else: print("结论: 相似度较低,大概率不是同一素材") if __name__ == "__main__": main()5.4 运行与验证
假设我们准备了三张图:
assets/original.png:原始立绘;assets/stolen_new_bg.png:把立绘主体抠出来,换到新背景后的图;assets/other.png:完全无关的另一张角色图。
执行:
cd image_fingerprint python detector.py assets/original.png assets/stolen_new_bg.png预期输出类似:
pHash 汉明距离: 28 ORB 匹配点数: 86 结论: 局部特征高度相似,疑似从同一立绘抠图或裁剪再执行:
python detector.py assets/original.png assets/other.png预期输出类似:
pHash 汉明距离: 42 ORB 匹配点数: 5 结论: 相似度较低,大概率不是同一素材这里的数字只是演示,实际数字会因图片内容不同而变化。但趋势是一致的:换背景后 pHash 距离升高,但 ORB 匹配数仍然很高,说明局部特征匹配可以有效识别“抠图 + 换背景”这类搬运行为。
5.5 结果说明
这个实战案例告诉我们:只靠整图哈希无法识别“抠图 + 换背景”,必须引入局部特征匹配。
如果你想做一个自动化监测工具,可以在这个基础之上扩展:
- 维护一个正版素材库,预先计算每个素材的 ORB 描述子;
- 对目标图片和库中素材跑 ORB 匹配;
- 匹配数超过阈值时告警并保存截图;
- 定时巡检,或接入上传接口做实时检测。
6. 进阶:给立绘加一道数字水印
图像指纹属于“被动检测”,是事情发生后的比对。如果你希望在源头上增加追踪能力,可以使用数字水印。
6.1 可见水印 vs 隐蔽水印
- 可见水印:直接在图上叠加半透明文字或 logo。优点是直观,缺点是影响观感,而且现在部分 AI 工具可以自动去除。
- 隐蔽水印:把一段信息隐藏在图像的频率域或像素噪声中,肉眼看不出来,但通过程序可以提取验证。
隐蔽水印的常见方案包括频域水印(DCT/DWT)、空域水印、基于神经网络的水印等。
6.2 简化频域水印示例
下面给一个简化版 DCT 水印思路,演示“如何把一段 ID 嵌入图像”。
核心做法:
- 把图像从 RGB 转为 YCrCb,取 Y 通道。
- 对 Y 通道做 DCT。
- 在 DCT 系数中挑出一批位置,根据水印比特是 0 还是 1,把系数调整到对应奇偶性。
- 逆 DCT,得到含水印的 Y 通道。
提取时,再次 DCT,读取这批位置的奇偶性,就能还原水印。
# 文件路径:image_fingerprint/watermark.py import cv2 import numpy as np WATERMARK_LEN = 32 def _get_indices(): """选择低频区域中的位置,避开 DC 分量""" indices = [] for i in range(1, 9): for j in range(0, 9 - i): indices.append((i, j)) if len(indices) >= WATERMARK_LEN: return indices return indices def embed_watermark(image_path, output_path, watermark_bits): """把 32 位水印嵌入图像,输出到 output_path""" img = cv2.imread(image_path) if img is None: raise ValueError(f"无法读取图片: {image_path}") ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) y, cr, cb = cv2.split(ycrcb) y_float = np.float32(y) dct = cv2.dct(y_float) indices = _get_indices() strength = 8.0 for idx, bit in enumerate(watermark_bits): x, y_pos = indices[idx] val = dct[y_pos, x] base = round(val / strength) if bit == 1 and base % 2 == 0: base += 1 if bit == 0 and base % 2 == 1: base += 1 dct[y_pos, x] = base * strength y_watermarked = cv2.idct(dct) y_watermarked = np.uint8(np.clip(y_watermarked, 0, 255)) merged = cv2.merge([y_watermarked, cr, cb]) result = cv2.cvtColor(merged, cv2.COLOR_YCrCb2BGR) cv2.imwrite(output_path, result) def extract_watermark(image_path): """提取 32 位水印""" img = cv2.imread(image_path) if img is None: raise ValueError(f"无法读取图片: {image_path}") ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) y, _, _ = cv2.split(ycrcb) y_float = np.float32(y) dct = cv2.dct(y_float) indices = _get_indices() strength = 8.0 bits = [] for x, y_pos in indices: val = dct[y_pos, x] base = round(val / strength) bits.append(1 if base % 2 == 1 else 0) return bits这段代码只是演示思路,强度、位置选取、JPEG 压缩鲁棒性都需要按实际项目调整。如果要在真实场景中使用,建议考虑:
- 用 DWT 代替全局 DCT,抗压缩能力更强;
- 水印信息加纠错编码,提升提取成功率;
- 嵌入强度不能过高,否则图像质量下降明显。
需要强调的是,水印不能替代法律和平台规则保护,但能提高追踪能力。
7. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| pHash 汉明距离很高,但其实是同一张图 | 图片被大幅裁剪、旋转或加了强滤镜 | 改用 ORB 等局部特征匹配 |
| ORB 匹配点数很少,但视觉上很像 | 图片压缩严重,或主体在图中占比过小 | 先对主体区域做 ROI 裁剪,再做 ORB 匹配 |
| 透明背景 PNG 直接比较失败 | 透明区域在灰度图中变成黑色,干扰特征 | 读取时先合成到白色背景,再参与计算 |
| 水印提取失败 | 图片被二次压缩、裁剪或缩放 | 加入纠错编码,提高嵌入强度,或改用 DWT 水印 |
| 待检测图尺寸过大,程序很慢 | 没有预处理 | 先缩放到合理尺寸,再做特征检测 |
| 匹配阈值无法通用 | 不同图片内容复杂度差异大 | 用自定义数据集标定阈值,而不是用固定阈值 |
这里特别想强调透明背景 PNG 的坑。很多立绘会存成透明底,直接用 OpenCV 读取时,透明区域会变成黑色。如果拿这张图跟原图比,背景差异会拉高 pHash 距离。所以我在fingerprint.py里额外提供了read_rgb_with_white_bg,目的就是把透明背景合成到白色画布上,再参与特征提取。
8. 最佳实践:AI 绘画作品的版权保护
最后从工程和内容层面,给出一些实际建议。
第一,生成过程留档。在生成立绘时,建议记录下模型、prompt、seed、原图、修改记录。这样做不是为了发文章,而是将来发生争议时,你可以提供完整的创作链路证明。
第二,发布策略分层。公开渠道尽量发布带可见水印或低分辨率的预览图,高清原图在确认授权后再提供给合作方。很多“顺手抠图”事件,就是因为公开渠道直接放了高清无码图,降低了搬运成本。
第三,建立素材指纹库。用本文的 pHash + ORB 思路,把已经发布的素材全部计算好指纹。如果有平台疑似搬运,可以对截图或下载图做批量比对。频率不用太高,每周或每月一次即可。
第四,隐蔽水印作为追踪手段。在正式对外发布前,对预览图嵌入不可见水印,记录批次、账号、时间。水印写入可以作为独立服务,配合发布流程使用。
第五,维权时保持理性。先通过平台投诉、邮件联系等正常渠道沟通,保留证据截图。如果涉及商业利益,建议咨询专业法律人士。技术检测只能证明相似,最终判定还需要结合创作过程记录和平台规则。
9. 总结与学习路线
本文从“AI 立绘被直接抠走”的常见场景出发,梳理了三条线:
- 立绘生成链路:理解一张可用立绘的产出成本;
- 抠图技术原理:理解为什么一键抠图如此高效;
- 反搬运检测:掌握 pHash、ORB 和频域水印的基本实现,并得到一个可运行的检测小工具。
如果继续深入,可以从这几个方向进阶:
- 用 DWT 做更抗压缩的频域水印;
- 用深度学习做图像向量化检索,对遮挡、裁剪更鲁棒;
- 把指纹库放到服务端,做成 API 或定时巡检任务;
- 研究 AI 生成图像的来源追踪,例如生成模型的隐式指纹。
技术层面的“防搬运”从来不是一劳永逸的,但记录、指纹、水印、留痕这套组合拳,能让搬运成本大幅提高,也能在争议发生时保留关键证据。如果你也在做 AI 绘画或素材管理工作,建议先用文中的小工具给现有素材做一次指纹归档,后续排查会省很多力气。