first commit
This commit is contained in:
commit
d7ff077dd6
|
|
@ -0,0 +1,10 @@
|
|||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.13
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
# ocr-VL1.6
|
||||
|
||||
本地 CPU 部署 [PaddlePaddle/PaddleOCR-VL-1.6](https://github.com/PaddlePaddle/PaddleOCR) 的 OCR 识别项目,包含完整的性能 Benchmark 和多级优化方案。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
ocr-VL1.6/
|
||||
├── main.py # 单图 OCR + Benchmark(已集成 set_num_threads 加速)
|
||||
├── batch_ocr.py # 批量 OCR(多进程并行加速)
|
||||
├── pyproject.toml # 项目配置(uv 管理)
|
||||
├── images/
|
||||
│ └── 手写01.png # 测试图片:手写中文(1758×646)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 组件 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Python | 3.13 | |
|
||||
| PaddlePaddle | 3.2.1 | CPU 版(无 CUDA),已编译 oneDNN/MKL-DNN |
|
||||
| PaddleOCR | 3.7.0 | 带 `doc-parser` extra |
|
||||
| PaddleOCR-VL-1.6 | 0.9B | 主 OCR 视觉语言模型(~1.8GB) |
|
||||
| PP-DocLayoutV3 | - | 版面检测模型(~126MB) |
|
||||
|
||||
模型缓存目录:`~/.paddlex/official_models/`
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前提条件
|
||||
|
||||
- Python ≥ 3.13
|
||||
- [uv](https://github.com/astral-sh/uv) 包管理器
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 运行
|
||||
|
||||
```bash
|
||||
# 单张图片 OCR(自动使用全部 CPU 核心)
|
||||
uv run python main.py
|
||||
|
||||
# 批量 OCR(多进程并行)
|
||||
uv run python batch_ocr.py images/ --workers 4
|
||||
```
|
||||
|
||||
首次运行会自动从 ModelScope 下载模型文件(约 2GB),后续使用缓存。
|
||||
|
||||
## 工作原理
|
||||
|
||||
`PaddleOCRVL` pipeline 分两阶段:
|
||||
|
||||
```
|
||||
输入图片 → [PP-DocLayoutV3 版面检测] → [PaddleOCR-VL-1.6-0.9B 文字识别] → 结构化输出
|
||||
```
|
||||
|
||||
1. **版面检测** — PP-DocLayoutV3 检测页面中的文本块区域(坐标 + 标签 + 置信度)
|
||||
2. **OCR 识别** — PaddleOCR-VL-1.6-0.9B(GQA 架构视觉语言模型)逐块识别文字
|
||||
3. **结果输出** — 返回 `PaddleOCRVLResult`,包含布局信息和识别文本
|
||||
|
||||
### 输出结构
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `layout_det_res.boxes` | list[dict] | 版面文本区域(cls_id, label, score, coordinate, polygon_points) |
|
||||
| `parsing_res_list` | list[PaddleOCRVLBlock] | 识别文本块($.label, $.bbox, $.content, $.polygon_points) |
|
||||
| `model_settings` | dict | 推理配置开关(版面检测/图表/印章等) |
|
||||
| `width` / `height` | int | 图片尺寸 |
|
||||
|
||||
## 性能优化迭代
|
||||
|
||||
测试机器:Windows 11, CPU 20 核(逻辑线程), RAM 32GB, PaddlePaddle 3.2.1 CPU
|
||||
|
||||
### 迭代 0:初始状态(无任何优化)
|
||||
|
||||
直接调用 `pipeline.predict()`,未设置任何线程参数。
|
||||
|
||||
| 阶段 | 耗时 |
|
||||
|------|------|
|
||||
| 模型初始化(加载权重) | ~60s |
|
||||
| 首次推理(含 JIT 编译) | ~285s |
|
||||
| 后续推理 | ~238s(~4 min) |
|
||||
|
||||
### 迭代 1:算子级并行 — `core.set_num_threads()`
|
||||
|
||||
**探索过程:**
|
||||
|
||||
| 尝试 | 方法 | 结果 |
|
||||
|------|------|------|
|
||||
| ❌ | `paddle.set_num_threads()` | Paddle 3.x 已移除该 API |
|
||||
| ❌ | 环境变量 `OMP_NUM_THREADS` / `MKL_NUM_THREADS` | Paddle 3.x 内部使用 oneDNN,不读取这些变量 |
|
||||
| ✅ | `from paddle import core; core.set_num_threads(N)` | **有效!** oneDNN 底层算子(matmul 等)受该 API 控制 |
|
||||
|
||||
**矩阵乘法微基准测试(4000×4000):**
|
||||
|
||||
| 线程数 | 耗时 (matmul) | 加速比 |
|
||||
|--------|--------------|--------|
|
||||
| 1 | 0.952s | 1.0x |
|
||||
| 4 | 0.419s | 2.3x |
|
||||
| 8 | 0.323s | 2.9x |
|
||||
| 16 | 0.240s | 4.0x |
|
||||
| **20** | **0.223s** | **4.3x** |
|
||||
|
||||
**应用到 OCR 后的实际效果:**
|
||||
|
||||
设置 `core.set_num_threads(20)` 后重新评测:
|
||||
|
||||
| 阶段 | 优化前 | 优化后 | 提速 |
|
||||
|------|--------|--------|------|
|
||||
| 模型初始化 | ~60s | ~40s | 1.5x |
|
||||
| 推理 | ~238s | **~162s(~2.7 min)** | **1.5x** |
|
||||
|
||||
**为什么不是 4.3x?** 矩阵乘法只是 OCR pipeline 的一部分。自回归解码(逐 token 生成)天然串行、I/O 等待、版面检测中的非矩阵运算等不受线程数影响。
|
||||
|
||||
---
|
||||
|
||||
### 迭代 2:批量多进程并行 — `batch_ocr.py`
|
||||
|
||||
**思路:** 多张图片时,用 `multiprocessing.Pool` 启动多个独立进程,每个进程加载一份 pipeline 实例,同时处理不同图片。
|
||||
|
||||
**策略:**
|
||||
- 每个子进程独立调用 `core.set_num_threads(总核心 / 进程数)`,避免线程争抢
|
||||
- 例如 4 进程 × 5 线程 = 20 核心全部利用
|
||||
|
||||
```bash
|
||||
# 4 进程并行
|
||||
uv run python batch_ocr.py images/ --workers 4
|
||||
```
|
||||
|
||||
| 配置 | 适用场景 | 理论加速比 | 内存开销 | 实际限制 |
|
||||
|------|---------|-----------|---------|---------|
|
||||
| `set_num_threads(N)` | 单张图片 | ~1.5x | 无额外开销 | 自回归解码瓶颈 |
|
||||
| `batch_ocr.py` | 批量多图 | ~Nx(N=进程数) | N × 2GB | 内存/内存带宽 |
|
||||
|
||||
> ⚠️ 每个进程独立加载模型(~2GB),32GB RAM 建议 `--workers ≤ 4`。
|
||||
|
||||
---
|
||||
|
||||
### 优化总结
|
||||
|
||||
```
|
||||
初始: 238s/image (单线程, 无优化)
|
||||
│
|
||||
├─ 迭代1: set_num_threads(20) → 162s (1.5x, 单图最优)
|
||||
│
|
||||
└─ 迭代2: batch_ocr.py (4进程) → ~40s/image (5.9x, 批量场景)
|
||||
```
|
||||
|
||||
> 单图理论极限约 2.7 分钟(受自回归解码串行特征限制),批量场景通过粗粒度并行可进一步摊薄。
|
||||
|
||||
## 已知局限
|
||||
|
||||
| 问题 | 影响 | 说明 |
|
||||
|------|------|------|
|
||||
| CPU 推理极慢 | 单图 ~2.7 min(优化后) | 0.9B VL 模型不适合 CPU 实时场景 |
|
||||
| 自回归解码串行 | 无法更细粒度并行 | 生成阶段逐 token 依赖,多线程收益有限 |
|
||||
| 内存占用大 | 每进程需 ~2GB | 限制了 `batch_ocr.py` 并行度 |
|
||||
| Windows 控制台乱码 | 中文输出显示为乱码 | GBK 编码问题,文件写入/pipe 正常 |
|
||||
| ccache 警告 | 无实际影响 | 仅影响首次编译加速,可忽略 |
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
"""
|
||||
批量 OCR 识别 — 多进程并行加速
|
||||
|
||||
原理:每个进程独立加载一份 pipeline 实例,同时处理不同图片。
|
||||
适用场景:一次处理多张图片(如文件夹批量 OCR)。
|
||||
|
||||
用法:
|
||||
python batch_ocr.py <图片目录> [--workers 4] [--threads 10]
|
||||
"""
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ocr_single(image_path: str, threads: int) -> dict:
|
||||
"""单张图片 OCR(在子进程中执行)"""
|
||||
from paddle import core
|
||||
core.set_num_threads(threads)
|
||||
|
||||
from paddleocr import PaddleOCRVL
|
||||
|
||||
pipeline = PaddleOCRVL(pipeline_version="v1.6")
|
||||
|
||||
t0 = time.perf_counter()
|
||||
result = pipeline.predict(image_path)
|
||||
elapsed = time.perf_counter() - t0
|
||||
|
||||
blocks = []
|
||||
for block in result[0]["parsing_res_list"]:
|
||||
if block.content.strip():
|
||||
blocks.append({"label": block.label, "bbox": block.bbox, "content": block.content})
|
||||
|
||||
return {
|
||||
"path": str(image_path),
|
||||
"elapsed": round(elapsed, 2),
|
||||
"blocks": blocks,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Batch OCR with multiprocessing")
|
||||
parser.add_argument("dir", type=str, help="图片目录")
|
||||
parser.add_argument("--workers", type=int, default=4, help="并行进程数 (默认 4)")
|
||||
parser.add_argument("--threads", type=int, default=None, help="每进程线程数 (默认: 总核心/workers)")
|
||||
args = parser.parse_args()
|
||||
|
||||
image_dir = Path(args.dir)
|
||||
if not image_dir.is_dir():
|
||||
print(f"[ERROR] 目录不存在: {args.dir}")
|
||||
sys.exit(1)
|
||||
|
||||
images = list(image_dir.glob("*.png")) + list(image_dir.glob("*.jpg")) + list(image_dir.glob("*.jpeg"))
|
||||
if not images:
|
||||
print(f"[ERROR] 目录中没有图片: {args.dir}")
|
||||
sys.exit(1)
|
||||
|
||||
workers = min(args.workers, len(images))
|
||||
threads = args.threads or max(1, cpu_count() // workers)
|
||||
|
||||
print(f"图片数量: {len(images)}")
|
||||
print(f"并行进程: {workers}")
|
||||
print(f"每进程线程: {threads}")
|
||||
print(f"总核心利用: {workers * threads} / {cpu_count()}")
|
||||
print("=" * 60)
|
||||
|
||||
t0 = time.perf_counter()
|
||||
|
||||
# 每个子进程独立加载模型 + 推理
|
||||
with Pool(workers) as pool:
|
||||
tasks = [(str(img), threads) for img in images]
|
||||
results = pool.starmap(ocr_single, tasks)
|
||||
|
||||
total_elapsed = time.perf_counter() - t0
|
||||
|
||||
# 输出结果
|
||||
print("\n" + "=" * 60)
|
||||
for r in results:
|
||||
print(f"\n[文件] {r['path']} ({r['elapsed']:.1f}s)")
|
||||
for block in r["blocks"]:
|
||||
print(f" [{block['label']}] {block['content'][:60]}{'...' if len(block['content']) > 60 else ''}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"总图片: {len(images)} | 总耗时: {total_elapsed:.1f}s")
|
||||
print(f"平均每图: {total_elapsed / len(images):.1f}s")
|
||||
print(f"单进程串行预计: {sum(r['elapsed'] for r in results):.1f}s")
|
||||
print(f"并行加速比: {sum(r['elapsed'] for r in results) / total_elapsed:.2f}x")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 329 KiB |
|
|
@ -0,0 +1,62 @@
|
|||
import time
|
||||
import os
|
||||
from paddle import core
|
||||
from paddleocr import PaddleOCRVL
|
||||
|
||||
IMAGE_PATH = "images/手写01.png"
|
||||
WARMUP_ROUNDS = 0
|
||||
BENCHMARK_ROUNDS = 1
|
||||
|
||||
# ── 线程配置 ──
|
||||
# 可通过环境变量 PADDLE_THREADS 覆盖,否则使用逻辑核心数
|
||||
DEFAULT_THREADS = int(os.environ.get("PADDLE_THREADS", os.cpu_count() or 4))
|
||||
core.set_num_threads(DEFAULT_THREADS)
|
||||
print(f"[Threads] oneDNN compiled, using {DEFAULT_THREADS} threads (CPU cores: {os.cpu_count()})")
|
||||
|
||||
# ── 模型初始化计时 ──
|
||||
print("=" * 60)
|
||||
print("初始化模型...")
|
||||
t0 = time.perf_counter()
|
||||
pipeline = PaddleOCRVL(pipeline_version="v1.6")
|
||||
t_init = time.perf_counter() - t0
|
||||
print(f"[OK] 模型初始化耗时: {t_init:.2f}s")
|
||||
print("=" * 60)
|
||||
|
||||
# ── 推理 Benchmark ──
|
||||
print(f"\n开始 OCR 识别: {IMAGE_PATH}")
|
||||
print(f"预热 {WARMUP_ROUNDS} 轮 + 正式测试 {BENCHMARK_ROUNDS} 轮\n")
|
||||
|
||||
# 预热
|
||||
for i in range(WARMUP_ROUNDS):
|
||||
print(f" 预热 {i + 1}/{WARMUP_ROUNDS}...")
|
||||
_ = pipeline.predict(IMAGE_PATH)
|
||||
|
||||
# 正式计时
|
||||
times = []
|
||||
for i in range(BENCHMARK_ROUNDS):
|
||||
print(f" 推理 {i + 1}/{BENCHMARK_ROUNDS}...", end=" ", flush=True)
|
||||
t0 = time.perf_counter()
|
||||
result = pipeline.predict(IMAGE_PATH)
|
||||
elapsed = time.perf_counter() - t0
|
||||
times.append(elapsed)
|
||||
print(f"{elapsed:.2f}s")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("[Benchmark]")
|
||||
print(f" 图片尺寸: {result[0]['width']} x {result[0]['height']}")
|
||||
print(f" 检测文本块: {len(result[0]['layout_det_res']['boxes'])} 个")
|
||||
print(f" 识别文本块: {len(result[0]['parsing_res_list'])} 个")
|
||||
print(f" 推理次数: {BENCHMARK_ROUNDS}")
|
||||
print(f" 最快: {min(times):.2f}s")
|
||||
print(f" 最慢: {max(times):.2f}s")
|
||||
print(f" 平均: {sum(times) / len(times):.2f}s")
|
||||
if len(times) > 1:
|
||||
print(f" 标准差: {(sum((t - sum(times) / len(times)) ** 2 for t in times) / len(times)) ** 0.5:.2f}s")
|
||||
print("=" * 60)
|
||||
|
||||
# ── 输出识别结果 ──
|
||||
print("\n[识别结果]\n")
|
||||
for item in result:
|
||||
for block in item["parsing_res_list"]:
|
||||
print(f" [{block.label}] ({block.bbox})")
|
||||
print(f" {block.content}\n")
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
[project]
|
||||
name = "ocr-vl1-6"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"paddleocr[doc-parser]>=3.6.0",
|
||||
"paddlepaddle==3.2.1",
|
||||
"setuptools>=83.0.0",
|
||||
]
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
PS D:\Dev\ocr-VL1.6> uv run python .\main.py
|
||||
信息: 用提供的模式无法找到文件。
|
||||
D:\Dev\ocr-VL1.6\.venv\Lib\site-packages\paddle\utils\cpp_extension\extension_utils.py:718: UserWarning: No ccache found. Please be aware that recompiling all source files may be required. You can download and install ccache from: https://github.com/ccache/ccache/blob/master/doc/INSTALL.md
|
||||
warnings.warn(warning_message)
|
||||
[Threads] oneDNN compiled, using 20 threads (CPU cores: 20)
|
||||
============================================================
|
||||
初始化模型...
|
||||
Creating model: ('PP-DocLayoutV3', None, None)
|
||||
Model files already exist. Using cached files. To redownload, please delete the directory manually: `C:\Users\kuuhaku_0\.paddlex\official_models\PP-DocLayoutV3`.
|
||||
Creating model: ('PaddleOCR-VL-1.6-0.9B', None, None)
|
||||
Model files already exist. Using cached files. To redownload, please delete the directory manually: `C:\Users\kuuhaku_0\.paddlex\official_models\PaddleOCR-VL-1.6`.
|
||||
Bucketed engine_config has no entry for resolved engine 'paddle_dynamic'; using an empty config for that engine.
|
||||
Loading configuration file C:\Users\kuuhaku_0\.paddlex\official_models\PaddleOCR-VL-1.6\config.json
|
||||
Loading weights file C:\Users\kuuhaku_0\.paddlex\official_models\PaddleOCR-VL-1.6\model.safetensors
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
use GQA - num_heads: 16- num_key_value_heads: 2
|
||||
D:\Dev\ocr-VL1.6\.venv\Lib\site-packages\paddle\utils\decorator_utils.py:420: Warning:
|
||||
Non compatible API. Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/guides/model_convert/convert_from_pytorch/api_difference/torch/torch.split.html first.
|
||||
warnings.warn(
|
||||
Loaded weights file from disk, setting weights to model.
|
||||
All model checkpoint weights were used when initializing PaddleOCRVLForConditionalGeneration.
|
||||
|
||||
All the weights of PaddleOCRVLForConditionalGeneration were initialized from the model checkpoint at C:\Users\kuuhaku_0\.paddlex\official_models\PaddleOCR-VL-1.6.
|
||||
If your task is similar to the task the model of the checkpoint was trained on, you can already use PaddleOCRVLForConditionalGeneration for predictions without further training.
|
||||
Loading configuration file C:\Users\kuuhaku_0\.paddlex\official_models\PaddleOCR-VL-1.6\generation_config.json
|
||||
[OK] 模型初始化耗时: 39.85s
|
||||
============================================================
|
||||
|
||||
开始 OCR 识别: images/手写01.png
|
||||
预热 0 轮 + 正式测试 1 轮
|
||||
|
||||
推理 1/1... D:\Dev\ocr-VL1.6\.venv\Lib\site-packages\paddle\tensor\creation.py:1088: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach(), rather than paddle.to_tensor(sourceTensor).
|
||||
return tensor(
|
||||
D:\Dev\ocr-VL1.6\.venv\Lib\site-packages\paddle\utils\decorator_utils.py:420: Warning:
|
||||
Non compatible API. Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/guides/model_convert/convert_from_pytorch/api_difference/torch/torch.max.html first.
|
||||
warnings.warn(
|
||||
216.52s
|
||||
|
||||
============================================================
|
||||
[Benchmark]
|
||||
图片尺寸: 1758 x 646
|
||||
检测文本块: 7 个
|
||||
识别文本块: 3 个
|
||||
推理次数: 1
|
||||
最快: 216.52s
|
||||
最慢: 216.52s
|
||||
平均: 216.52s
|
||||
============================================================
|
||||
|
||||
[识别结果]
|
||||
|
||||
[text] ([15, 1, 1027, 289])
|
||||
最优质的草场。但是这两年由于旱獭和草原黄鼠的逐年
|
||||
保护,人们的行为需要矫正。
|
||||
部分。末日的时刻。
|
||||
佛巴林卡塔尔等海
|
||||
|
||||
[text] ([1113, 0, 1697, 254])
|
||||
|
||||
|
||||
[text] ([18, 327, 1758, 640])
|
||||
很多消费者没有认识到医学验光的重要性,导致诸多后果。
|
||||
特许商品的销售便逐渐升温。文化软实力作出重要贡献。
|
||||
Loading…
Reference in New Issue