一、 深入 CRAFT:不仅仅是画框

如果你去翻 EasyOCR 的源码(主要是 detection.pycraft_utils.py),你会发现检测阶段并不是像 YOLO 那样直接吐出 (x, y, w, h),而是生成了两张图。

CRAFT 的核心逻辑是基于 VGG16(或者轻量版的 ResNet)作为骨干网络,输出一个双通道的 Feature Map:

  1. Region Score(区域分数):这像素点是不是字符的一部分?
  2. Affinity Score(亲和力分数):这两个像素点之间是不是属于同一个文本行?

源码逻辑还原

craft_utils.py 里,你会看到大量的图像后处理逻辑。模型吐出来的热力图(Heatmap)是这样的:

  • 高亮区域代表字符中心。
  • 连接区域代表字符之间的空隙。

源码中最耗时的步骤其实不在卷积计算,而在 getDetBoxes 这个函数里。它需要做这几件事:

  1. 对 Region Map 和 Affinity Map 进行阈值过滤(Thresholding)。
  2. 将两个 Map 融合。
  3. 调用 cv2.connectedComponents 找连通域。
  4. 计算最小外接矩形(MinAreaRect)。

这就是为什么 EasyOCR 对弯曲文本(Curved Text)支持极好的原因:因为它本质上是在做像素级的连接,字符弯了,连接线也就跟着弯,最后生成的多边形自然就是弯的。

二、 性能瓶颈分析

既然知道了原理,我们来看看为什么有时候它跑得慢。

EasyOCR 的标准流水线是串行的: Image -> Resize -> CRAFT检测 (GPU) -> 后处理成框 (CPU) -> Crop切图 -> CRNN识别 (GPU) -> 结果

这里有两个大坑:

  1. CRAFT 的后处理在 CPU:生成热力图很快,但从热力图算坐标(cv2.findContours 等)全是 CPU 操作。如果图片分辨率极大(默认 canvas_size=2560),这一步非常慢。
  2. 显存利用率低:默认 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 不慢,慢的是默认参数和使用方式。

  1. CRAFT 的本质:它是基于分割的热力图。如果你的应用场景全是水平文字(比如身份证),CRAFT 反而是杀鸡用牛刀,且后处理太慢。这种情况下,考虑换用基于回归的检测算法(如 PaddleOCR 的 DBNet)会更快。
  2. 优化的优先级
    • Level 1: 开启 gpu=True
    • Level 2: 针对整图识别,调小 canvas_size
    • Level 3: 针对切片识别,直接调 reader.recognize 并拉大 batch_size

技术人员用开源库,不要只当调包侠。点进 site-packages/easyocr 看看源码,你会发现很多官方文档没写的参数,那才是提升性能的金钥匙。