图片 AIGC 检测接入实战——URL 与 Base64 的取舍、体积校验与「你送过去的可能不是那张图」

文本检测的接入难点在「异步 + 扣费」,图片检测则多了一层:你得先解决怎么把图片送过去,以及送过去的是不是你想检测的那张图。

第一层是传输问题,URL 和 Base64 各有代价;第二层是数据问题,同一张图在微信里转一手、被截图一次、加个滤镜,像素统计特征就变了。很多人只解决了第一层,然后在第二层上得出「这个检测不准」的结论。

这篇按接入顺序拆开讲。

一、先看清接口形态

图片检测和文本检测在协议层是同构的:提交拿 task_id,再轮询。但有几个硬约束必须先知道:

项目 约束 说明
入参 image_url 或 image_base64 二选一 同时传以服务端实现为准,不要依赖
格式 jpg / png / webp 其他格式先转码
尺寸 宽高都需大于 300 像素 表情包、图标、缩略图会直接失败
体积 单张解码后 ≤ 10MB 注意是解码后,不是 Base64 字符串长度
请求体 整体 ≤ 16MB(默认) Base64 会膨胀 33%,10MB 原图约 13.3MB
结果 只有一个整体 confidence 没有分段、没有定位,别指望它指出「哪块是 AI 生成的」

最后一条最容易被误解。文本检测能给出逐段标签,图片检测只给一个总分------这是能力差异,不是接口没做完。产品上如果要做「圈出 AI 区域」,需要另外的模型,别在这个接口上等。

二、URL 还是 Base64

先给结论,再解释:

维度 image_url image_base64
上行带宽 几乎为零 原图 ×1.33
提交延迟 低(服务端异步拉取) 与图片大小线性相关
公网可达 必须 不需要
鉴权 / 防盗链 不支持自定义头,容易 403 不受影响
有效期 预签名 URL 过期即失败 无
内网图床 不可用 可用
可复现性 依赖对方内容不变 内容随请求固化

决策很简单:

  • 图片已经在公网 CDN / 对象存储上,且是公开可读的 → 用 URL,省带宽、快。
  • 图片在私网、需要鉴权、是用户刚上传的临时文件 → 用 Base64。
  • 两者都可能 → 写一个回退:先试 URL,只在「取不到图」的失败上回退到 Base64。

回退有个前提:失败必须是"图没取到"这一类。402 余额不足、429 限流、401 密钥错误,换 Base64 一样失败,回退只会把一次错误放大成两次请求。

三、Base64 的四个细节

这四个坑我都在真实项目里见过,按踩到的频率排序。

1. 把 data URI 前缀一起编码进去了。 前端用 FileReader.readAsDataURL() 拿到的是 data:image/png;base64,iVBORw0...,直接塞进 image_base64 字段,服务端解码失败。必须切掉前缀:

python 复制代码
def strip_data_uri(s: str) -> str:
    if s.startswith("data:"):
        _, _, payload = s.partition(",")
        return payload
    return s

2. 用了 URL-safe Base64。 某些语言的 base64.urlsafe_b64encode 会把 + / 换成 - _,标准解码器遇到会报错。统一用标准字母表。

3. 中间夹了换行。 一些库默认每 76 字符插入 \n(MIME 风格)。JSON 里带换行不报错,但解码端可能不容错。编码后统一 replace("\n", "")。

4. 用 Base64 长度判断体积。 这是最容易写错的一条:Base64 长度约为原始字节的 4/3,拿它跟 10MB 比,会把 7.5MB 的图误判成超限,也会把 13MB 的图放进请求然后被 413。正确做法是先解码再量,或者按 len(b64) * 3 // 4 估算。

python 复制代码
import base64

def b64_to_bytes(payload: str) -> bytes:
    payload = strip_data_uri(payload).replace("\n", "").replace("\r", "")
    raw = base64.b64decode(payload, validate=True)   # validate=True:非法字符直接报错
    if len(raw) > 10 * 1024 * 1024:
        raise ValueError(f"图片解码后 {len(raw)} 字节,超过 10MB 上限")
    return raw

validate=True 很重要。默认的 b64decode 会静默丢弃非字母表字符,于是「混进了空格和前缀」的脏数据也能解码成功,只是解出来的图是坏的------最后表现为「检测结果莫名其妙」,排查成本极高。

四、提交前先校验,别让请求白跑

一次失败的提交虽然不扣费,但它消耗了你的 QPS、一次往返、一条错误日志,而且会让重试逻辑变复杂。能在本地判断的事情不要交给服务端。

python 复制代码
import io
from PIL import Image

ALLOWED_FORMATS = {"JPEG", "PNG", "WEBP"}
MIN_SIDE = 301          # 上游要求宽高都大于 300
MAX_BYTES = 10 * 1024 * 1024


def inspect_image(raw: bytes) -> dict:
    """只读文件头拿尺寸与格式,不解码像素。"""
    img = Image.open(io.BytesIO(raw))
    width, height = img.size

    if img.format not in ALLOWED_FORMATS:
        raise ValueError(f"不支持的格式: {img.format}")
    if width < MIN_SIDE or height < MIN_SIDE:
        raise ValueError(f"尺寸过小: {width}x{height},宽高都需大于 300")
    if len(raw) > MAX_BYTES:
        raise ValueError(f"图片过大: {len(raw)} 字节")

    return {"format": img.format, "width": width, "height": height, "bytes": len(raw)}

两个细节:

  • 用 img.format 判断格式,不要看扩展名 。.png 改成 .jpg 是零成本的,扩展名不构成任何约束。
  • Image.open() 是惰性的,只读头部,不会把整张图解码进内存。不要在这里调 img.load() 或 np.array(img)------那等于给一个 10MB 的图分配几十 MB 的内存,还可能撞上解压炸弹。

五、你送过去的那张图,可能不是你看到的那张

这一节是图片检测和文本检测最大的不同:图片在到达你手里之前,已经被处理过很多次了。

1. EXIF 方向。 手机竖拍时,传感器存的是横向图,靠 EXIF 的 Orientation 标记告诉查看器「转 90 度显示」。你在预览里看到的是正的,但字节流是横的。服务端是否应用 EXIF 取决于实现,同一张图可能因此得到不同结果。想稳定复现,就在提交前物理转正:

python 复制代码
from PIL import ImageOps

def normalize_orientation(raw: bytes) -> bytes:
    img = Image.open(io.BytesIO(raw))
    fixed = ImageOps.exif_transpose(img)          # 按 EXIF 真正旋转像素
    if fixed is img:
        return raw                                 # 没有方向标记,原样返回
    buf = io.BytesIO()
    fixed.save(buf, format=img.format or "JPEG")
    return buf.getvalue()

2. 平台二次压缩。 微信、微博、小红书在传输时都会重编码并缩图。重编码会破坏高频细节,而很多检测特征恰恰藏在高频里。同一张图「原图」和「微信保存后」的分数可能差很多。

3. 截图。 截图会叠加状态栏、UI、圆角、阴影,甚至把图片缩放成非整数倍。截图送检,测的其实是「一张带 UI 的截图」,不是原图。

4. 透明底 PNG。 透明像素转成 JPEG 时,如果没有铺底色,透明区域会变黑。黑底本身就是一个强特征,会干扰判断。需要转码时铺白底:

python 复制代码
def flatten_to_rgb(img: Image.Image) -> Image.Image:
    if img.mode in ("RGBA", "LA", "P"):
        img = img.convert("RGBA")
        canvas = Image.new("RGB", img.size, (255, 255, 255))
        canvas.paste(img, mask=img.split()[-1])
        return canvas
    return img.convert("RGB")

5. 长截图与拼接图。 一张几万像素高的长图,缩放到模型输入尺寸后,单块区域的细节几乎全部丢失。正确做法是先切分再分别检测,或者只取主体区域。

6. 水印、贴纸、滤镜、打印拍照。 这些都是叠加在像素上的额外信号。尤其是「对着屏幕拍照」,会引入摩尔纹,几乎必然污染结果。

由此得出一条工程原则:不要为了"统一格式"无条件重编码。重编码会让结果不可复现,还会破坏你想检测的那些痕迹。只在必要时(EXIF 方向、透明底)处理,并且把「提交的是哪个版本」记录下来------原图、哈希、处理动作,申诉和复盘都要用。

六、完整提交与轮询

python 复制代码
import base64
import os
import time
import uuid

import requests

# inspect_image / strip_data_uri / normalize_orientation 见上文

BASE = "https://sbnew.top"
KEY = os.environ["AIGC_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}


def submit_url(image_url: str, idem: str | None = None) -> str:
    idem = idem or str(uuid.uuid4())
    resp = requests.post(
        f"{BASE}/v1/image/classify",
        headers={**HEADERS, "Idempotency-Key": idem},
        json={"image_url": image_url},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["data"]["task_id"]


def submit_bytes(raw: bytes, idem: str | None = None) -> str:
    inspect_image(raw)
    payload = base64.b64encode(raw).decode("ascii")
    idem = idem or str(uuid.uuid4())
    resp = requests.post(
        f"{BASE}/v1/image/classify",
        headers={**HEADERS, "Idempotency-Key": idem},
        json={"image_base64": payload},
        timeout=60,          # 大图上传要给足时间
    )
    resp.raise_for_status()
    return resp.json()["data"]["task_id"]


def submit_with_fallback(*, url: str | None = None, raw: bytes | None = None) -> str:
    """同一个逻辑请求:先试 URL,取不到图再回退 Base64,全程复用同一个幂等键。"""
    idem = str(uuid.uuid4())

    if url:
        try:
            return submit_url(url, idem)
        except requests.HTTPError as exc:
            # 只有"图取不到"才回退。402/429/401 换通道也没用。
            if exc.response is None or exc.response.status_code not in (400, 404, 422):
                raise

    if raw is None:
        raise ValueError("URL 不可用且没有提供图片内容")
    return submit_bytes(raw, idem)


def wait(task_id: str, timeout: int = 180) -> dict:
    deadline = time.time() + timeout
    interval = 1.5

    while time.time() < deadline:
        resp = requests.get(f"{BASE}/v1/tasks/{task_id}", headers=HEADERS, timeout=30)
        resp.raise_for_status()
        task = resp.json()["data"]
        if task["status"] in ("succeeded", "failed"):
            return task
        time.sleep(interval)
        interval = min(interval * 1.5, 5.0)

    raise TimeoutError(f"{task_id} 仍在执行,稍后继续查,不要重新提交")

取结果:

python 复制代码
task = wait(submit_with_fallback(url="https://cdn.example.com/a.jpg"))
if task["status"] == "failed":
    print("失败:", task["error_code"], task["error_msg"])
else:
    confidence = task["result"]["data"]["confidence"]
    print(f"AI 生成概率:{confidence:.4f}")

注意 result 的结构是 {"status": "success", "data": {"confidence": ...}},不是文本那套 softmax_confidence + segment_labels。两套结果不要混着解析。

七、失败怎么处理

HTTP 层(请求未被受理,不产生任务、不扣费):

状态 code 图片场景的常见原因
400 invalid_param 两个字段都没传 / 都传了 / URL 不是 http(s) / Base64 解不出来
402 insufficient_balance 余额不足
413 payload_too_large 请求体超 16MB,通常是 Base64 大图
429 rate_limited 提交太快,按 Retry-After 等

任务层(HTTP 200,status=failed):

error_code 含义 退款
bad_request 图片不被受理(尺寸、格式、内容) 全额退
rate_limited 检测服务繁忙 全额退
service_unavailable 服务暂不可用 全额退
service_error 检测异常,请求已送达 不退
detect_failed 结果未取回 不退
timeout 检测超时 不退

图片按张计费,所以重试就是重复扣费 。凡是「不确定上一次有没有成功」的场景,一律复用同一个幂等键,或者直接拿 task_id 去查,不要重新提交。

八、小结

图片检测的接入质量,八成取决于提交之前做的事:

  1. 公网图用 URL,私网图用 Base64,只在「取不到图」时回退;
  2. Base64 去掉 data URI 前缀、去掉换行、用标准字母表、按解码后字节校验;
  3. 提交前用 Pillow 只读头部校验格式、尺寸、体积,别让请求白跑;
  4. 想清楚你送的是原图、转正后的图,还是被平台压过的截图------这决定结果能不能复现;
  5. 不要为了统一格式重编码,那会破坏你想检测的痕迹;
  6. 图片只有一个总分,没有分段定位,产品设计别指望它。

剩下的是老规矩:幂等键跟着逻辑请求走,轮询超时不重提,退款以资金流水为准。


示例基于文识云(sbnew.top)的 AIGC 检测接口,底层为朱雀模型。图片校验、EXIF 处理与回退策略与具体服务商无关,换成别家也能对上大半。

相关推荐
SEO_juper1 小时前
Node.js 重定向链检测工具:301 跳了几跳、有没有成环,一次看清
xml·开发语言·python·node.js·json·bash
曲径海盗1 小时前
【Python 学习第 37 天】for 循环
python·学习笔记·编程入门
心静财富之门2 小时前
Python 数据共享四种方案完整实例(同进程 / JSON / Redis / HTTP)
数据库·python·oracle
读完一本书2 小时前
AI 生成的图,为什么一眼就能看出是 AI?5 个破绽
人工智能·aigc
王仔小奶2 小时前
慢SQL治理与性能调优:DBA如何用Shell/Python提升运维效率?
python·sql·dba
用户0332126663672 小时前
使用 Python 快速在 PowerPoint 中生成表格【教程】
python
用户11977079480732 小时前
服装电商的虚拟试穿怎么落地?拆解开源 OOTDiffusion:原理流程、本地推理与商用边界
aigc
夜晚回家3 小时前
Python 身份运算符详解 | is 与 == 到底差在哪
python
徐开泉3 小时前
推荐判定的规则管线:2026年否定句、并列句与位置权重的处理
aigc