个人学习小站 SIGNAL DESK 2026
LIVE --:--:-- 投稿 账号
资讯 人工智能 光明网、央视新闻

图片真伪检测:四层验证跑完,只有两层靠得住

11 月 13 日警方通报的天价账单案,是在真账单上用 AI 改的数字。这篇按算账、看痕迹、查来源、回原处四层拆解验证流程,附可跑脚本和三张样本的实测数据,说清哪些常见检测方法实际已经失效。

2026年11月14日 8 小时前 作者 站长 约 13 分钟 9 阅读

11 月 13 日中午,上海警方通报了那起"天价酒水账单"。

通报写得很直白:一家互联网酒类销售公司的负责人,组织人利用 AI 软件篡改账单金额,把饭店用的酒换成自家销售的牌子,再做成视频,靠控制账号、雇人代发推出去。15 人被抓,4 人被采取刑事强制措施,11 人被行政处罚。

这里有个细节值得所有做内容、做电商、做公关的人记住:他们没有凭空生成一张假账单,是在一张真账单上改的。

这两件事的难度完全不同,被识破的方式也完全不同。市面上讲"AI 检测"的文章,绝大多数在教你识别前者,而这次案件用的是后者。

这篇讲一件事:拿到一张可疑的账单、转账截图、报价单,按什么顺序查,能在几分钟内给出判断。文中的技术结论都来自我自己做的三张对照样本,脚本可以复制走。

通报里的动作可以拆成四步,每一步留下的痕迹都不一样。

假账单的四步与破绽落点

第一步是拿到一张真账单,拍下来或者下载。第二步是改金额、换品名。第三步是把它做成短视频。第四步是多账号铺量。

看起来每一步都很轻,但第二步是个死结。

一张餐厅账单上至少有五组数字互相咬合:单项金额、明细小计、服务费、税费、合计。改动其中任何一个,剩下四个都得跟着重算,而且服务费率、税率、四舍五入规则得同时对得上。

真实小票还有一个更麻烦的约束:它是打印机一次输出的,字体、字距、行距、右对齐方式全局一致。只改一行,那一行的字宽和右边距就跟别处不一样。

所以造假者面对的是一个三选一:

  • 只改看得见的数字:算术关过不了,一算就露
  • 全部重算:得同时复刻整张单的排版,工作量大
  • 干脆重做一张:得把餐厅抬头、单号规则、打印机字体全复刻一遍

这次案件走的是第一条路,然后用视频和铺量把细节盖住。视频还有个额外好处:压缩后画质下降,很多细节"看起来正常"了。

这也解释了一件事:为什么警方通报里用"篡改"而不是"生成"。篡改和生成,留下的证据完全不是一个类型。

一、第一层:算账,三十秒能做完

这是唯一能被脚本自动化的一层,也是最容易被跳过的一层。

任何一张消费凭证,都有四道算术关。

关卡校验内容常见翻车点
行内乘积数量 × 单价 = 金额单价改了、金额没改,或反过来
明细求和所有单项金额相加 = 小计加了一行,小计忘了动
费率小计 × 服务费率 = 服务费只改总价,费率行不动
合计小计 + 服务费 + 税 = 合计用了正确的服务费去算合计,但显示行是旧的

第三关和第四关最容易被忽略,因为它们离"总价"最远,人的注意力都在最后那个大数字上。

我做过一张样本,专门验证这件事。

两种伪造手法,两种翻车方式

第一种是局部改金额:把一行 688.00 改成 6,880.00,其余不动。这是最常见的粗糙改法。

第二种是整图重做:加一行 39,600.00 的酒水,小计、合计、支付额全部重算,让所有看得见的数字都自洽。这是"认真造假"的做法。

把两张单的文字分别存成 txt(bill_local.txt 是局部改的,bill_remade.txt 是整图重做的),跑出来是这样:

$ python voucher_check.py --text bill_local.txt --rate 0.10
{
  "money": {
    "items": 6,
    "issues": [
      "行内乘积不符:清蒸东星斑 1×688.00≠6880.00",
      "明细合计不符:逐项 7,558.00 vs 小计 1,366.00"
    ],
    "passed": false
  }
}

$ python voucher_check.py --text bill_remade.txt --rate 0.10
{
  "money": {
    "items": 7,
    "issues": [
      "服务费不符:应 4,096.60,实 136.60",
      "合计不符:应 41,102.60,实 45,062.60"
    ],
    "passed": false
  }
}

第一种被两条规则同时抓住,在意料之中。

第二种值得多说两句。它的单价、金额、小计、合计、支付额全部对得上,人眼扫十遍也找不到问题。它栽在一个地方:造假者算合计的时候用了正确的服务费(40,966 × 10% = 4,096.60),但显示出来的服务费那一行,还是原来的 136.60。

这种错,靠眼睛基本无解,靠减法一秒就出来。

这就是自动化的价值所在。它不比人聪明,只是比人有耐心——人看到最后一个数字对上了就收工,脚本会把每一行都算一遍。

脚本

只依赖 Pillow,其余全是标准库,复制就能跑。

# voucher_check.py
import argparse, json, re
from PIL import Image

SUSPECT = ("photoshop", "gimp", "snapseed", "lightroom", "picsart", "pixlr",
           "美图", "醒图", "canva", "midjourney", "stable diffusion", "firefly")
NUM = re.compile(r"-?\d[\d,]*\.?\d{0,2}")

def container(path):
    im = Image.open(path)
    raw = im.getexif()
    exif = {str(k): str(v)[:80] for k, v in raw.items() if not isinstance(v, bytes)}
    low = open(path, "rb").read().lower()
    return {"size": list(im.size), "format": im.format,
            "exif_fields": len(exif),
            "software": [s for s in SUSPECT if s.encode() in low],
            "c2pa": b"c2pa" in low}

def money(text, rate):
    num = lambda s: float(s.replace(",", ""))
    rows, issues = [], []
    for line in text.splitlines():
        if not line.strip() or re.search(r"\d:\d|单号|桌号|人数|品名", line):
            continue
        if re.search(r"小计|合计|服务费|支付|总计", line):
            continue
        t = NUM.findall(line)
        if len(t) >= 3:
            q, p, a = num(t[-3]), num(t[-2]), num(t[-1])
            if q and abs(q * p - a) > 0.01:
                name = line.strip().split()[0]
                issues.append(f"行内乘积不符:{name} {q:g}×{p:.2f}≠{a:.2f}")
            rows.append(a)

    def pick(key):
        m = re.search(r"(?:" + key + r")[^\n]*?(-?[\d,]+\.\d{2})", text)
        return num(m.group(1)) if m else None

    sub, svc, tot = pick("小计"), pick("服务费"), pick("合计|总计")
    if rows and sub is not None and abs(sum(rows) - sub) > 0.01:
        issues.append(f"明细合计不符:逐项 {sum(rows):,.2f} vs 小计 {sub:,.2f}")
    if sub is not None and svc is not None and abs(svc - sub * rate) > 0.01:
        issues.append(f"服务费不符:应 {sub * rate:,.2f},实 {svc:,.2f}")
    if sub is not None and tot is not None:
        want = sub + (svc if svc is not None else sub * rate)
        if abs(tot - want) > 0.01:
            issues.append(f"合计不符:应 {want:,.2f},实 {tot:,.2f}")
    return {"items": len(rows), "issues": issues, "passed": not issues}

ap = argparse.ArgumentParser()
ap.add_argument("--image")
ap.add_argument("--text")
ap.add_argument("--rate", type=float, default=0.10)
a = ap.parse_args()
out = {}
if a.image:
    out["container"] = container(a.image)
if a.text:
    out["money"] = money(open(a.text, encoding="utf-8").read(), a.rate)
print(json.dumps(out, ensure_ascii=False, indent=2))

用法:

python voucher_check.py --image bill.jpg
python voucher_check.py --text bill.txt --rate 0.10

文字从哪来?不用装 OCR。手机相册长按图片就有"提取文字",微信里长按也能提取,复制粘贴存成 txt 就行。这个精度对算账够用——只要求数字认对,认错的概率远低于人眼漏看。

--rate 是服务费率。餐厅常见 10%,酒店可能 15%,外卖平台另算。这次案件里那个"天价账单",服务费的比例本身就是个校验点。

同样的逻辑,换个凭证

四道关的内核是"数字之间必须互相咬合",换凭证只需要换咬合关系。

凭证类型咬合关系
餐厅账单服务费率、包间最低消费、开瓶费、茶位费
电商订单优惠券叠加规则、满减门槛、运费、实付金额
转账截图余额变动、手续费、时间戳与聊天记录是否连续
报价单单价×数量、税率、折扣前后金额、大小写是否一致
医疗/维修单据项目单价、医保报销比例、自付与自费拆分

最后一项顺带提一句:正规单据的中文大写金额和阿拉伯数字必须一致,这个校验连脚本都不用写,肉眼对一遍即可。造假者改小写数字的时候,经常忘了改大写。

二、第二层:看痕迹,人眼能查,软件不行

算账能解决"数字对不对",解决不了"数字有没有被改过"。后者得看图像本身。

我做了两张对照样本,左边原图,右边改了第一行金额。

原图与局部涂改对照

先试一下:你能在右图里找到改动的地方吗?

大多数人在这个尺寸下找不到。放大到原始像素也不一定找得到,因为这张图先走了一遍完整拍摄链路——纸张纹理、光照渐变、颗粒噪声、轻微旋转、JPEG 压缩——涂改是叠加在这之上的。

这不是眼力问题,是方法问题。

三个真正能看的物理细节

一是涂改区的纹理落差。

打印纸有颗粒,手机拍下来一定带噪点。用修图工具涂一块白色再重打字,这块区域的噪声会被抹平,跟四周形成落差。

在上面那张图里,改动处的底色比周围"干净"——这是最可靠的肉眼判据,比看边缘锯齿管用得多。

高级一点的造假会用"内容识别填充"保留纹理,落差就没那么明显。但填充出来的纹理是重复的,同一片纸纹在附近出现两次,也是破绽。

二是右对齐基线。

金额列在小票上永远是右对齐的。改过的数字宽度不同,右边缘要么被裁掉一点,要么整体左移,跟上下行的右边缘对不齐。

这条用手机横过来、贴着屏幕边缘看,很容易发现。所有金额的右端应该在同一条垂直线上,有一行偏了 2 像素就是问题。

三是字体度量。

同一台打印机出来的字,字重、字距、基线高度是完全一致的。贴上去的字,哪怕用了同款字体,字号差 1pt、基线差 1px,放在一起就能看出来。

判断方法:找一行没改过的数字,与可疑行上下叠着看,注意数字的顶部和底部是不是在同一条水平线上。

我的失败实验:三个"标准方法"全部失效

写到这里我本来想给一个自动检测涂改的脚本。业界常见的三件套是 ELA(误差水平分析)、JPEG 块效应检测、噪声一致性分析,网上教程很多。

我按标准做法实现了一遍,在三张样本上跑,结果是:

样本ELA p99ELA 高差异像素占比块效应比(垂直)局部纹理方差
原图50.00%1.03811.98
局部涂改50.00%1.03711.98
整图重做50.00%1.04411.99

三个指标几乎完全相同。涂改区那么大一块白斑,脚本一点反应都没有。

原因不复杂,三条:

ELA 依赖足够强的压缩差异。 它的原理是把图按固定质量重新存一次,跟原图做差,差异集中的地方就是改动区。但我的样本先经了压缩、旋转、轻微模糊,高频信息被削弱,重存一次的新增差异 p99 只有 5 个灰阶。这个量级跟压缩自身的噪声混在一起,分不开。

局部方差分不清"空白"和"涂改"。 我最初的实现是按网格算纹理强度,找异常平滑的格子。结果整张图最平滑的永远是下方大片空白区,把真正的涂改盖住了。

块效应本来就弱。 JPEG 的 8×8 块边界会在图上留下周期性梯度,拼接过的图块相位会错位。但背景大片纯色时,块边界本身就不明显,这个信号弱到测不出来。

关键在于:这三道处理,微信转发一遍就全做了。

任何一张经过聊天软件转发的图,都会被重新编码、缩到较小的长边(通常 1080 上下)、剥离元数据。做完这三件事,像素级取证的价值基本归零。

这也解释了为什么那些"AI 图片检测器"的准确率众说纷纭。它们依赖的信号,在上传那一刻就没了。厂商公布的数字通常基于公开数据集的原图,不是你手上那张转过三手的截图。

所以这一层的正确用法是:人眼看纹理,软件只做参考。

三、第三层和第四层:查来源,问本人

前两层查的是"这张图本身有没有问题"。第三层查"它是从哪来的",第四层查"原始出处认不认"。

第三层:文件能告诉你什么

--image 参数会读两类信息:EXIF 里的 Software 字段,和有没有 C2PA 内容凭证。

Software 字段记录的是"用什么软件保存过这张图"。如果里面出现 Photoshop、美图、醒图这类名字,至少说明它被编辑过。脚本会把这些命中项列出来。

但有两个坑必须说清楚,不然会误判。

坑一:没有 EXIF,不能证明是假的。

截图没有 EXIF,微信转发的图没有 EXIF,绝大多数平台上传后会自动剥离。一张干干净净、什么信息都没有的图,是"正常转发"的典型状态,不是"造假"的证据。

反过来,EXIF 也能伪造。真要造假的人,填个相机型号是几行代码的事。

坑二:有 C2PA 也不能全信。

C2PA 是拍摄或生成那一刻写入的加密签名,理论上能证明来源。但目前支持它的设备和工具还不多,而且社交平台转存时普遍会剥掉。

它更适合用来"证明这张图是真的",不适合用来"证明这张图是假的"。没有签名,只能说明无从验证,不等于伪造。

在线检测工具,什么时候能用

这类工具我建议这样用:

  • 只做参考,不做结论。 任何单一结果都不足以支撑举报或报案。
  • 优先用在"疑似 AI 生成"的图上,不是"疑似 P 图"的图上。它们训练时见的多是整图生成样本。
  • 同一张图跑两三个工具,结论不一致就说明这个场景不可靠。
  • 结果只写"疑似",不写"确认"。 一旦写死,后面反转要自己承担。

真正需要出结论的场合——比如要走法律程序——应当做司法鉴定,不是跑在线工具。

第四层:回原处问,这是唯一能定性的

前三层都是筛子,能挡掉大部分粗糙的伪造,但都只能给"疑似"。

能给出确定答案的,只有回到源头。

  • 账单:打商家电话,报单号查系统记录
  • 转账:让对方在聊天里重新发一次实时截图,或者直接开视频
  • 报价单:找发单人确认,别在转发链里问
  • 新闻截图:去原始发布账号的主页找,看有没有这条

这次案件里,饭店只要出示自己的系统记录,一个单号就能说清。警方调查也正是从"这张单存不存在"开始的。

技术手段的作用,是帮你在打电话之前,先把该问的问题准备好。


四、如果你是被造谣的那一方

上面讲的是"怎么判断别人发的图"。换个位置,如果你是那张假图里的商家、品牌、个人,处理顺序不一样。

先固定证据,别急着辟谣

黄金时间是发现后的两小时,顺序不能错。

  • 原始页面 URL,连同完整截图。截图必须包含账号名、发布时间、点赞转发数,缺一项后面投诉会被退回
  • 录屏。从打开 App 开始滚一遍,证明内容不是拼接出来的
  • 图片文件本身。右键另存,不是截图——原图才有元数据可查
  • 传播节点。谁首发、谁转发、什么时候开始放量

辟谣发得再快,证据没固定住,后面全是空口。

平台投诉

用平台的"不实信息"举报入口,把证据包一起提交。多数平台对"企业名誉"类投诉有专门通道,走通用举报入口反而慢。

投诉时把话说具体:哪一句不实、真实的数字是什么、证据在第几页。别写"恶意造谣、严重侵害"这类空话,审核员一天看几百条。

报案

带证据包去派出所,说清三件事:内容不实、传播范围、造成的损失。

损失要能算出来——退单金额、客流下降、合作方解约。这类案件要够得上刑事,立案追诉标准之一是"直接经济损失五十万元以上"(另一条是停业停产六个月以上或破产)。损失算不算得清,直接决定走哪条程序。

法律依据

《刑法》第二百二十一条规定了损害商业信誉、商品声誉罪:捏造并散布虚伪事实,损害他人的商业信誉、商品声誉,给他人造成重大损失或者有其他严重情节的,处二年以下有期徒刑或者拘役,并处或者单处罚金。

这里有个容易被忽略的点。相关司法解释明确,"捏造"既包括完全虚构,也包括在真实情况基础上的部分虚构。

也就是说,"真账单 + 改数字"这种手法,在法律上和"凭空编造"是一回事,不因为底子是真的就减轻。通报里 4 人被采取刑事强制措施,对应的正是刑事程序。

至于行政处罚,《治安管理处罚法》对散布谣言、扰乱公共秩序另有规定,11 人被行政处罚对应的就是这一层。


一张可疑的截图,怎么判断真假。

按这个顺序来:

  1. 算账——数字对不对得上,三十秒,能筛掉大部分粗糙伪造
  2. 看痕迹——涂改区有没有纹理落差,右对齐有没有错位,字体度量一致不一致
  3. 查来源——元数据、内容凭证、发布账号的历史
  4. 回原处——打电话,要实时截图,找原始出处

四层里,能给出确定答案的只有两层:算账,和回原处。另外两层只能把"看起来可疑"变成"更可疑",它们决定你该问什么,不决定结论。

至于那些号称"一键检测 AI 图片"的工具——在你的图被微信压缩过一次之后,它们就已经失明了。

如果需要理解 AIGC 检测的整体原理和误差边界,可以看 AIGC 检测是怎么回事:原理、误差与应对;音视频类的造假手法与风险边界,AI 配音克隆音色:能克隆到什么程度,风险在哪 那篇讲得更细。

登录后可点赞、收藏与反馈。

继续阅读 NEXT

更多 →