mineru_处理流程.md 26 KB

正在收集工作区信息正在筛选到最相关的信息根据 demo.py 的代码分析,我来生成 MinerU 的处理流程图:

1. MinerU 处理流程图

graph TB
    Start([开始: 输入PDF/图片文件]) --> ReadFile[read_fn: 读取文件字节]
    ReadFile --> CheckBackend{选择后端?}
  
    %% Pipeline 后端流程
    CheckBackend -->|backend=pipeline| ConvertPDF1[convert_pdf_bytes_to_bytes_by_pypdfium2<br/>转换PDF字节流]
    ConvertPDF1 --> PipelineAnalyze[pipeline_doc_analyze<br/>批量分析文档]
  
    PipelineAnalyze --> CheckMethod{检查解析方法}
    CheckMethod -->|parse_method=auto| Classify[classify模型<br/>PDF分类: txt/ocr]
    CheckMethod -->|parse_method=txt| SetTxt[设置txt模式]
    CheckMethod -->|parse_method=ocr| SetOcr[设置ocr模式]
  
    Classify --> PDFType{PDF类型?}
    SetTxt --> LoadImages1
    SetOcr --> LoadImages1
  
    PDFType -->|文字PDF txt模式| LoadImages1[load_images_from_pdf<br/>加载PDF页面图像<br/>data: images_list, pdf_doc]
    PDFType -->|图片PDF ocr模式| LoadImages1
  
    LoadImages1 --> BatchInfer[batch_image_analyze<br/>批量推理]
  
    BatchInfer --> LayoutModel[布局检测模型<br/>MinerU Layout Detection]
    LayoutModel --> LayoutResult[layout_det结果<br/>检测文本/表格/图片区域]
  
    LayoutResult --> OCRBranch{需要OCR?}
    OCRBranch -->|txt模式: 文字PDF| ExtractText[提取嵌入文本<br/>直接读取PDF文本层]
    OCRBranch -->|ocr模式: 图片PDF| OCRModel[OCR识别模型<br/>PaddleOCR/Surya]
  
    ExtractText --> MergeText[文本合并]
    OCRModel --> OCRResult[OCR识别结果<br/>文本块坐标+内容]
    OCRResult --> MergeText
  
    MergeText --> FormulaCheck{公式识别?}
    FormulaCheck -->|formula_enable=True| FormulaModel[公式识别模型<br/>UniMERNet]
    FormulaCheck -->|formula_enable=False| TableCheck
    FormulaModel --> FormulaResult[公式LaTeX结果]
    FormulaResult --> TableCheck
  
    TableCheck{表格识别?}
    TableCheck -->|table_enable=True| TableModel[表格识别模型<br/>StructEqTable]
    TableCheck -->|table_enable=False| ModelList
    TableModel --> TableResult[表格HTML结果]
    TableResult --> ModelList
  
    ModelList[生成model_list<br/>原始模型输出JSON] --> ToMiddleJson[pipeline_result_to_middle_json<br/>转换为中间JSON格式]
  
    ToMiddleJson --> MiddleJson[middle_json<br/>包含pdf_info数组]
    MiddleJson --> ProcessOutput1[_process_output<br/>处理输出]
  
    %% VLM 后端流程
    CheckBackend -->|backend=vlm-xxx| ConvertPDF2[convert_pdf_bytes_to_bytes_by_pypdfium2<br/>转换PDF字节流]
    ConvertPDF2 --> VLMAnalyze[vlm_doc_analyze<br/>VLM端到端分析]
  
    VLMAnalyze --> CheckVLMBackend{VLM后端类型?}
    CheckVLMBackend -->|transformers| VLMTransformers[加载本地VLM模型<br/>MinerU-VLM/MinerU2.5-2509-1.2B]
    CheckVLMBackend -->|vllm-engine| VLMEngine[vLLM引擎<br/>本地GPU推理]
    CheckVLMBackend -->|http-client| VLMClient[HTTP客户端<br/>连接远程vLLM服务器]
  
    VLMTransformers --> VLMInfer[VLM模型推理<br/>输入: PDF图像<br/>输出: 结构化JSON]
    VLMEngine --> VLMInfer
    VLMClient --> HTTPRequest[发送HTTP请求<br/>server_url]
    HTTPRequest --> VLMInfer
  
    VLMInfer --> VLMResult[VLM输出结果<br/>直接生成结构化内容]
    VLMResult --> VLMMiddleJson[result_to_middle_json<br/>转换为middle_json格式]
    VLMMiddleJson --> ProcessOutput2[_process_output<br/>处理输出]
  
    %% 输出处理流程
    ProcessOutput1 --> DrawLayout{绘制布局框?}
    ProcessOutput2 --> DrawLayout
  
    DrawLayout -->|f_draw_layout_bbox=True| DrawLayoutPDF[draw_layout_bbox<br/>生成layout.pdf]
    DrawLayout -->|False| DrawSpan
    DrawLayoutPDF --> DrawSpan
  
    DrawSpan{绘制span框?}
    DrawSpan -->|f_draw_span_bbox=True| DrawSpanPDF[draw_span_bbox<br/>生成span.pdf]
    DrawSpan -->|False| MakeMarkdown
    DrawSpanPDF --> MakeMarkdown
  
    MakeMarkdown{生成Markdown?}
    MakeMarkdown -->|f_dump_md=True| UnionMake1[pipeline_union_make 或<br/>vlm_union_make]
    MakeMarkdown -->|False| ContentList
  
    UnionMake1 --> MarkdownFile[输出: xxx.md<br/>包含文本/公式/表格/图片]
    MarkdownFile --> ContentList
  
    ContentList{生成内容列表?}
    ContentList -->|f_dump_content_list=True| UnionMake2[union_make<br/>MakeMode.CONTENT_LIST]
    ContentList -->|False| MiddleJsonOut
    UnionMake2 --> ContentListFile[输出: xxx_content_list.json<br/>结构化内容列表]
    ContentListFile --> MiddleJsonOut
  
    MiddleJsonOut{保存中间JSON?}
    MiddleJsonOut -->|f_dump_middle_json=True| MiddleJsonFile[输出: xxx_middle.json<br/>中间处理结果]
    MiddleJsonOut -->|False| ModelOut
    MiddleJsonFile --> ModelOut
  
    ModelOut{保存模型输出?}
    ModelOut -->|f_dump_model_output=True| ModelFile[输出: xxx_model.json<br/>原始模型输出]
    ModelOut -->|False| End
    ModelFile --> End([结束: 所有文件保存到output_dir])
  
    %% 样式定义
    classDef modelClass fill:#e1f5ff,stroke:#01579b,stroke-width:2px
    classDef dataClass fill:#fff3e0,stroke:#e65100,stroke-width:2px
    classDef processClass fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
  
    class LayoutModel,OCRModel,FormulaModel,TableModel,VLMTransformers,VLMEngine,Classify modelClass
    class LayoutResult,OCRResult,FormulaResult,TableResult,ModelList,MiddleJson,VLMResult dataClass
    class PipelineAnalyze,VLMAnalyze,ToMiddleJson,VLMMiddleJson,ProcessOutput1,ProcessOutput2 processClass

关键差异说明

1. 文字PDF (txt模式) vs 图片PDF (ocr模式)

维度 文字PDF (txt模式) 图片PDF (ocr模式)
检测方式 classify 模型自动判断 手动指定或分类识别
文本提取 直接读取PDF嵌入的文本层 使用OCR模型识别图像中的文字
处理速度 快速(无需OCR) 较慢(需要OCR推理)
准确性 高(原生文本) 取决于OCR模型质量
典型场景 Word转PDF、电子书 扫描件、截图PDF

2. Pipeline vs VLM 后端差异

特性 Pipeline后端 VLM后端
模型架构 多个专用模型(Layout+OCR+公式+表格) 单一端到端VLM模型
处理流程 分步骤处理,每步调用不同模型 一次性输出结构化结果
优势 可控性强,每步可优化 简化流程,理解能力强
劣势 流程复杂,错误累积 需要大模型支持
适用场景 通用文档解析 复杂布局/多语言混排

3. 关键数据流

# Pipeline模式数据流
PDF字节流 → 图像列表(images_list) → 模型推理(model_list) → 中间JSON(middle_json) → Markdown

# VLM模式数据流
PDF字节流 → 图像列表 → VLM推理(infer_result) → 中间JSON(middle_json) → Markdown

4. 输出文件说明

  • xxx.md: 最终Markdown文档
  • xxx_content_list.json: 结构化内容列表(用于程序化处理)
  • xxx_middle.json: 中间处理结果(包含完整的 pdf_info数组)
  • xxx_model.json: 原始模型输出(用于调试)
  • xxx_layout.pdf: 布局可视化(标注文本/表格/图片区域)
  • xxx_span.pdf: span级别可视化(标注每个文本块)

2. PaddleOCR 模型架构分析

[10.192.72.11:~/models/modelscope_cache/models/OpenDataLab/PDF-Extract-Kit-1___0/models]$ find .
.
./MFR
./MFR/unimernet_hf_small_2503
./MFR/unimernet_hf_small_2503/config.json
./MFR/unimernet_hf_small_2503/model.safetensors
./MFR/unimernet_hf_small_2503/README.md
./MFR/unimernet_hf_small_2503/tokenizer_config.json
./MFR/unimernet_hf_small_2503/tokenizer.json
./MFR/unimernet_hf_small_2503/special_tokens_map.json
./MFR/unimernet_hf_small_2503/generation_config.json
./TabRec
./TabRec/UnetStructure
./TabRec/UnetStructure/unet.onnx
./TabRec/SlanetPlus
./TabRec/SlanetPlus/slanet-plus.onnx
./ReadingOrder
./ReadingOrder/layout_reader
./ReadingOrder/layout_reader/config.json
./ReadingOrder/layout_reader/model.safetensors
./OCR
./OCR/paddleocr_torch
./OCR/paddleocr_torch/latin_PP-OCRv5_rec_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv3_det_infer.pth
./OCR/paddleocr_torch/en_PP-OCRv3_det_infer.pth
./OCR/paddleocr_torch/te_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv4_det_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv5_rec_server_infer.pth
./OCR/paddleocr_torch/en_PP-OCRv4_rec_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv4_rec_server_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv5_rec_infer.pth
./OCR/paddleocr_torch/korean_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/korean_PP-OCRv5_rec_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv4_rec_server_doc_infer.pth
./OCR/paddleocr_torch/en_PP-OCRv5_rec_infer.pth
./OCR/paddleocr_torch/ch_ptocr_mobile_v2.0_cls_infer.pth
./OCR/paddleocr_torch/th_PP-OCRv5_rec_infer.pth
./OCR/paddleocr_torch/latin_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/el_PP-OCRv5_rec_infer.pth
./OCR/paddleocr_torch/Multilingual_PP-OCRv3_det_infer.pth
./OCR/paddleocr_torch/arabic_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv4_rec_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv5_det_infer.pth
./OCR/paddleocr_torch/devanagari_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/eslav_PP-OCRv5_rec_infer.pth
./OCR/paddleocr_torch/ta_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/cyrillic_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/ka_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/chinese_cht_PP-OCRv3_rec_infer.pth
./OCR/paddleocr_torch/japan_PP-OCRv3_rec_infer.pth
./TabCls
./TabCls/paddle_table_cls
./TabCls/paddle_table_cls/PP-LCNet_x1_0_table_cls.onnx
./Layout
./Layout/YOLO
./Layout/YOLO/doclayout_yolo_docstructbench_imgsz1280_2501.pt
./OriCls
./OriCls/paddle_orientation_classification
./OriCls/paddle_orientation_classification/PP-LCNet_x1_0_doc_ori.onnx
./MFD
./MFD/YOLO
./MFD/YOLO/yolo_v8_ft.pt

# mineru调用的paddle模型是已经什么架构?是pytouch还是paddle自有文件格式,转换了吗?

1. 模型文件格式

从文件列表可以看到:

./OCR/paddleocr_torch/ch_PP-OCRv4_det_infer.pth
./OCR/paddleocr_torch/ch_PP-OCRv5_rec_infer.pth
# ... 等等

关键发现

  • ✅ 文件扩展名是 .pth (PyTorch 格式)
  • ✅ 目录名是 paddleocr_torch(明确标注了 torch)
  • ✅ 这些是已经转换为 PyTorch 格式的模型

2. 原始 Paddle 模型转换

查看 MinerU 的模型加载代码:

# mineru/model/ocr_rec/paddleocr_rec/paddleocr_rec.py
class RecPaddleOCRX:
    def __init__(self, rec_model_path, lang=None):
        # 加载 PyTorch 模型
        self.model = torch.jit.load(rec_model_path)  # 或者直接 torch.load

3. 转换流程说明

MinerU 使用的是 Paddle2ONNX → PyTorch直接转换到 PyTorch 的模型:

graph LR
    A[原始 PaddlePaddle 模型<br/>.pdparams + .pdmodel] --> B{转换方式}
  
    B -->|方式1| C[Paddle2ONNX<br/>转换为 .onnx]
    B -->|方式2| D[paddle2torch<br/>直接转 PyTorch]
  
    C --> E[ONNX Runtime<br/>推理]
    C --> F[ONNX -> PyTorch<br/>转换为 .pth]
  
    D --> G[PyTorch 模型<br/>.pth 文件]
    F --> G
  
    G --> H[MinerU 使用<br/>torch.load/jit.load]
    E --> I[MinerU 使用<br/>onnxruntime]
  
    style A fill:#ffe0b2
    style G fill:#c8e6c9
    style H fill:#bbdefb

4. 实际使用的模型格式对比

模型类型 原始格式 MinerU 使用格式 推理引擎
OCR .pdparams .pth (PyTorch) PyTorch
表格分类 .pdparams .onnx ONNX Runtime
方向分类 .pdparams .onnx ONNX Runtime
Layout检测 - .pt (YOLO原生) PyTorch
公式识别 - .safetensors PyTorch

5. 代码验证

查看 paddleocr_rec.py:

class RecPaddleOCRX:
    def __init__(self, rec_model_path, ...):
        # 加载 PyTorch 格式的模型
        if rec_model_path.endswith('.pth'):
            # PyTorch JIT 格式
            self.model = torch.jit.load(rec_model_path, map_location=self.device)
        else:
            # 标准 PyTorch 格式
            checkpoint = torch.load(rec_model_path, map_location=self.device)
            self.model.load_state_dict(checkpoint)

查看 paddleocr_det.py:

class DetPaddleOCRX:
    def __init__(self, det_model_path, ...):
        # 同样加载 PyTorch 格式
        self.model = torch.jit.load(det_model_path, map_location=self.device)

6. ONNX 格式的模型

从文件列表还可以看到部分使用 ONNX:

./TabCls/paddle_table_cls/PP-LCNet_x1_0_table_cls.onnx
./OriCls/paddle_orientation_classification/PP-LCNet_x1_0_doc_ori.onnx
./TabRec/UnetStructure/unet.onnx
./TabRec/SlanetPlus/slanet-plus.onnx

这些使用 ONNX Runtime 推理:

# mineru/model/table/rec/slanet_plus/main.py
class RapidTableModel:
    def __init__(self, model_path):
        import onnxruntime as ort
        self.session = ort.InferenceSession(
            model_path,
            providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
        )

7. 转换工具

MinerU 团队使用的转换工具可能包括:

  1. Paddle2ONNX - 官方转换工具

    paddle2onnx --model_dir paddle_model \
               --model_filename model.pdmodel \
               --params_filename model.pdiparams \
               --save_file model.onnx
    
  2. X2Paddle - Paddle 到其他框架的转换

    x2paddle --framework=paddle \
            --model=model.pdmodel \
            --params=model.pdiparams \
            --save_dir=pytorch_model \
            --to_framework=pytorch
    
  3. 自定义转换脚本 - 直接映射权重

    import paddle
    import torch
    
    # 加载 Paddle 模型
    paddle_state_dict = paddle.load('model.pdparams')
    
    # 转换为 PyTorch 格式
    torch_state_dict = convert_paddle_to_torch(paddle_state_dict)
    
    # 保存为 .pth
    torch.save(torch_state_dict, 'model.pth')
    

总结

MinerU 中 PaddleOCR 模型的实际情况

特性 说明
原始格式 PaddlePaddle (.pdparams + .pdmodel)
转换后格式 PyTorch (.pth) 或 ONNX (.onnx)
推理引擎 PyTorch / ONNX Runtime
是否转换 已转换,不是原生 Paddle 格式
优势 统一推理引擎,无需安装 PaddlePaddle

为什么要转换

  1. 依赖简化:无需安装 PaddlePaddle 框架
  2. 性能优化:PyTorch/ONNX 推理可能更快
  3. 部署方便:PyTorch 生态更成熟
  4. 统一架构:其他模型都是 PyTorch,统一管理

所以答案是:MinerU 使用的是已经转换为 PyTorch (.pth) 或 ONNX (.onnx) 格式的 PaddleOCR 模型,不是原生 Paddle 格式

Two Step Extract 流程解析

1. VLM 是否调用版式识别模型?

是的vlm_doc_analyze 会调用版式识别模型。从代码可以看到:

# vlm_analyze.py
results = predictor.batch_two_step_extract(images=images_pil_list)

这里的 batch_two_step_extract 内部第一阶段就是版式识别(Layout Detection)。

2. 两阶段识别详细流程

graph TB
    Start([输入: PDF页面图像列表]) --> BatchExtract[batch_two_step_extract]
  
    BatchExtract --> Stage1[第一阶段<br/>Layout Detection 版式识别]
  
    Stage1 --> Step1[步骤1: prepare_for_layout<br/>调整图像到1036x1036]
    Step1 --> Step2[步骤2: VLM推理<br/>prompt: Layout Detection]
    Step2 --> Step3[步骤3: parse_layout_output<br/>解析输出获取blocks]
  
    Step3 --> Example[输出示例<br/>box_start 100 200 300 400 box_end<br/>ref_start table ref_end<br/>type=table bbox=归一化坐标]
  
    Example --> Stage2[第二阶段<br/>Content Extraction 内容提取]
  
    Stage2 --> Step4{步骤4: FilterBlocks<br/>过滤blocks}
    Step4 -->|跳过| Skip[SkipTypes<br/>image/list/equation_block]
    Step4 -->|处理| Process[ProcessTypes<br/>text/title/table/equation]
  
    Process --> Step5[步骤5: CropBlocks<br/>裁剪每个block区域]
    Step5 --> Step6[步骤6: RotateBlocks<br/>根据angle旋转<br/>0/90/180/270度]
    Step6 --> Step7[步骤7: ResizeBlocks<br/>最小边大于等于28px<br/>宽高比小于等于50:1]
  
    Step7 --> Step8[步骤8: PreparePrompts<br/>准备每个block的prompt]
    Step8 --> Prompts[Prompt类型<br/>table: Table Recognition<br/>equation: Formula Recognition<br/>text: Text Recognition]
  
    Prompts --> Step9[步骤9: BatchInfer<br/>VLM批量推理]
    Step9 --> Step10[步骤10: FillContent<br/>填充blocks的content字段]
  
    Step10 --> Step11[步骤11: PostProcess<br/>后处理]
    Step11 --> Actions[处理动作<br/>1.合并相邻文本块<br/>2.处理公式块<br/>3.清理列表项]
  
    Actions --> Final([最终输出<br/>list of ContentBlock<br/>type + bbox + content])
    Skip --> Final
  
    classDef stage1Class fill:#e1f5ff,stroke:#01579b,stroke-width:3px
    classDef stage2Class fill:#fff3e0,stroke:#e65100,stroke-width:3px
    classDef processClass fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    classDef skipClass fill:#ffebee,stroke:#c62828,stroke-width:2px
    classDef exampleClass fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
  
    class Stage1,Step1,Step2,Step3 stage1Class
    class Stage2,Step5,Step6,Step7,Step9 stage2Class
    class Step8,Step10,Step11,Actions processClass
    class Skip skipClass
    class Example,Prompts exampleClass

3. 两阶段详细代码分析

阶段1: Layout Detection(版式识别)

# mineru_client.py - batch_layout_detect
def batch_layout_detect(self, images: list[Image.Image]) -> list[list[ContentBlock]]:
    # 1. 准备图像:resize到1036x1036
    layout_images = self.helper.batch_prepare_for_layout(self.executor, images)
  
    # 2. 使用固定的Layout Detection prompt
    prompt = "[layout]"  # 实际为: "\nLayout Detection:"
    params = DEFAULT_SAMPLING_PARAMS["[layout]"]  # temperature=0.0, top_k=1
  
    # 3. VLM推理
    outputs = self.client.batch_predict(layout_images, prompt, params)
  
    # 4. 解析输出
    return self.helper.batch_parse_layout_output(self.executor, outputs)

输出格式示例

<|box_start|>100 200 300 400<|box_end|><|ref_start|>table<|ref_end|><|rotate_up|>
<|box_start|>50 100 200 150<|box_end|><|ref_start|>text<|ref_end|>
<|box_start|>250 300 450 500<|box_end|><|ref_start|>equation<|ref_end|>

解析后的 ContentBlock

ContentBlock(
    type="table",
    bbox=[0.1, 0.2, 0.3, 0.4],  # 归一化坐标 (0-1)
    angle=0,  # 从<|rotate_up|>解析
    content=None  # 第一阶段不填充
)

阶段2: Content Extraction(内容提取)

# mineru_client.py - stepping_two_step_extract
def stepping_two_step_extract(self, images: list[Image.Image]) -> list[list[ContentBlock]]:
    # 1. 获取第一阶段的blocks
    blocks_list = self.batch_layout_detect(images)
  
    # 2. 准备提取内容
    all_images: list[Image.Image | bytes] = []
    all_prompts: list[str] = []
    all_params: list[SamplingParams | None] = []
    all_indices: list[tuple[int, int]] = []
  
    for img_idx, (image, blocks) in enumerate(zip(images, blocks_list)):
        # 3. 准备每个block
        for idx, block in enumerate(blocks):
            # 跳过不需要提取的类型
            if block.type in ("image", "list", "equation_block"):
                continue
          
            # 4. 裁剪block区域
            x1, y1, x2, y2 = block.bbox
            scaled_bbox = (x1 * width, y1 * height, x2 * width, y2 * height)
            block_image = image.crop(scaled_bbox)
          
            # 5. 根据角度旋转
            if block.angle in [90, 180, 270]:
                block_image = block_image.rotate(block.angle, expand=True)
          
            # 6. resize处理
            block_image = self.resize_by_need(block_image)
          
            # 7. 准备prompt(根据block类型)
            if block.type == "table":
                prompt = "\nTable Recognition:"
                params = MinerUSamplingParams(presence_penalty=1.0, frequency_penalty=0.005)
            elif block.type == "equation":
                prompt = "\nFormula Recognition:"
                params = MinerUSamplingParams(presence_penalty=1.0, frequency_penalty=0.05)
            else:  # text, title等
                prompt = "\nText Recognition:"
                params = MinerUSamplingParams(presence_penalty=1.0, frequency_penalty=0.05)
          
            all_images.append(block_image)
            all_prompts.append(prompt)
            all_params.append(params)
            all_indices.append((img_idx, idx))
  
    # 8. 批量推理
    outputs = self.client.batch_predict(all_images, all_prompts, all_params)
  
    # 9. 填充content
    for (img_idx, idx), output in zip(all_indices, outputs):
        blocks_list[img_idx][idx].content = output
  
    # 10. 后处理
    return self.helper.batch_post_process(self.executor, blocks_list)

4. 为什么要两阶段?

设计原因

1. 任务分离,提高准确性

# 第一阶段:专注于"在哪里" (Where)
# - 输入:整页图像 (1036x1036)
# - 输出:所有元素的位置和类型
# - 优势:全局视野,准确定位

# 第二阶段:专注于"是什么" (What)
# - 输入:单个元素的裁剪图像
# - 输出:该元素的具体内容
# - 优势:局部细节,精确识别

2. 不同的优化目标

特性 第一阶段 (Layout) 第二阶段 (Extract)
图像尺寸 固定 1036x1036 动态(根据block大小)
Sampling参数 temperature=0.0(确定性) 根据类型调整
Prompt 统一的Layout Detection 针对性(Table/Formula/Text)
推理目标 结构化输出(bbox+type) 内容识别
批处理方式 按页批处理 按block批处理

3. 处理效率

# 示例:10页文档,每页5个blocks
# 方法1:单阶段(每个block独立识别)
total_infer = 10 * 5 = 50次推理

# 方法2:两阶段
total_infer = 10 (layout) + 50 (extract) = 60次推理
# 但是:
# - Layout阶段可以大batch(固定尺寸)
# - Extract阶段可以按类型分组(table一批,text一批)
# - 跳过image/list等不需要提取的block(实际<50次)

4. 不同类型的特殊处理

# mineru_client.py - prepare_for_extract
def prepare_for_extract(self, image, blocks):
    for block in blocks:
        # 跳过不需要提取内容的类型
        if block.type in ("image", "list", "equation_block"):
            continue  # 节省推理成本
      
        # 表格:需要高precision
        if block.type == "table":
            params = MinerUSamplingParams(
                presence_penalty=1.0,
                frequency_penalty=0.005  # 允许重复(表格单元格)
            )
      
        # 公式:需要严格格式
        elif block.type == "equation":
            params = MinerUSamplingParams(
                presence_penalty=1.0,
                frequency_penalty=0.05  # 限制重复
            )
      
        # 文本:平衡准确性和流畅性
        else:
            params = MinerUSamplingParams(
                presence_penalty=1.0,
                frequency_penalty=0.05
            )

对比 Pipeline 后端

维度 VLM (两阶段) Pipeline (多模型)
模型数量 1个VLM模型 6+个专用模型
第一阶段 VLM Layout Detection DocLayout-YOLO
第二阶段 VLM Content Extract OCR/公式/表格各自模型
优势 端到端,理解力强 各环节可控,速度快
劣势 推理成本高 错误累积,流程复杂

5. 实际执行流程示例

# 输入:1页PDF
image = load_pdf_page()

# ========== 第一阶段 ==========
layout_output = """
<|box_start|>100 200 900 300<|box_end|><|ref_start|>title<|ref_end|>
<|box_start|>100 320 900 500<|box_end|><|ref_start|>text<|ref_end|>
<|box_start|>100 520 900 900<|box_end|><|ref_start|>table<|ref_end|>
"""

blocks = [
    ContentBlock(type="title", bbox=[0.1, 0.2, 0.9, 0.3], content=None),
    ContentBlock(type="text", bbox=[0.1, 0.32, 0.9, 0.5], content=None),
    ContentBlock(type="table", bbox=[0.1, 0.52, 0.9, 0.9], content=None),
]

# ========== 第二阶段 ==========
# 准备3个裁剪图像
crop1 = image.crop([100, 200, 900, 300])  # title
crop2 = image.crop([100, 320, 900, 500])  # text
crop3 = image.crop([100, 520, 900, 900])  # table

# 批量推理
outputs = vlm_infer([
    (crop1, "\nText Recognition:"),
    (crop2, "\nText Recognition:"),
    (crop3, "\nTable Recognition:"),
])

# 填充结果
blocks[0].content = "审计报告"
blocks[1].content = "我们审计了至远彩色印刷工业..."
blocks[2].content = "<table><tr><td>项目</td><td>金额</td></tr>...</table>"

# 后处理
final_blocks = post_process(blocks)

总结

两阶段设计的核心思想:

  1. 分而治之:第一阶段找位置,第二阶段识内容
  2. 专业化:每个阶段使用最适合的参数和prompt
  3. 效率优化:跳过不必要的处理,合理批处理
  4. 质量保证:全局定位+局部细节,双重保障

这种设计使得单个VLM模型能够达到甚至超越多模型Pipeline的效果。