元数据剥离 — C2PA / EXIF / XMP¶
文件:
metadata.py、noai/c2pa.py、noai/cleaner.py、noai/isobmff.py、noai/extractor.py
为什么元数据很重要¶
社媒平台(Instagram、Facebook、X/Twitter)使用图像元数据中的 AI 标记来显示"Made with AI"标签。元数据中可能泄露:
- 生成 prompt(用户输入的完整文本)
- 模型 hash(可追溯到具体模型版本)
- 种子值、采样器参数
- 用户账号标识符(通过 C2PA 签名链)
AI 元数据检测(metadata.py)¶
精确匹配的 AI 键名(20+)¶
parameters, prompt, negative_prompt, workflow, comfyui, sd-metadata,
invokeai_metadata, generation_data, ai_metadata, Dream,
sd:prompt, sd:negative_prompt, sd:seed, sd:steps, sd:sampler,
sd:cfg_scale, sd:model_hash, c2pa, c2pa_chunk, Software
关键词子串匹配¶
stable_diffusion, comfyui, automatic1111, invokeai, midjourney,
dall-e, dalle, imagen, synthid, google_ai, openai, c2pa
IPTC AI 标记(触发社媒"Made with AI"标签的元凶)¶
has_ai_metadata() 检测流程¶
graph TD
A[PIL 打开图片] --> B[遍历 img.info 键]
B --> C{精确匹配 AI 键?}
C -->|是| DETECTED[检测到 AI 元数据]
C -->|否| D{子串匹配 AI 关键词?}
D -->|是| DETECTED
D -->|否| E[调用 c2pa 库 has_c2pa_metadata]
E --> F{C2PA 存在?}
F -->|是| DETECTED
F -->|否| G[二进制扫描前 512KB]
G --> H{找到 C2PA UUID / IPTC 标记?}
H -->|是| DETECTED
H -->|否| CLEAN[无 AI 元数据]
二进制扫描模式:
c2pa/C2PA字节串- C2PA UUID:
d8fec3d61b0e483c92975828877ec481(16 字节) - IPTC AI marker 字节串
C2PA JUMBF 解析(c2pa.py)¶
C2PA 简介¶
C2PA(Coalition for Content Provenance and Authenticity)是一种加密签名的内容来源标准。AI 生成图像中嵌入 JUMBF(JSON Unified MetaBox Format)容器,包含:
- 生成工具信息(如 "OpenAI DALL-E 3")
- 签名者证书链
- 操作类型(created / converted / edited)
- 时间戳
PNG 中的 C2PA 存储¶
PNG 使用自定义 chunk 类型 caBX 存储 JUMBF 容器。
遍历 PNG chunk:
def has_c2pa_metadata(data):
# 验证 PNG 签名
assert data[:8] == b'\x89PNG\r\n\x1a\n'
offset = 8
while offset < len(data):
length = struct.unpack(">I", data[offset:offset+4])[0]
chunk_type = data[offset+4:offset+8] # 4 字节类型
chunk_data = data[offset+8:offset+8+length]
if chunk_type == b'caBX':
# 检查是否包含 C2PA 签名
if any(sig in chunk_data for sig in [b'c2pa', b'C2PA', b'jumb', b'JUMBF']):
return True
offset += 12 + length # 4(length) + 4(type) + data + 4(CRC)
JUMBF 字节级解析(_parse_c2pa_chunk())¶
不依赖官方 c2pa 库,直接在字节级别提取关键信息:
| 字段 | 匹配模式 | 示例 |
|---|---|---|
| 发行者 | Google, Adobe, OpenAI, Microsoft, Truepic |
"Google" |
| AI 工具 | GPT-4o, ChatGPT, Sora, DALL-E, Imagen, Firefly |
"DALL-E 3" |
| 软件代理 | 正则 softwareAgent.*?dname |
提取软件名 |
| 声明生成器 | claim_generator, claimGenerator |
"openai/c2pa/0.1" |
| 动作类型 | c2pa.created, c2pa.converted, c2pa.edited |
"created" |
| 时间戳 | 正则 \d{14}Z |
"20240115120000Z" |
| 数字来源类型 | trainedAlgorithmicMedia / algorithmicMedia |
触发 AI 标签 |
CBOR 文本提取(_scan_png_c2pa_chunk())¶
手动解析 CBOR major-type 3(UTF-8 字符串)长度前缀:
提取内容:name(claim generator)、specVersion、digitalSourceType URL、签名者组织。
C2PA chunk 注入(inject_c2pa_chunk())¶
将 C2PA chunk 插入到第一个 IDAT chunk 之前(PNG 规范要求非关键 chunk 在数据之前)。
EXIF/XMP 清理(cleaner.py)¶
remove_ai_metadata() 流程¶
def remove_ai_metadata(input_path, output_path):
# 1. 分类所有 metadata 键为 AI / 标准 / 其他
ai_keys, standard_keys, other_keys = _extract_non_ai_metadata(img)
# 2. EXIF 处理
exif_dict = piexif.load(exif_bytes)
# 保留非 AI 字段(Author, Copyright, Title 等)
# 删除 AI 相关字段
# 3. 写回
# PNG: PngInfo.add_text() 重建保留的文本 chunk
# JPEG: 直接写入清理后的 EXIF bytes
# DPI 和 gamma 始终保留
ISOBMFF 容器解析(isobmff.py)¶
支持 AVIF、HEIF、HEIC、JPEG-XL(均基于 ISO Base Media File Format)。
顶层 Box 遍历:
三种 size 编码:
| size 值 | 含义 |
|---|---|
| > 1 | 32 位标准 size |
| == 1 | 后续 8 字节为 64 位 largesize |
| == 0 | box 延伸到文件末尾 |
C2PA 移除逻辑:
def strip_c2pa_boxes(data):
# 验证 ISOBMFF 签名: data[4:8] == b"ftyp"
for box_type, box_data in iter_top_level_boxes(data):
if box_type == b'uuid':
# 检查 16 字节 UUID 是否匹配 C2PA
if box_data[:16] == C2PA_UUID: # d8fec3d6...
continue # 跳过(移除)
elif box_type == b'jumb':
continue # JPEG-XL JUMBF 容器,移除
output.append(box) # 其他 box 原样保留
关键:像素数据 bit-for-bit 不变,只移除元数据容器。
当前限制¶
- AVIF/HEIF/JPEG-XL 的 EXIF/XMP box 尚未清除(仅移除顶层
uuid和jumbbox) - PNG 和 JPEG 已完全覆盖
清理结果对照¶
| 来源 | 清理前 | 清理后 |
|---|---|---|
| Stable Diffusion | prompt, seed, model_hash, sampler, steps | 全部移除 |
| Midjourney | EXIF 中的 prompt, model, seed | 全部移除 |
| DALL-E 3 | C2PA manifest | 全部移除 |
| Adobe Firefly | Content Credentials | 全部移除 |
| Gemini | C2PA + EXIF | 全部移除 |
| Instagram/Facebook | trainedAlgorithmicMedia 标签 |
移除,不再显示 AI 标签 |