跳到主内容
快讯直播
AI智模界
教程

Guided Vision 长截图与 PDF 结构化提取

在发票、合同、报表里提字段,难点从来不是"模型看不看得懂字",而是"它有没有看对地方"。长截图和 PDF 恰好是最容易让它看错地方的输入:一张 2500×14000 的电商订单长图,或者一份 30 页的扫描合同,直接丢进去,返回的 JSON 语法完全合法,数字却串了行、漏了页。下面按排错的路子走一遍。

这里说的"引导取景(guided vision)",指的是用缩略图导航、坐标框选、局部裁剪放大这一套动作,把模型的注意力强行限制在指定区域,再配合分步追问拿到结构化结果。具体的功能命名、参数与可用范围,以官方文档当前版本为准。

报错现象

典型场景:要把一批发票长截图和 PDF 合同里的「发票号 / 开票日期 / 价税合计 / 购买方 / 销售方 / 税率」提成一张表。

出错时的样子通常是这几类:

1. 接口直接报错拒收。 返回信息类似 image dimensions too large、request payload size exceeds limit、invalid image。影响范围是整批文件全军覆没,最容易发现。

2. JSON 解析失败。 拿到的是 json.decoder.JSONDecodeError: Expecting ',' delimiter,因为返回里混了 ``` `json `` 代码块围栏、// 注释、中文全角引号 “2024年”`,或者末尾跟了一句"以上信息仅供参考"。

3. 格式合法但内容错位。 这是最要命的一类:发票号那格填的是对方公司名,价税合计那格拿到的是税额,10 行的明细表只回了 6 行。因为 JSON 本身能解析,下游直接入库,错误要等对账时才被发现。

4. 坐标框选失效。 让人工指定"只看下半部分",或者让模型返回区域坐标,结果裁出来的图和想看的区域差了一整行,或者框在了空白处。

5. PDF 只读了第一页。 30 页的合同只返回第 1 页的字段,其余静默丢失,没有报错。

6. 多页字段互相污染。 第 3 页的金额配了第 5 页的合同编号,合并成一条记录。

影响范围从"跑不通"到"跑通了但数据是错的"都有,后者更危险。

可能原因

按出现概率从高到低:

1. 图像被缩放降采样,小字糊了。 长截图为了塞进模型输入,长边被压缩,正文 12px 的字直接糊成一团,发票号里 0 和 O、1 和 l 分不出来。

2. 没有给注意力锚点。 整图一次性丢进去,模型做的是平铺扫描;表格列一多,列边界就串了。

3. 一次问太多。 一张图里同时要 20 个字段 + 明细表格 + 印章信息,注意力被平均分配,长尾字段必漏。

4. 坐标约定不一致。 人理解的"下半部分"和模型输出的像素值对不上;归一化坐标(0–1)和绝对像素坐标混用;原点在左上还是左下没说。

5. PDF 处理链路的问题。 只渲染了第一页;渲染 DPI 太低;扫描件没有文字层;PDF 本身加密,渲染出来是空图。

6. 输出格式约束太弱。 只说"给我 JSON",没有给 schema、没有禁止代码块和注释、没有规定缺失值怎么写。

7. 图片方向或透视不正。 手机拍屏、斜着拍,文本行是斜的,模型读行时错行。

8. 多页拼在一起调用。 一次塞 5 页进同一个请求,页与页之间没有隔离标记。

逐条排查与解决

第 1 条:先量分辨率,再决定要不要切片

判断方法:

```bash

python -c "from PIL import Image; im=Image.open('fapiao.png'); print(im.size)"

```

如果长边明显超过模型稳态识别的尺寸(具体上限以官方文档当前版本为准),就必须切片。切片要带重叠,否则会把一行字从中间切断:

```python

from PIL import Image

im = Image.open("long.png")

w, h = im.size

tile_h = 1600 # 每片高度

overlap = 120 # 重叠像素,防止切断文本行

for i in range(0, h, tile_h):

top = max(0, i - overlap)

bottom = min(h, i + tile_h + overlap)

im.crop((0, top, w, bottom)).save(f"tile_{i:06d}.png")

```

判断是不是这个原因:把切片出来的图打开看一眼,如果原来糊的发票号变清楚了,就是这个原因。

第 2 条:用"缩略图导航 → 框选 → 裁高清重看"做引导取景

不要让模型一次看全图。分两步走。

第一步,给它一张压小的整页缩略图,只让它干一件事——找区域:

```text

这是一整页文档的缩略图。请只做一件事:找出「价税合计」这一项所在的矩形区域。

返回如下 JSON,不要输出任何其他文字:

{"box": [x1, y1, x2, y2]}

坐标是相对整图的归一化值,范围 0 到 1,原点在左上角,x 向右,y 向下,

x1<x2,y1<y2,框要完整包住这一行文字(含左边的标签和右边的金额)。

```

第二步,按返回的框裁原图(用高清原图,不要用缩略图):

```python

from PIL import Image

im = Image.open("fapiao.png")

w, h = im.size

x1, y1, x2, y2 = box # 模型返回的归一化坐标

四周留一点余量,避免把字的边缘切掉

pad_x, pad_y = 0.01, 0.01

crop = im.crop((

int(max(0, x1 - pad_x) * w),

int(max(0, y1 - pad_y) * h),

int(min(1, x2 + pad_x) * w),

int(min(1, y2 + pad_y) * h),

))

crop.save("roi.png")

```

第三步,把框画回缩略图,人眼确认一次再往下走:

```python

from PIL import ImageDraw

vis = im.copy()

d = ImageDraw.Draw(vis)

d.rectangle([x1 * w, y1 * h, x2 * w, y2 * h], outline="red", width=6)

vis.save("check_box.png")

```

这一步是整个流程里性价比最高的动作。check_box.png 一看就知道模型有没有看对地方,比盯着 JSON 猜快得多。

判断是不是这个原因:如果模型返回的 box 明显偏了(比如框在了表头而不是合计行),说明缩略图太小看不清,把缩略图长边调大一点再试,或者手动给坐标。

第 3 条:分步追问,一轮只干一件事

把一次性提问拆成四轮:

  • 第一轮:只看版面,输出这是什么类型的文档、有哪些区块(表头区 / 明细表区 / 合计区 / 印章区)。
  • 第二轮:针对每个区块,只输出字段名清单,不输出值。
  • 第三轮:逐个字段取值,每个值必须附上"原图里的原文片段"作为证据。
  • 第四轮:只针对前几轮里对不上的字段复核一次。

第三轮的输出模板可以这样约束:

```text

请只输出 JSON,不要代码块围栏,不要注释,不要任何解释文字。

每个字段必须给出 value 和 evidence:

  • value:字段的最终取值,识别不到就写 null,不要猜测
  • evidence:该值在图中对应的原文片段,逐字照抄
  • page:来自第几页,单页图写 1
  • confidence:high / medium / low

格式:

{"fields": [{"name": "...", "value": "...", "evidence": "...", "page": 1, "confidence": "high"}]}

```

判断是不是这个原因:如果一次性提问漏了 4 个字段,拆成四轮后漏的字段补回来了,就是注意力被平均分配的问题。

第 4 条:把坐标约定写死

在提示词里明确三件事:归一化还是像素、原点在哪个角、边界是否包含。能给出一个输入输出示例最好。如果模型连续两轮返回的坐标都对不上,直接放弃坐标方案,改成"让人工给出像素值"或者"用固定的版面切分比例",别在坐标上耗。

第 5 条:检查 PDF 链路

```bash

pdfinfo contract.pdf # 看页数、是否加密

pdftoppm -r 220 -png contract.pdf page # 逐页渲染成图

```

要点:

  • DPI 建议 200 到 300 之间。72 DPI 渲染出来的小字基本没法用。
  • 扫描件没有文字层的,先过一遍 OCR 工具生成文字层,再渲染。
  • 加密 PDF 先用 qpdf --decrypt 之类的方式解密,否则渲染出来是白图。
  • 每次请求只送一页,并且把页码写进输出结构里。

判断是不是这个原因:ls page-*.png | wc -l 的数目和 pdfinfo 报的页数对不上,就是只渲染了部分页。

第 6 条:输出被污染导致解析失败

解析前先把原始返回打印出来看,别急着 json.loads。常见补救是抽取第一个完整的 JSON 对象:

```python

import json

def extract_json(text):

start = text.find("{")

if start == -1:

return None

depth = 0

in_str = False

esc = False

for i in range(start, len(text)):

ch = text[i]

if in_str:

if esc:

esc = False

elif ch == "\\":

esc = True

elif ch == '"':

in_str = False

else:

if ch == '"':

in_str = True

elif ch == "{":

depth += 1

elif ch == "}":

depth -= 1

if depth == 0:

try:

return json.loads(text[start:i + 1])

except json.JSONDecodeError:

return None

return None

```

判断是不是这个原因:日志里打印出来的原文里有代码块围栏或解释性文字。

第 7 条:方向与透视

```bash

exiftool -Orientation fapiao.jpg

```

```python

from PIL import ImageOps

im = ImageOps.exif_transpose(im)

```

手机斜拍的,先做一次旋转校正;透视变形严重、文本行明显歪斜的,建议直接重拍或用扫描类应用重新生成,比在模型侧想办法划算。

第 8 条:多页隔离

每页单独调用,输出里强制带 page 和 file 字段;合并阶段先做冲突检查——同一个合同编号在不同页出现不同值时,直接进人工队列,不要静默取最后一个。

都不管用时的兜底方案

  • 降级到"人机协同"。 让模型只负责框出可疑字段的位置,数字由人来填。核对 5 个框比核对一份全错的表快。
  • 换链路:先 OCR 后抽取。 扫描件质量差的时候,先用 OCR 或表格识别工具产出文本层,再把纯文本交给模型做结构化。文本抽取的字段稳定性通常高于直接看图,代价是丢掉版面信息。
  • 拆得更碎。 把 20 个字段拆成 5 次提问,每次只问 4 个。调用次数上去了,但准确率往往能拉回来。
  • 两次交叉验证。 用两个不同措辞的提示词各跑一遍,值不一致的字段全部标记出来交人工。
  • 局部增强重问。 把可疑区域放大 2 到 3 倍、做一次灰度化和对比度增强,再问一次。
  • 兜底落地方式。 无论如何先把结果落成 CSV,人工只改差异行,而不是从头重录。

如何预防再次发生

  • 建回归样例集。 攒 10 到 20 个文件,覆盖长截图、扫描件、斜拍、多页 PDF、带印章遮挡的,每次改提示词都跑一遍,统计字段级准确率,而不是"看着好像对"。
  • 强制 evidence。 每个值都必须带原文片段和位置,没有 evidence 的值直接打标记。这条能挡掉大部分编造。
  • 做格式校验闸门。 金额、日期、编号在入库前过一遍解析:

```python

import re

from datetime import datetime

from decimal import Decimal, InvalidOperation

def check_amount(s):

if s is None:

return None

try:

return Decimal(s.replace(",", "").replace("¥", "").replace("¥", "").strip())

except InvalidOperation:

return None

def check_date(s, fmts=("%Y年%m月%d日", "%Y-%m-%d", "%Y/%m/%d")):

for f in fmts:

try:

return datetime.strptime(s.strip(), f)

except (ValueError, AttributeError):

continue

return None

def check_invoice_no(s):

if not s:

return False

return bool(re.fullmatch(r"[A-Z0-9]{8,20}", s.replace(" ", "").upper()))

```

校验不过的进人工队列,不要往库里写。

  • 输出里带溯源字段。 schema_version、file、page、prompt_id 都写进结果,出问题时能回溯到是哪一版提示词、哪一页。
  • 参数进配置。 切片高度、重叠像素、渲染 DPI、缩略图长边,全部写进配置文件,别散落在脚本各处,方便调参和复现。
  • 日志留全。 原图、切片、提示词、原始返回都存下来,出错时能离线重放。
  • 心态上把模型输出当草稿。 结构化提取的上限不是模型能力,而是校验环节做得够不够。入库前有一道校验闸门,比换一个更强的模型有用得多。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。