
如果你在 PDF 解析里走过图像路线,大概率都经历过某个时刻:
- 服务刚上线跑得好好的
- 某天来了一份"看起来也没多大"的 PDF
- 然后内存直接飙升
- 最后以一个毫无尊严的 OOM 结束
一开始我也以为问题出在模型上。
- 是不是模型太大?
- batch 开多了?
- GPU 显存不够?
后来才意识到一个非常残酷的事实:
很多图像路线的 OOM,在模型跑之前就已经注定了。
一个最常见、也最致命的误区:假设 PDF 页面是 A4
我一开始做 PDF 渲染时,犯过一个非常典型的错误:
默认 PDF 页面就是 A4。
既然是 A4,那我就可以:
- 拍一个固定 DPI
- 300 dpi、400 dpi
- 渲出来的图像清清楚楚
- 模型效果看起来也不错
直到有一天,系统收到了这样一页 PDF:
- 宽高比极端
- 页面尺寸巨大
- 但文件体积并不大
渲染一开,内存直接炸掉。
这时候你才会意识到:
PDF 页面大小和"纸张大小"没有任何强制关系。
它可能是:
- 一整张海报
- 一个长条账单
- 一个被错误导出的巨大画布
- 甚至是拼接过的扫描页
而你用 A4 的认知去渲染它,本身就是在赌运气。
误区:默认 PDF 页 = A4
固定 DPI 渲染 (比如 300dpi)
遇到超大 BBox 页面
渲染像素数 = (page_w * dpi) × (page_h * dpi)
像素数暴涨 → 单页图像内存暴涨
并发页数 × 单页内存 → 进程内存瞬间打满
OOM / 被系统杀掉
图:固定 DPI 渲染为什么会炸
DPI 这东西,本质上是在"放大世界"
工程里经常能看到这样的代码:
python
render_page(dpi=300)
看起来很合理,对吧?但 DPI 的真实含义是:
把 PDF 的 user space,按比例放大到像素空间。
如果页面本身就很大,那 DPI 就是在帮你把"大问题放得更大"。这也是为什么:
- 同样是 300 dpi
- 有的页面渲出来几十 MB
- 有的页面直接上百 MB
问题根本不在 DPI 本身,而在:
你是在对多大的页面用这个 DPI。
一点直观到残酷的内存计算
我们可以直接算算这笔账,因为这比任何提醒都有效。假设我们渲染的是 BGR 三通道 uint8 的 NDArray:
单张图像内存 ≈ width × height × 3 bytes
来看几个非常常见、但容易被忽略的数字:
情况一:A4 页面,300 DPI
- A4 ≈ 8.27 × 11.69 inch
- 300 dpi → 2480 × 3508
- 像素数 ≈ 8.7M
- 内存占用 ≈ 26 MB / 页
这已经不算小了。
情况二:看起来"只是大一点"的页面
- 渲染后尺寸:6000 × 8000
- 像素数:48M
- 内存占用 ≈ 144 MB / 页
如果你:
- 并发 4 页
- 再 copy 几次数组
- 再丢进模型前处理
内存瞬间就没了。
情况三:极端情况(真实见过)
- 渲染后尺寸:10000 × 10000
- 像素数:100M
- 内存占用 ≈ 300 MB / 页
这一页 PDF,可能在你服务里,比整个模型还值钱。
下表进行了总结:
| 渲染后尺寸(W×H) | 像素数(约) | 单张 BGR 图像内存(约) |
|---|---|---|
| 2480×3508(A4@300dpi 常见量级) | 8.7M | 26 MB |
| 3000×4000 | 12.0M | 36 MB |
| 4000×6000 | 24.0M | 72 MB |
| 6000×8000 | 48.0M | 144 MB |
| 8000×8000 | 64.0M | 192 MB |
| 10000×10000 | 100.0M | 300 MB |
说明:按 BGR / uint8 / 3 通道计算,单页图像内存 ≈ W×H×3 bytes
注意这只是"单张原始图"。一旦进入预处理、copy、resize、tensor 化,实际占用常常是它的 2~5 倍。
正确的工程思路:不要从 DPI 出发
后来我彻底换了一种思路。
不再从"我用多少 DPI"开始想问题,而是反过来:
我的模型,最终需要多大的输入?
这是一个完全不同的起点。
比如:
- 布局模型期望:
- 短边 1024
- 或长边不超过 1600
- OCR 模型期望:
- 高度有限
- 多模态模型:
- 本身会做 resize
那我就问一个更现实的问题:
那我为什么要渲染一张比模型最终用到的尺寸还大的图?
我的实际做法:按目标尺寸反推渲染比例
在工程里,我后来采用的是这样一套逻辑:
- 先确定模型侧的目标尺寸
- 比如 max(width, height) ≤ 1600
- 或按短边对齐
- 读取当前 PDF 页面的 BBox
- 得到原始宽高(user space)
- 动态计算缩放比例
- 令渲染后的图像,直接落在目标尺寸附近
- 需要的话,稍微预留一点冗余
- 用这个比例去渲染页面
- 而不是用固定 DPI
这样做的结果是:
- 每一页图像的最大尺寸是可控的
- 单页内存上限是可预期的
- 再大的 PDF 页面,也不会"突然爆炸"
方案 A:固定 DPI(容易炸)
读取 PDF
固定 dpi=300 渲染
输出大图(尺寸不可控)
内存 / 耗时随页面 BBox 飘
方案 B:目标尺寸反推渲染(可控)
读取 PDF
读取当前页 BBox(w,h)
设定模型目标尺寸\n(max_side ≤ S)
计算缩放比例\nscale = S / max(w,h)
按 scale 渲染
单页最大内存可预期
图:固定DPI和动态DPI方案
控制并发,才是最后一道保险
哪怕你把单页图像控制住了,还有一件事不能忽略:
并发页数 × 单页内存 = 服务的真实压力
我的经验是:
图像路线的并发度,一定要单独控制,而且要比纯文本路线保守得多
这一步的目标不是压榨性能,而是:
保证任何时候,内存占用都有明确上限。
只要你能做到:
- 单页最大图像内存可控
- 并发页数有限
OOM 风险会直线下降。
一个很现实的结论
做到最后,我对图像路线的看法变得非常现实:
图像路线不是"慢",而是"贵"。
贵的不是模型,而是:
- 像素
- 内存
- 带宽
- copy
- 并发
而 DPI,只是那个最容易被滥用的放大器。
小结:模型还没跑,你的内存已经决定结局了
如果你在图像路线里频繁遇到 OOM,我的建议永远是:
- 先别换模型
- 先别调 batch
- 先回头看看:
- 你渲染了多大的图
- 你是怎么决定这个尺寸的
很多时候,问题在模型开始工作之前,就已经发生了。
收个尾:图像路线的痛点,不止在显存里
做到第 11 章这个位置,心里大概已经有数了:
- 不能盲目固定 DPI
- 页面尺寸不能想当然
- 并发配额必须像银行额度一样管理
- resize 不是数学操作,而是内存调度策略
当这些工程动作逐渐成型,你会感觉系统 finally 能上路了。至少不至于在模型还没跑起来前,就被自己炸出 OOM。
但新的问题往往也会在这个阶段冒出来:
"明明模型很好,怎么边缘总是识别得像喝醉了一样?"
当你把图像渲染得干干净净,模型却一靠近边缘就开始失手:文本漏、表格断、bbox贴墙就歪、结构模型像没睡醒。这时候,你会意识到:
控制分辨率 ≠ 控制模型状态。
内存和算力的问题解决了,但模型本身的"舒适区"还没被照顾好。换句话说
我们帮模型把房间清理干净了,但它还坐在角落里发呆。
接下来的一章,我们就来聊一个看似离谱但在工程实战中效果夸张地稳定的技巧:
给模型一点"私人空间"。在四周加一圈空白。
不是为了美观,也不是玄学,而是因为------模型不喜欢贴着墙走。