AI深度伪造图片标注与校验:从可见标签到数字水印的工程实践
2026/8/27 6:58:18 网站建设 项目流程

AI 深度伪造(Deepfake)内容已经不只是技术圈讨论的概念,它正在成为内容平台、新闻媒体和审核系统必须处理的现实材料。生成一张以假乱真的人像图片、一段换脸视频或者一段语音克隆,现在的工具门槛已经低到普通用户都可以操作。内容量上来之后,真正有挑战的不是“能不能识别”,而是“如何低成本地告诉所有人:这是 AI 生成的”。标注 AI 深度伪造,考验的是工程能力:在内容生成或发布链路上,给图片、视频、音频附加可读取、可校验的 AI 生成标识。这篇文章以 AI 生成图片为例,从概念、技术路径到代码实现和排查方法,完整走一遍标注与校验流程。

1. 为什么 AI 深度伪造必须被标注

1.1 深度伪造已经从“肉眼可辨”进入“规模生产”阶段

深度伪造指通过生成模型生成或篡改的图像、视频、语音内容,常见技术包括 GAN 换脸、扩散模型文生图、语音克隆等。早期伪造内容往往存在五官变形、边缘闪烁、光影不自然等问题,靠肉眼和经验还能判断。但近两年的生成模型在分辨率、语义一致性、人脸细节上都提升明显,再加上开源工具和在线服务的大量出现,伪造内容的生产成本已经降到极低。

问题的关键不在单张图片有多逼真,而在规模。一个人可以在一小时内生成成百上千张符合特定描述的图片,审核人员不可能逐张判断。如果这些内容流入新闻配图、商品宣传、聊天记录截图等场景,就可能造成误导、欺诈甚至声誉损害。因此,AI 生成内容需要被“提前标注”,而不是等出事之后再靠人工回溯。

1.2 标注不是口号,而是要为机器设计

标注的英文常用词是 labeling,含义是在生成内容上附加一段可识别信息。这个信息必须满足两个基本要求:第一,人能看懂,或者系统能读出;第二,信息在内容传播过程中尽量保留下来。

实际项目中,标注不是给某一张图贴一个“AI生成”文字那么简单,而是要设计成一条流水线。比如上传接口收到图片后,立即写入元数据;发布接口发布前,校验标注是否存在;对外输出时,叠加可见水印;内容进入素材库后,定时抽样校验标记是否完整。这些动作都要求标注信息是结构化的、机器可读的,而不是一段随意写在图片角落的文字。

1.3 标注和检测是互补关系,不能混为一谈

很多人会把“标注 AI 内容”和“检测 AI 内容”当成一回事,实际上它们是两个方向。

标注是主动行为。内容生成方或发布方在源头写入标识,可信度高,执行成本低,但依赖执行方配合。检测是被动行为。审核系统对已经发布、已经丢失标识的内容做识别,不依赖源头,但存在误判、漏判和对抗风险。

一条完整的内容治理链路通常是:生成时自动标注,发布时校验标注,之后再用检测模型兜底。只做标注,无法覆盖恶意剥离标识的内容;只做检测,又无法处理海量内容的成本问题。两者配合,才能形成闭环。

2. 标注一张 AI 图片有哪几条技术路径

2.1 可见标注:让观看者第一眼就能看到

最直观的标注方式是在图片上叠加文字或图标,例如角落位置的“AI GENERATED”“AI 生成”标签。实现成本低,普通图像库就能完成,也不需要额外的解析逻辑。它的局限也很明显:容易被裁切、被遮挡,也影响画面美感。对新闻图片、素材库这类需要保留画面完整性的场景,可见标注通常只作为辅助手段。

2.2 元数据标注:让程序能读到结构化信息

图片文件内部可以携带结构化元数据,例如 JPEG 的 EXIF、PNG 的文本块、通用 XMP。标注时可以写入“这张图由哪个模型生成、什么时间生成、由谁发布”等信息。程序读取元数据后可以做自动归档、风险分级、来源追踪。常见的内容来源与真实性标准 C2PA 就是沿着这条思路设计的,它把“谁生成、怎么生成、是否被编辑”等信息加密写入文件。

元数据的问题是容易被剥离。只要对图片做一次重新保存、转码、截图,很多元数据就会丢失。这也是为什么它不能单独作为防伪手段,而需要与水印和可见标注组合使用。

2.3 数字水印:把标识藏进内容本身

数字水印通过在像素、频域或压缩域中嵌入一段隐藏信息,使标注不依赖文件外壳。脆弱水印用于完整性校验,内容被篡改时水印会失效;鲁棒水印用于在压缩、缩放、裁剪后仍能提取出隐藏信息。后者实现复杂,通常需要把水印嵌入到 DCT、DWT 等变换域中,对版权追踪、证据留存更有价值。

2.4 检测模型:作为事后兜底

如果内容在发布前没有标注,或者标注被有意剥离,就需要检测模型兜底。检测模型把图片、视频帧或音频片段作为输入,输出“真实”或“伪造”的概率。视频类深伪检测还会关注帧间人脸特征是否一致、眨眼频率是否异常、声音与口型是否同步等。这类方案依赖训练数据和 GPU 资源,并且对抗样本可能让模型失效,所以通常只作为审核链路中的一道关卡,而不是唯一依据。

技术路径人眼可读机器可读抗剥离能力实现成本典型场景
可见标注需 OCR社交平台、素材库
元数据标注媒体出版、内容凭证
数字水印中到高版权追踪、证据留存
检测模型不依赖源头标注事后审核、平台巡查

3. 环境准备和工程目录

3.1 依赖清单

接下来用 Python 实现一个最小演示:给一张测试图片叠加可见标注、写入元数据、嵌入 LSB 水印,最后用独立脚本校验。所需依赖很少。

软件或库版本建议用途
Python3.9 及以上运行环境
Pillow9.0 及以上图像读取、绘制、元数据写入
numpy1.24 及以上像素级水印计算

安装命令:

pip install pillow numpy

如果后续要接入检测模型,再按模型框架安装 torch、transformers 等依赖。本文先不引入,避免把演示流程复杂化。

3.2 工程目录结构

建议按下面的结构组织代码和产物:

deepfake_labeling/ ├── requirements.txt ├── fake_image/ # 待标注的输入图片 ├── labeled_output/ # 标注后的输出图片 ├── label.py # 标注流水线脚本 ├── label_utils.py # 标注工具函数 └── verify.py # 校验脚本

实际项目里,输入图片不会放在本地目录,而是来自上传接口、对象存储或消息队列。这里的目录结构只用于演示,核心逻辑和部署方式解耦后,可以平滑迁移到服务端。

3.3 需要掌握的基础知识点

阅读后面代码前,建议先确认几个基础点:

  • 位运算:value & 0xFE表示把最低位清零,value | 1表示把最低位置为 1,这是 LSB 水印的基础。
  • 图像数组:Pillow 读取图片后用 numpy 转换,得到形状为(高, 宽, 通道数)的三维数组。
  • 图片格式差异:PNG 采用无损压缩,适合保存 LSB 水印;JPEG 是有损压缩,会破坏像素最低位,水印提取会失败。

4. 实现一个最小标注和校验流程

4.1 先生成一张测试图片

为了让演示不依赖外部素材,先写个小函数生成一张带渐变色和简单图形的图片。

from PIL import Image, ImageDraw def create_test_image(path: str, width: int = 640, height: int = 360) -> None: img = Image.new("RGB", (width, height), (28, 36, 52)) draw = ImageDraw.Draw(img) for y in range(height): color = ( 20 + int(y / height * 60), 30 + int(y / height * 40), 50 + int(y / height * 80), ) draw.line([(0, y), (width, y)], fill=color) draw.rectangle([30, 30, 180, 120], outline=(200, 200, 200), width=2) draw.ellipse([220, 30, 320, 130], outline=(180, 220, 255), width=2) img.save(path, quality=92) if __name__ == "__main__": create_test_image("fake_image/demo.png")

这段代码用逐行渐变模拟一张内容简单的“图片素材”,后续所有标注操作都基于这张图。生成后可以用以下命令确认文件存在:

python -c "from PIL import Image; img=Image.open('fake_image/demo.png'); print(img.format, img.mode, img.size)"

预期输出类似:PNG RGB (640, 360)

4.2 叠加可见标注

可见标注使用半透明黑底加白字,保证在浅色背景上也能看清。

from PIL import Image, ImageDraw, ImageFont def add_visible_label(input_path: str, output_path: str, text: str = "AI GENERATED") -> None: img = Image.open(input_path).convert("RGB") overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) try: font = ImageFont.truetype("DejaVuSans-Bold.ttf", 28) except OSError: font = ImageFont.load_default() bbox = draw.textbbox((0, 0), text, font=font) text_w = bbox[2] - bbox[0] text_h = bbox[3] - bbox[1] margin = 12 bar_box = [10, 10, 10 + margin * 2 + text_w, 10 + margin * 2 + text_h] draw.rounded_rectangle(bar_box, radius=6, fill=(0, 0, 0, 160)) draw.text((10 + margin, 10 + margin), text, font=font, fill=(255, 255, 255, 255)) result = Image.alpha_composite(img.convert("RGBA"), overlay) result.convert("RGB").save(output_path, quality=92)

把标签画在独立的 RGBA 图层上,再用alpha_composite合成,是为了让文字和背景之间不出现生硬的覆盖边缘。文件路径里的字体名需要按系统调整,Windows 上可以换成arialbd.ttf,macOS 上可以换成Arial Bold.ttf,找不到字体时回退到默认字体。

4.3 写入 PNG 元数据

PNG 文件支持 tEXt 文本块,Pillow 通过PngInfo写入,读取时会出现在img.info中。

from PIL import Image from PIL.PngImagePlugin import PngInfo def add_metadata(input_path: str, output_path: str, fields: dict) -> None: img = Image.open(input_path).convert("RGB") metadata = PngInfo() for key, value in fields.items(): metadata.add_text(key, str(value)) img.save(output_path, format="PNG", pnginfo=metadata)

这里要注意:JPEG 不支持pnginfo参数。如果目标格式是 JPEG,需要走 EXIF 或者 XMP 方案,例如先写入 EXIF UserComment,再通过img.save(path, exif=exif_bytes)保存。演示统一使用 PNG,可以减少格式差异带来的干扰。

4.4 嵌入一个简单的 LSB 水印

LSB 水印把隐藏信息写入像素值的最低位。人眼对 RGB 颜色最低位的差异几乎无感知,但程序可以用位运算精确提取。

import numpy as np from PIL import Image END_MARKER = "|END" def _text_to_bits(text: str) -> np.ndarray: raw = text.encode("utf-8") return np.unpackbits(np.frombuffer(raw, dtype=np.uint8)) def embed_watermark(input_path: str, output_path: str, secret: str) -> None: img = Image.open(input_path).convert("RGB") arr = np.array(img).astype(np.uint8) payload = np.concatenate([_text_to_bits(secret), _text_to_bits(END_MARKER)]) flat = arr.reshape(-1) if len(payload) > len(flat): raise ValueError("secret is too long for this image") for i in range(len(payload)): flat[i] = (flat[i] & 0xFE) | payload[i] height, width = arr.shape[0], arr.shape[1] Image.fromarray(flat.reshape(height, width, 3)).save(output_path) def extract_watermark(input_path: str) -> str: img = Image.open(input_path).convert("RGB") flat = np.array(img).reshape(-1) bits = flat & 1 byte_count = len(bits) // 8 raw = np.packbits(bits[: byte_count * 8]).tobytes() marker = END_MARKER.encode("utf-8") if marker in raw: raw = raw.split(marker)[0] return raw.decode("utf-8", errors="replace")

关键点有三个。第一,容量公式是“图片宽 x 高 x 通道数”,一张 640 x 360 的 RGB 图片可以存放 640 x 360 x 3 = 691200 个比特,约 86 KB。第二,& 0xFE先清零最低位,再和待嵌入比特做或运算,保证每次只修改一个比特。第三,结尾标记|END用于告诉提取程序“水印内容到这里结束”,避免把后面的随机像素噪声也当作水印。

注意:这种 LSB 水印是教学级实现。图片一旦经过 JPEG 压缩、缩放或裁剪,最低位信息就会被破坏,水印会丢失。生产环境需要把水印嵌入到变换域,例如 DCT 系数中,才能获得更好的鲁棒性。

4.5 把三步串成一条标注流水线

把可见标注、元数据、水印组合起来,写成一个完整的label.py脚本:

from label_utils import add_visible_label, add_metadata, embed_watermark input_path = "fake_image/demo.png" visible_path = "labeled_output/demo_visible.png" meta_path = "labeled_output/demo_meta.png" final_path = "labeled_output/demo_final.png" add_visible_label(input_path, visible_path, "AI GENERATED") add_metadata( meta_path, final_path, { "ai_generated": "true", "generator_model": "diffusion-demo-v2", "generated_at": "2025-06-01T10:00:00+08:00", "operator": "content-team", "content_id": "IMG-20250601-001", }, )

这里先写一个单独的label_utils.py,把上面的add_visible_labeladd_metadataembed_watermark放进去,label.py只负责编排。顺序是按照“可见标注 -> 元数据 -> 水印”执行,最终文件是 PNG,三种标识同时存在。实际项目中,顺序会影响最终效果:先做可见标注再做水印,可以保证水印嵌入的是最终对外可见的画面;先写元数据再做水印,可以避免水印修改像素时影响元数据内容。

4.6 编写独立校验脚本

校验脚本应该能回答三个问题:图片是否包含可见标注、元数据是否存在、水印能否提取出来。

from PIL import Image from label_utils import extract_watermark def verify(path: str) -> dict: img = Image.open(path) result = { "format": img.format, "mode": img.mode, "size": img.size, "metadata": img.info, "watermark": None, } try: result["watermark"] = extract_watermark(path) except Exception as exc: result["watermark_error"] = str(exc) return result if __name__ == "__main__": import json result = verify("labeled_output/demo_final.png") print(json.dumps(result, ensure_ascii=False, indent=2))

正常运行时,输出片段类似:

{ "format": "PNG", "mode": "RGB", "size": [640, 360], "metadata": { "ai_generated": "true", "generator_model": "diffusion-demo-v2", "generated_at": "2025-06-01T10:00:00+08:00", "operator": "content-team", "content_id": "IMG-20250601-001" }, "watermark": "AI:demo:IMG-20250601-001" }

如果三个环节都正常,说明这张图片同时具备人眼可见标注、机器可读元数据和像素内嵌水印。接下来可以人为破坏文件,例如用 Python 缩放图片或另存为 JPEG,再跑一次校验,观察水印和元数据的变化,这会直接加深对三条路径优缺点的理解。

5. 关键参数与实现细节

5.1 可见标注的位置、字号和对比度

可见标注不是随便贴一行字,参数会影响最终效果。

位置建议固定在左上角或右上角,因为主流社交平台裁图时通常优先保留中心区域,角落信息相对容易被裁掉。这里存在一个取舍:角落容易被裁掉,所以位置要结合平台具体行为测试后再定。

字号建议和图片宽度挂钩,而不是写死。一张 360 像素宽的小图和一张 4096 像素宽的大图,如果都用 28 号字,小图里文字会占掉很大面积。可按max(16, int(width * 0.06))计算。背景半透明色建议使用(0, 0, 0, 120)(0, 0, 0, 180)之间的 alpha 值,文字使用纯白(255, 255, 255, 255),保证绝大多数背景色下都有对比度。

5.2 元数据字段怎么设计

元数据字段建议先统一约定,再接入流水线。本文示例使用如下字段:

字段名含义示例值
ai_generated是否为 AI 生成true
generator_model生成模型标识diffusion-demo-v2
generated_at生成时间,使用 ISO 86012025-06-01T10:00:00+08:00
operator操作者或部门content-team
content_id内容唯一标识IMG-20250601-001

字段命名要简洁、语义明确,方便下游系统读取。生成时间建议统一使用带时区的 ISO 8601 格式,避免不同时区出现歧义。content_id要和业务系统中的素材 ID 对应,否则即使读到元数据,也无法关联到具体生成任务。字段中不要写入隐私信息、密钥或内部接口地址,因为元数据在大多数情况下是明文可读的。

5.3 LSB 水印的容量和鲁棒性权衡

LSB 水印的容量取决于像素总数,修改哪个位平面取决于对画质的要求。

位平面视觉影响640 x 360 x 3 容量抗压缩能力
第 0 位(最低位)几乎无感知691200 bit,约 86 KB
第 1 位轻微颗粒感691200 bit
第 5 位明显噪点691200 bit

容量和位平面没有直接关系,但位平面越靠高位,改动对画面影响越大,同时更不容易被压缩彻底抹掉。实际选型时,不要求水印容量很大,因为通常只需要嵌入几十字节的标识信息。更重要的指标是鲁棒性:图片经过二次编码后,水印是否还能提取。教学演示用第 0 位,生产环境建议结合 DCT 或 DWT 方案实现。

5.4 学习环境和生产环境的核心差异

本地演示跑通后,进入生产环境至少要补齐以下内容:

  • 服务化:标注逻辑封装成独立服务,通过 HTTP 或消息队列接收图片,而不是由业务代码直接调用函数。
  • 密钥管理:水印密钥、元数据签名密钥要放在密钥管理服务中,不能写死在代码或配置里。
  • 日志审计:每次标注要记录输入图片哈希、输出图片哈希、操作时间、操作者、字段内容。
  • 多格式支持:PNG 演示简单,但线上大量内容来自 JPEG、WebP 和视频,需要为每种格式设计不同的元数据写入方案。
  • 监控告警:标注成功率、校验失败率、处理耗时都要埋点。校验失败率突然上升,往往说明平台转码策略发生了变化。
  • 回滚机制:如果标注服务导致线上图片裂图或转码异常,要能快速切换回不标注的版本。

6. 验证与评估

6.1 功能验证流程

跑通功能只是第一步,验证要看“正常路径”和“异常路径”两条线。

正常路径验证步骤:

  1. 执行label.py,检查labeled_output目录下是否生成三个中间产物。
  2. 用图片查看器打开最终图片,确认左上角出现“AI GENERATED”文字。
  3. 执行verify.py,确认元数据五个字段完整、水印提取成功。
  4. exiftool或 Pillow 单独读取元数据,和verify.py输出对比,确认不是程序内部自己骗自己。

异常路径验证步骤:

  1. 把最终图片另存为 JPEG,再执行校验,观察水印是否丢失。
  2. img.resize((320, 180))缩放图片,再执行校验,观察元数据是否还在。
  3. 用截图工具截取图片中心区域,再执行校验,观察可见标注、元数据、水印各剩哪些。

这组实验能直观展示三种标注方式的适用范围:可见标注会被裁掉,元数据会在转码时丢失,LSB 水印对缩放敏感。理解这些边界,比背结论更重要。

6.2 端到端指标

如果是接入真实业务,建议从以下指标评估:

指标含义备注
标注成功率成功写入标注的图片比例目标接近 100%
校验通过率发布前校验通过的图片比例低于 99% 说明链路有断点
水印提取准确率提取内容与原始内容一致的比例不含误报统计
压缩后提取率经过平台压缩后仍能提取的比例生产环境重点指标
可见标注 OCR 识别率OCR 能否识别画面上的标注文字用于自动化审核

评估数据要来自线上真实图片分布,不要只用测试图片。不同来源的图片尺寸、格式、压缩质量差异很大,只在一类样本上达标没有说服力。

7. 常见问题和排查链路

7.1 排查顺序

遇到标注不生效或校验失败的问题,按下面的顺序定位,不要先怀疑水印算法本身:

  1. 输入文件是否真的是指定格式,扩展名和实际格式是否一致。
  2. 图片颜色模式是不是 RGB 或 RGBA,灰度图或调色板图要提前转换。
  3. 输出格式是否支持当前标注手段,PNG 保存 PngInfo 没问题,JPEG 会失败。
  4. 依赖版本是否匹配,Pillow 不同版本的textbboxrounded_rectangle行为有差异。
  5. 字体文件路径是否存在,找不到字体时是否走了默认字体。
  6. 下游平台是否对图片做过转码、压缩、裁剪,导致标注信息消失。
  7. 看日志,确认标注脚本实际执行到了哪一步,有没有异常被吞掉。

7.2 典型问题速查表

问题现象常见原因检查方式处理建议
PNG 元数据读取不到用 JPEG 保存,PngInfo 被丢弃检查输出文件扩展名和img.format统一输出 PNG,或改用 EXIF/XMP
水印提取出乱码图片经过 JPEG 压缩或缩放对原图和压缩图分别提取比对保存 PNG,或换用频域鲁棒水印
可见标注看不清文字颜色与背景接近人工查看图片对比度增加半透明黑底或描边
校验脚本报 shape 错误图片不是 RGB,例如带透明通道打印img.mode统一执行convert("RGB")
字体不生效系统路径没有指定字体打印字体加载分支提供字体配置项并做回退
下游平台标注消失平台转码剥离元数据和像素低位从平台下载后再校验在发布和下载链路分别做端到端测试

注意:排查时不要只验证“程序能跑”,要验证“执行后的文件确实变了”。例如确认元数据,就应该重新打开文件读取,而不是看脚本打印的日志。

8. 最佳实践与扩展方向

8.1 可复用的标注流水线检查清单

以下清单可以直接用在项目评审或上线前检查中:

  • 生成源头是否自动调用标注服务,而不是靠人工手动打标。
  • 标注字段是否由统一规范定义,是否包含生成时间、模型、操作者、内容 ID。
  • 发布接口是否强制校验标注,校验失败时是拒绝发布还是降级处理。
  • 是否对 JPEG、PNG、WebP、MP4 分别做了格式兼容测试。
  • 水印密钥是否放在配置中心或密钥管理服务中。
  • 是否记录了输入哈希、输出哈希和操作日志。
  • 是否监控标注成功率、校验失败率、处理耗时。
  • 是否在真实平台转码后做过端到端验证。
  • 是否有人工抽检机制,抽样验证机器判断是否正确。

8.2 后续扩展方向

标注 AI 深度伪造的工程化方向很多,按优先级可以从这几个方向展开。

第一,对齐 C2PA 等开放标准,让标注信息能被更多平台和工具识别,而不是只在自家系统内有效。第二,引入频域鲁棒水印,解决图片经过压缩、缩放后水印丢失的问题。第三,把标注能力从图片扩展到视频和音频,视频需要在关键帧和音轨中分别写入标识。第四,接入检测模型做兜底,对没有标注和剥离标注的内容进行识别。第五,建立审计追溯系统,当一张深伪图片引发争议时,能在分钟级时间内定位到生成任务、操作者和流转路径。

如果是从零开始,建议先在图片格式上把最小闭环跑通,再逐步增加格式支持、服务化和监控。这条链路的技术难度不高,但它把内容安全、图像处理、分布式服务和数据治理都串在了一起,对系统设计和工程规范化都有实际帮助。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询