正在收集工作区信息正在筛选到最相关的信息这是一个非常好的问题!让我详细解释 bbox 坐标的处理逻辑和原因。
# 原始代码
para_content['bbox'] = [
int(x0 * 1000 / page_width), # 归一化到 [0, 1000] 范围
int(y0 * 1000 / page_height),
int(x1 * 1000 / page_width),
int(y1 * 1000 / page_height),
]
核心目的:将 bbox 坐标从绝对坐标转换为相对坐标(归一化到 0-1000 范围)
让我用流程图说明:
graph TB
Start([PDF 原始文档]) --> Convert[PDF → 图片转换<br/>DPI=200]
Convert --> ImageCoord[图片坐标系统<br/>分辨率依赖]
ImageCoord --> Example1[示例1: 小页面<br/>page_size: 1654x2338<br/>bbox: 378,111,591,142]
ImageCoord --> Example2[示例2: 大页面<br/>page_size: 3308x4676<br/>bbox: 756,222,1182,284]
Example1 --> Normalize1[归一化<br/>x0: 378/1654*1000=228<br/>y0: 111/2338*1000=47]
Example2 --> Normalize2[归一化<br/>x0: 756/3308*1000=228<br/>y0: 222/4676*1000=47]
Normalize1 --> Same[✅ 相同的归一化坐标<br/>228,47,...]
Normalize2 --> Same
Same --> Benefits[优势]
Benefits --> Benefit1[1. 分辨率无关<br/>不同DPI结果一致]
Benefits --> Benefit2[2. 设备无关<br/>手机/电脑显示一致]
Benefits --> Benefit3[3. 易于比较<br/>不同页面可比]
Benefits --> Benefit4[4. 数据紧凑<br/>0-1000整数范围]
style Start fill:#e1f5ff
style Same fill:#c8e6c9
style Benefits fill:#fff3e0
# PDF 原始尺寸(点单位, 72 DPI)
pdf_page_size = [595, 842] # A4 纸
# 转换为图片(200 DPI)
dpi = 200
scale = dpi / 72
image_size = [595 * scale, 842 * scale] = [1653, 2339]
# 如果使用 300 DPI
dpi = 300
scale = 300 / 72
image_size = [595 * scale, 842 * scale] = [2480, 3508]
问题:同一个 PDF 元素,在不同 DPI 下的绝对坐标不同!
# VLM 模型通常会 resize 图片
original_image_size = (1654, 2338)
model_input_size = (1000, 1000) # 模型固定输入
# 如果不归一化,bbox 坐标会失效
original_bbox = [378, 111, 591, 142]
# 需要按比例缩放才能对应到模型输入
# 原始坐标(绝对坐标)
bbox_absolute = [378, 111, 591, 142]
page_size = (1654, 2338)
# 归一化后(相对坐标,0-1000 范围)
bbox_normalized = [
378 * 1000 / 1654, # 228
111 * 1000 / 2338, # 47
591 * 1000 / 1654, # 357
142 * 1000 / 2338, # 60
]
# 结果: [228, 47, 357, 60]
优势:
# 选项1: 归一化到 [0, 1]
bbox_float = [0.228, 0.047, 0.357, 0.060] # 浮点数
# 选项2: 归一化到 [0, 1000]
bbox_int = [228, 47, 357, 60] # 整数
# 优势对比:
# 1. 整数占用空间更小(JSON 序列化)
# 2. 整数计算更快(无浮点误差)
# 3. 1000 提供足够精度(0.1% 精度)
# 4. 符合行业惯例(很多 CV 模型使用 1000)
# ========== 第1步: VLM 模型识别(使用归一化坐标) ==========
# 模型输出(已归一化)
model_output = {
"bbox": [228, 47, 357, 60], # 0-1000 范围
"type": "text",
"content": "审计报告"
}
# ========== 第2步: 保存到 JSON(直接使用归一化坐标) ==========
content_list = [
{
"type": "text",
"text": "审计报告",
"bbox": [228, 47, 357, 60], # 归一化坐标
"page_idx": 0
}
]
# ========== 第3步: 还原到原始坐标(用于可视化) ==========
def denormalize_bbox(bbox_normalized, page_size):
"""将归一化坐标还原为绝对坐标"""
page_width, page_height = page_size
x0, y0, x1, y1 = bbox_normalized
return [
int(x0 * page_width / 1000), # 228 * 1654 / 1000 = 377
int(y0 * page_height / 1000), # 47 * 2338 / 1000 = 109
int(x1 * page_width / 1000), # 357 * 1654 / 1000 = 590
int(y1 * page_height / 1000), # 60 * 2338 / 1000 = 140
]
# ========== 第4步: 在图片上绘制 bbox ==========
from PIL import Image, ImageDraw
img = Image.open("page_001.png")
draw = ImageDraw.Draw(img)
page_size = (1654, 2338)
bbox_normalized = [228, 47, 357, 60]
# 还原坐标
bbox_absolute = denormalize_bbox(bbox_normalized, page_size)
# [377, 109, 590, 140]
draw.rectangle(bbox_absolute, outline='red', width=2)
img.save("output_with_bbox.png")
查看 pipeline_middle_json_mkcontent.py:
# Line 240-250
page_width, page_height = page_size
para_bbox = para_block.get('bbox')
if para_bbox:
x0, y0, x1, y1 = para_bbox
para_content['bbox'] = [
int(x0 * 1000 / page_width), # 同样的归一化处理
int(y0 * 1000 / page_height),
int(x1 * 1000 / page_width),
int(y1 * 1000 / page_height),
]
vlm_middle_json_mkcontent.py Line 218-228
两个后端使用相同的归一化策略,确保输出格式一致。
| 特性 | 绝对坐标 | 归一化坐标(0-1000) |
|---|---|---|
| 分辨率依赖 | ❌ 依赖 DPI | ✅ 无关 |
| 设备兼容性 | ❌ 需要适配 | ✅ 通用 |
| 数据大小 | ❌ 较大(浮点) | ✅ 紧凑(整数) |
| 计算精度 | ❌ 浮点误差 | ✅ 整数精确 |
| 模型友好 | ❌ 不友好 | ✅ 标准格式 |
| 可视化 | ✅ 直接使用 | ⚠️ 需要反归一化 |
最佳实践: