MiniCPM-V 4.7 读图读错表格 / 是图片切分还是提示词写法在作怪?

文章导读
同一张表格截图,模型有时读对、有时读错,通常不是模型本身“忽好忽坏”,而是两个变量在同时动:图片进模型前被缩放或切分成了什么样子,以及提问有没有把读取范围、输出格式讲清楚。判断顺序建议反过来走——先把模型真正收到的图存下来看一眼,再固定其中一个变量做对照,而不是同时改尺寸和提示词。
📋 目录
  1. A 把模型真正收到的图片存下来对比
  2. B 查清楚输入图片被限制在多长边或多少像素
  3. C 固定图片尺寸只改提问写法
  4. D 固定提问只改图片尺寸与切分方式
  5. E 把稳定复现的那组参数固化成调用模板
A A

同一张表格截图,模型有时读对、有时读错,通常不是模型本身“忽好忽坏”,而是两个变量在同时动:图片进模型前被缩放或切分成了什么样子,以及提问有没有把读取范围、输出格式讲清楚。判断顺序建议反过来走——先把模型真正收到的图存下来看一眼,再固定其中一个变量做对照,而不是同时改尺寸和提示词。

读错表格的排查方向,先分清是“模型看到的图被改了”还是“提问没说清”。适用场景是同一张表格截图结果不稳定。操作动作:把预处理后的图另存对比、打印实际输入尺寸与切片数、固定尺寸只改提问、固定提问只改尺寸。验证方式:看预处理图是否糊、表头是否被切走、输出字段数与表列数是否一致。风险边界:不同版式差异大,结论只在同类表格图上成立,换版式需重跑对照。

把模型真正收到的图片存下来对比

多模态模型很少直接吃原图。前端通常会把图片按长边或像素总量做缩放,宽表还会被切成多片送给视觉编码器。你看到的“同一张图”,到模型那里可能已经变过形。所以第一步不是改提示词,而是把处理后的图落盘。

如果走本地加载,可以先从 processor 的参数复现缩放动作;更直接的办法是把 pixel_values 反归一化还原成图。下面是一个通用骨架,字段名按你实际使用的 processor 调整:

from PIL import Image

# 1) 按 processor 实际生效的长边做一次同样的缩放,存下来对比
def dump_resized(path, out, max_side):
    im = Image.open(path).convert("RGB")
    w, h = im.size
    s = min(1.0, max_side / max(w, h))
    im2 = im.resize((max(1, int(w * s)), max(1, int(h * s))), Image.BICUBIC)
    im2.save(out)
    print("orig", (w, h), "sent", im2.size)
    return im2

# 2) 把模型真正收到的张量还原成图,逐片存出来
# inputs = proc(images=im, text=prompt, return_tensors="pt")
# pv = inputs["pixel_values"]
# print("pixel_values", tuple(pv.shape))
# for i, t in enumerate(pv[0]):
#     arr = ((t * 0.5 + 0.5).clamp(0, 1) * 255).byte().permute(1, 2, 0).numpy()
#     Image.fromarray(arr).save(f"slice_{i:02d}.png")

肉眼对比方法很简单:把原图和 slice_XX.png 并排看三件事——表格线是否变粗糊在一起、小字号数字是否已经认不出、表头行和数据行是否落在不同切片里。如果表头出现在第 0 片、第 3 行数据出现在第 1 片,模型很容易把列对应错。

查清楚输入图片被限制在多长边或多少像素

第二步是确认限制值到底是多少,而不是猜。限制通常写在 image_processor 上,常见名称包括 size、max_pixels、min_pixels、max_side、crop、slice、max_slice_nums 这类(不同版本叫法不一样,先打印出来看)。

proc = AutoProcessor.from_pretrained(MODEL_DIR)
print(proc.image_processor)
print("size:", getattr(proc.image_processor, "size", None))
print("max_pixels:", getattr(proc.image_processor, "max_pixels", None))
print("slice 相关:", getattr(proc.image_processor, "max_slice_nums", None))

inputs = proc(images=image, text=prompt, return_tensors="pt")
print("pixel_values:", tuple(inputs["pixel_values"].shape))

如果 pixel_values 的第一个维度大于 1 片,说明图被切了。这时要看的是“切在哪里”:是沿高度切还是沿宽度切,切点是否正好落在表格的行或列中间。判断切分是否切断了行列,最快的办法是把每片单独存图,数一下哪片缺表头、哪片缺左列。日志里可以搜 resize、crop、slice 这几个关键字,确认生效的是哪一档。表格列多而字小的时候,像素总量限制往往会先触发切分,而不是单纯缩小。

固定图片尺寸只改提问写法

确认了图片输入路径不变之后,把尺寸和切分参数锁死,只换提问。这一步的目的,是看错误到底有多少来自“没说清楚读哪一块”。建议至少试三种写法,并把每次答案记在同一张表里:

row_prompt = "只读取第 3 行,从左到右输出单元格内容,用 | 分隔,不要输出任何解释。"
col_prompt = "只读取『金额』这一列,从上到下输出数值,每行一个,不要输出表头。"
region_prompt = "只处理表头以下的区域,按行输出:行号|第1列|第2列|第3列,缺失的单元格写 NULL。"

对照记录建议包含这些字段,避免只凭印象判断:图片指纹(尺寸或文件哈希)、切片数、提问模板编号、模型原始输出、字段个数是否等于表列数、错误类型(漏行/串列/表头当成数据/合并单元格拆错)。

MiniCPM-V 4.7 读图读错表格 / 是图片切分还是提示词写法在作怪?
图片标识切片数模板字段个数错误类型备注
待填待填row待填待填待填
待填待填col待填待填待填
待填待填region待填待填待填

如果逐行、逐列提问能稳定读对,而开放式提问总出错,问题更偏提问写法;如果三种写法都错在同一列或同一行,那基本可以回到图片那一边继续查。

固定提问只改图片尺寸与切分方式

反过来做一遍:提问文案一个字不改,只改图片的长边、像素上限和切分重叠。尺寸不要一次跳太多,按阶梯走,每次只动一个参数。切分重叠(overlap)如果可配,建议先设一个小重叠量,观察跨片行是否还能被拼回来。

# 只改这里的值,提问固定不变
MAX_SIDE_LIST = [896, 1120, 1344]
SLICE_OVERLAP_LIST = [0, 小重叠]  # 具体字段名按你的 processor 填
长边/像素上限是否切分切片数重叠结果错误位置
待填待填待填待填待填待填

这一步的判断口径是看“错误位置是否随尺寸改变而移动”。若把长边调大后错误行消失,说明是清晰度或切分引起;若无论怎么调错都在同一处,更可能是提问范围或表格本身的合并单元格在干扰。注意:调大尺寸会带来更长的视觉 token 和更高耗时,需要在你的环境里权衡,不要把它当成无代价的开关。

把稳定复现的那组参数固化成调用模板

找到一档能稳定读对的组合后,把它固化成函数,让同一类表格图每次都走同一条输入路径,而不是每次手改参数。骨架大概是这样:

def build_inputs(proc, img_path, prompt, fixed):
    im = Image.open(img_path).convert("RGB")
    im = im.resize(fixed["size"], Image.BICUBIC)
    inputs = proc(images=im, text=prompt, return_tensors="pt",
                  max_pixels=fixed["max_pixels"])
    return inputs, tuple(inputs["pixel_values"].shape)

FIXED = {"size": (1120, 1120), "max_pixels": None, "prompt": region_prompt}

一次验证运行的观察点建议只盯四条:pixel_values 的形状是否和记录一致、切片数是否一致、输出字段个数是否等于表列数、输出里是否混进了表头文字。四条都对,才把这条路径留给同类表格图复用;只要有一条对不上,就别扩大适用范围。

最后提醒一句边界:这套对照能帮你分清“切分”和“提问”各占多少责任,但结论只对同类版式的表格图成立。表格结构、字号、是否有合并单元格一变,建议重新走一遍前四步,不要直接把参数套过去。