一、 深入 CRAFT:不仅仅是画框
如果你去翻 EasyOCR 的源码(主要是 detection.py 和 craft_utils.py),你会发现检测阶段并不是像 YOLO 那样直接吐出 (x, y, w, h),而是生成了两张图。
CRAFT 的核心逻辑是基于 VGG16(或者轻量版的 ResNet)作为骨干网络,输出一个双通道的 Feature Map:
- Region Score(区域分数):这像素点是不是字符的一部分?
- Affinity Score(亲和力分数):这两个像素点之间是不是属于同一个文本行?
源码逻辑还原
在 craft_utils.py 里,你会看到大量的图像后处理逻辑。模型吐出来的热力图(Heatmap)是这样的:
- 高亮区域代表字符中心。
- 连接区域代表字符之间的空隙。
源码中最耗时的步骤其实不在卷积计算,而在 getDetBoxes 这个函数里。它需要做这几件事:
- 对 Region Map 和 Affinity Map 进行阈值过滤(Thresholding)。
- 将两个 Map 融合。
- 调用
cv2.connectedComponents找连通域。 - 计算最小外接矩形(MinAreaRect)。
这就是为什么 EasyOCR 对弯曲文本(Curved Text)支持极好的原因:因为它本质上是在做像素级的连接,字符弯了,连接线也就跟着弯,最后生成的多边形自然就是弯的。
二、 性能瓶颈分析
既然知道了原理,我们来看看为什么有时候它跑得慢。
EasyOCR 的标准流水线是串行的: Image -> Resize -> CRAFT检测 (GPU) -> 后处理成框 (CPU) -> Crop切图 -> CRNN识别 (GPU) -> 结果
这里有两个大坑:
- CRAFT 的后处理在 CPU:生成热力图很快,但从热力图算坐标(
cv2.findContours等)全是 CPU 操作。如果图片分辨率极大(默认canvas_size=2560),这一步非常慢。 - 显存利用率低:默认
readtext是一张张跑的,GPU 大部分时间在等 CPU 处理数据,CUDA Core 都在摸鱼。
三、 GPU 加速实战:榨干显卡性能
要在工程上解决速度问题,核心思路只有两个:降低输入分辨率 和 加大批处理(Batch Size)。
1. 针对检测阶段:调整 Canvas Size
如果你的场景是文档扫描件(文字很小),保持默认即可。但如果是自然场景(如路牌、封面),文字通常很大,没必要把图放大到 2560。
在源码中,图片会被强制 Resize 到 canvas_size 的倍数。改小这个值,检测速度呈平方级提升。
Python
# 速度提升 2-3 倍的改法,前提是文字足够大
reader.readtext(image, canvas_size=1280)
2. 针对识别阶段:Batch Processing 的正确姿势
这是很多人的误区。EasyOCR 的 readtext 其实很难做端到端的 Batch(因为每张图检测出来的框数量不一样,没法堆叠 Tensor)。
真正的加速点在于:把“识别”阶段独立出来做 Batch。
假设你有 1000 张由于业务裁剪好的小图(比如截好的车牌、验证码、商品标签),千万别用 readtext,要用底层的 recognize 方法。
下面这段代码展示了如何绕过检测阶段,直接利用 GPU 批量识别,速度能比循环调用快 10 倍以上。
Python
import easyocr
import torch
import cv2
import time
import os
# 1. 初始化,确保开启 GPU
# quantize=False: 现在的显卡 FP16 运算很快,不一定非要量化,有时候量化反而只有 CPU 收益
reader = easyocr.Reader(['ch_sim', 'en'], gpu=True, quantize=False)
def batch_recognize_only(img_paths, batch_size=128):
"""
针对已切分图片的极速识别方案
"""
results = []
# 将图片分批加载,避免一次性把内存撑爆
for i in range(0, len(img_paths), batch_size):
batch_files = img_paths[i : i + batch_size]
# 1. 并不是读路径,而是把 numpy array 喂进去
# 这一步是 IO 密集型,生产环境建议用多线程预取
image_list = []
for p in batch_files:
img = cv2.imread(p)
if img is not None:
# 可以在这里做个简单的灰度化,CRNN 对颜色不敏感
# img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
image_list.append(img)
if not image_list:
continue
t0 = time.time()
# 2. 核心调用:recognize
# 这个方法跳过了 CRAFT 检测,直接跑 CRNN
# batch_size 控制显存占用,T4/3060 上设 128 没问题
batch_preds = reader.recognize(image_list, batch_size=batch_size)
print(f"Batch {i//batch_size} processed in {time.time()-t0:.4f}s")
# batch_preds 返回的是 (text, confidence)
for src_file, pred in zip(batch_files, batch_preds):
results.append({
"file": src_file,
"text": pred[1],
"conf": pred[2]
})
return results
# --- 测试代码 ---
if __name__ == "__main__":
# 假设你有一个文件夹,里面全是切好的小图
test_dir = "./cropped_images"
if os.path.exists(test_dir):
all_imgs = [os.path.join(test_dir, f) for f in os.listdir(test_dir) if f.endswith('.jpg')]
# 预热一下 GPU
reader.recognize([cv2.imread(all_imgs[0])])
start = time.time()
res = batch_recognize_only(all_imgs, batch_size=128)
print(f"Total time: {time.time() - start:.2f}s for {len(all_imgs)} images")
四、 总结与建议
EasyOCR 不慢,慢的是默认参数和使用方式。
- CRAFT 的本质:它是基于分割的热力图。如果你的应用场景全是水平文字(比如身份证),CRAFT 反而是杀鸡用牛刀,且后处理太慢。这种情况下,考虑换用基于回归的检测算法(如 PaddleOCR 的 DBNet)会更快。
- 优化的优先级:
- Level 1: 开启
gpu=True。 - Level 2: 针对整图识别,调小
canvas_size。 - Level 3: 针对切片识别,直接调
reader.recognize并拉大batch_size。
- Level 1: 开启
技术人员用开源库,不要只当调包侠。点进 site-packages/easyocr 看看源码,你会发现很多官方文档没写的参数,那才是提升性能的金钥匙。