乐于分享
好东西不私藏

这个文档解析模型刚刚颠覆了商业 OCR 与 VLM——96.33 SOTA、仅 0.9B 参数、完全开源

这个文档解析模型刚刚颠覆了商业 OCR 与 VLM——96.33 SOTA、仅 0.9B 参数、完全开源

字数 2524,阅读大约需 7 分钟

目录

  • • 1 PaddleOCR-VL-1.6 功能特性
  • • 2 PaddleOCR-VL-1.6 架构
  • • 3 PaddleOCR-VL-1.6 演示
  • • 4 本地部署
  • • 5 总结

这个开源模型以仅 0.9B 参数在 SOTA 上达到 96.33,全面超越 Gemini 3 Pro 和 Qwen3-VL-235B。

近期,PaddlePaddle 团队发布了最新的文档解析模型——PaddleOCR-VL-1.6。该模型拥有 0.9B 参数,是 PaddleOCR-VL-1.5 的升级版。它在 OmniDocBench v1.6 上取得了 96.33 的最高分,超越了 1.2B 参数的 MinerU2.5-Pro 模型的 95.69 分,同时也在 OmniDocBench v1.5 和 Real5-OmniDocBench 上刷新了记录。

PaddleOCR-VL-1.6 功能特性

  • • 支持文字行定位与识别,同时支持印章识别。
  • • 在文本、公式和表格识别方面达到新的 SOTA 精度,性能卓越。
  • • 支持不规则形状定位,即使文档发生倾斜、弯曲等变形,也能准确实现多边形检测。
  • • 支持跨页表格自动合并与跨页段落标题识别,解决了处理长文档时内容碎片化的问题。

PaddleOCR-VL-1.6 架构

PaddleOCR-VL-1.6 演示

在浏览器中访问 https://huggingface.co/spaces/PaddlePaddle/PaddleOCR-VL-1.6_Online_Demo,上传本地图片或选择已有样例图片,然后点击页面上的”Parse Document”按钮,即可体验 PaddleOCR-VL-1.6 模型提供的文档解析能力。

本地部署

PaddleOCR-VL-1.6 官方文档详细介绍了如何使用 PaddlePaddle 和 CUDA 运行该模型。下面将介绍如何在 macOS 上使用 mlx-vlm 在本地部署 PaddleOCR-VL-1.6 模型。

  1. 1. 配置虚拟环境
python3 -m venv .venvsource .venv/bin/activate
  1. 2. 安装 mlx-vlm
pip install mlx-vlm
  1. 3. 下载模型

这里使用 hf download 命令将 Hugging Face 在线模型下载到指定的本地目录。

hf download PaddlePaddle/PaddleOCR-VL-1.6 --local-dir model/PaddleOCR-VL-1.6
  1. 4. 运行 PaddleOCR-VL-1.6 模型
from mlx_vlm import load, generatefrom mlx_vlm.prompt_utils import apply_chat_templatemodel, processor = load("model/PaddleOCR-VL-1.6")image = ["ocr.png"]prompt = "OCR:"formatted_prompt = apply_chat_template(    processor,    model.config,    prompt,    num_images=len(image),)result = generate(    model=model,    processor=processor,    prompt=formatted_prompt,    image=image,    max_tokens=512,    temperature=0.0,)print(result.text)

PaddleOCR-VL-1.6 模型支持基础 OCR、表格识别、公式识别以及图表理解等任务。在上面的代码中,我们通过 prompt 参数将当前任务设置为 OCR。你可以将 prompt 参数的值改为 "Table Recognition:""Formula Recognition:" 或 "Chart Recognition:" 等。

4.0 OCR

prompt = "OCR:"

输入图片:

识别结果:

4.1 公式识别

prompt = "Formula Recognition:"

输入图片:

识别结果:

4.2 表格识别

from html import escapefrom pathlib import PathFCEL = "<fcel>"LCEL = "<lcel>"NL = "<nl>"UCEL = "<ucel>"ECEL = "<ecel>"DEFAULT_IMAGE_PATH = "complex-table.jpg"DEFAULT_MODEL_PATH = "model/PaddleOCR-VL-1.6"DEFAULT_OUTPUT_PATH = "table.html"DEFAULT_PROMPT = "Table Recognition:"def _new_cell(text: str = "", kind: str = "cell") -> dict[str, str | int]:    return {"kind": kind, "text": text.strip(), "colspan": 1, "rowspan": 1}def _parse_cells(row: str) -> list[dict[str, str | int]]:    """Parse PaddleOCR-VL table tokens into cells and merge markers."""    row = row.strip()    cells = []    index = 0    while index < len(row):        if row.startswith(FCEL, index):            index += len(FCEL)            next_index = len(row)            for token in (FCEL, LCEL, UCEL, ECEL):                token_index = row.find(token, index)                if token_index != -1:                    next_index = min(next_index, token_index)            cells.append(_new_cell(row[index:next_index]))            index = next_index        elif row.startswith(LCEL, index):            if cells:                cells[-1]["colspan"] += 1            index += len(LCEL)        elif row.startswith(UCEL, index):            cells.append(_new_cell(kind="up"))            index += len(UCEL)        elif row.startswith(ECEL, index):            cells.append(_new_cell())            index += len(ECEL)        else:            next_index = len(row)            for token in (FCEL, LCEL, UCEL, ECEL):                token_index = row.find(token, index)                if token_index != -1:                    next_index = min(next_index, token_index)            text = row[index:next_index].strip()            if text:                cells.append(_new_cell(text))            index = next_index    return cellsdef paddle_table_to_html(text: str) -> str:    rows = [row for row in text.strip().split(NL) if row.strip()]    if not rows:        return "<table></table>"    parsed_rows = [_parse_cells(row) for row in rows]    header = parsed_rows[0]    body_rows = parsed_rows[1:]    rowspan_by_col = {}    last_cell_by_col = {}    html_rows = []    for cells in body_rows:        row_index = len(html_rows)        html_rows.append([])        col_index = 0        for cell in cells:            colspan = int(cell["colspan"])            if cell["kind"] == "up":                for covered_col in range(col_index, col_index + colspan):                    if covered_col in rowspan_by_col:                        rowspan_by_col[covered_col] += 1                col_index += colspan                continue            for covered_col in range(col_index, col_index + colspan):                if covered_col in last_cell_by_col:                    last_row_index, last_cell_index = last_cell_by_col[covered_col]                    html_rows[last_row_index][last_cell_index]["rowspan"] = (                        rowspan_by_col.pop(covered_col, 1)                    )            last_cell_by_col[col_index] = (                row_index, len(html_rows[row_index]))            html_rows[row_index].append(                {"text": cell["text"], "colspan": colspan, "rowspan": 1}            )            for covered_col in range(col_index, col_index + colspan):                last_cell_by_col[covered_col] = (                    row_index, len(html_rows[row_index]) - 1                )                rowspan_by_col[covered_col] = 1            col_index += colspan    for col_index, rowspan in rowspan_by_col.items():        last_row_index, last_cell_index = last_cell_by_col[col_index]        html_rows[last_row_index][last_cell_index]["rowspan"] = rowspan    lines = ["<table>", "  <thead>", "    <tr>"]    for cell in header:        colspan_attr = (            f' colspan="{cell["colspan"]}"' if cell["colspan"] > 1 else ""        )        lines.append(f'      <th{colspan_attr}>{escape(cell["text"])}</th>')    lines.extend(["    </tr>", "  </thead>", "  <tbody>"])    for row in html_rows:        lines.append("    <tr>")        for cell in row:            colspan_attr = (                f' colspan="{cell["colspan"]}"' if cell["colspan"] > 1 else ""            )            rowspan_attr = (                f' rowspan="{cell["rowspan"]}"' if cell["rowspan"] > 1 else ""            )            lines.append(                f'      <td{colspan_attr}{rowspan_attr}>{escape(cell["text"])}</td>'            )        lines.append("    </tr>")    lines.extend(["  </tbody>", "</table>"])    return "\n".join(lines)def recognize_table(    image_path: str = DEFAULT_IMAGE_PATH,    model_path: str = DEFAULT_MODEL_PATH,    prompt: str = DEFAULT_PROMPT,    max_tokens: int = 1024,) -> str:    from mlx_vlm import generate, load    from mlx_vlm.prompt_utils import apply_chat_template    model, processor = load(model_path)    image = [image_path]    formatted_prompt = apply_chat_template(        processor,        model.config,        prompt,        num_images=len(image),    )    result = generate(        model=model,        processor=processor,        prompt=formatted_prompt,        image=image,        max_tokens=max_tokens,        temperature=0.0,    )    print(result.text)    return result.textdef build_html_page(table_html: str) -> str:    return f"""<!doctype html><html lang="en"><head>  <meta charset="utf-8">  <meta name="viewport" content="width=device-width, initial-scale=1">  <title>Table Recognition</title>  <style>body{{margin:24px;font-family:Arial,sans-serif;color:#222}}table{{border-collapse:collapse}}th,td{{border:1px solid #999;padding:4px 8px;text-align:left;vertical-align:middle}}th{{background:#f5f5f5;font-weight:600}}</style></head><body>{table_html}</body></html>"""def main() -> :    raw_table = recognize_table()    table_html = paddle_table_to_html(raw_table)    html_page = build_html_page(table_html)    Path(DEFAULT_OUTPUT_PATH).write_text(html_page, encoding="utf-8")if __name__ == "__main__":    main()

输入图片:

识别结果:

4.3 手写识别

prompt = "OCR:"

输入图片:

识别结果:

4.4 图表理解

prompt = "Chart Recognition:"

输入图片:

识别结果:

总结

PaddleOCR-VL-1.6 模型在处理含有倾斜、畸变等问题的图像方面表现出色。然而,测试中发现,mlx-vlm 提供的 PaddleOCR-VL 实现在识别复杂表格时存在文字合并错误的问题,而官方 PaddleOCR-VL-1.6 在线服务则能正确处理。此外,表格识别过程会输出 <fcel><lcel><ecel><nl><ucel> 和 <xcel> 等特殊 token,需要开发者自行实现对应的表格标签映射逻辑。

在图表理解测试中也发现了数据错误。你可以使用自己的图片测试集来评估该模型的能力;如果该模型无法满足你的需求,可以尝试测试得分为 95.69、参数量为 1.2B 的 MinerU2.5-Pro 模型。