从零构建OCR模型:PyTorch实战CRNN与DBNet核心架构

在计算机视觉领域,光学字符识别(OCR)技术正经历着从传统算法到深度学习范式的革命性转变。当我们谈论OCR时,实际上涉及两个关键子任务:文本检测(定位图像中的文字区域)和文本识别(将检测到的文字转换为可编辑的字符序列)。本文将带您深入这两个任务的经典实现——使用PyTorch框架从零开始构建CRNN(卷积循环神经网络)识别模型和DBNet(可微分二值化网络)检测模型。

1. 开发环境配置与数据准备

1.1 PyTorch环境搭建

建议使用Python 3.8+和PyTorch 1.10+环境,以下为快速配置命令:

conda create -n ocr python=3.8
conda activate ocr
pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113
pip install opencv-python scikit-image pandas

对于GPU加速,确保安装对应版本的CUDA工具包。验证环境是否正常工作:

import torch
print(f"PyTorch版本: {torch.__version__}")
print(f"GPU可用: {torch.cuda.is_available()}")

1.2 数据集选择与处理

针对不同任务需要准备特定格式的数据:

任务类型推荐数据集标注格式特点
文本识别SynthText字符级坐标+文本合成数据,规模大
文本检测ICDAR2015四边形坐标自然场景,多方向文本
端到端Total-Text多边形坐标+文本包含弯曲文本

文本识别数据需要转换为如下结构的CSV文件:

filepath,text
images/001.jpg,hello
images/002.jpg,world

文本检测数据则需要处理为COCO格式或如下简化格式:

x1,y1,x2,y2,x3,y3,x4,y4,text  # 四边形坐标

2. CRNN文本识别模型实现

2.1 网络架构解析

CRNN由三部分组成:

  1. CNN特征提取器:通常采用修改版的VGG16
  2. BiLSTM序列建模:捕捉上下文依赖
  3. CTC解码层:解决字符对齐问题
class CRNN(nn.Module):
    def __init__(self, num_chars):
        super().__init__()
        # CNN部分
        self.cnn = nn.Sequential(
            nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2,2),
            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2,2),
            nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(),
            nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d((2,1),(2,1)),
            nn.Conv2d(256, 512, 3, padding=1), nn.BatchNorm2d(512), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d((2,1),(2,1)),
            nn.Conv2d(512, 512, 2), nn.ReLU()
        )
        # RNN部分
        self.rnn = nn.LSTM(512, 256, bidirectional=True, num_layers=2)
        self.fc = nn.Linear(512, num_chars+1)  # +1 for CTC blank

2.2 CTC损失函数实现要点

CTC(Connectionist Temporal Classification)的核心挑战在于处理输入输出长度不一致问题:

def compute_loss(preds, targets, preds_len, targets_len):
    # preds: (T, N, C)  T=时序长度, N=batch大小, C=字符类别数
    # targets: (N, S)   S=标签最大长度
    log_probs = F.log_softmax(preds, dim=2)
    loss = nn.CTCLoss(blank=num_chars)(log_probs, targets, preds_len, targets_len)
    return loss

实际训练中发现:当使用Adam优化器时,学习率设置为0.0005效果较好;标签建议先转换为字符索引,并统一转换为小写处理标点符号问题。

2.3 数据增强策略

有效的增强方法能显著提升模型鲁棒性:

  • 几何变换:随机透视(概率0.3)、弹性变形(概率0.1)
  • 颜色扰动:随机亮度(±30%)、对比度(±20%)、饱和度(±15%)
  • 噪声添加:高斯噪声(σ=0.01)、局部像素丢弃(概率0.05)
from albumentations import (
    Compose, Perspective, ElasticTransform,
    RandomBrightnessContrast, GaussNoise, CoarseDropout
)

aug = Compose([
    Perspective(scale=0.1, p=0.3),
    ElasticTransform(alpha=1, sigma=20, p=0.1),
    RandomBrightnessContrast(p=0.5),
    GaussNoise(var_limit=(0, 0.01), p=0.3),
    CoarseDropout(max_holes=8, p=0.2)
])

3. DBNet文本检测模型实战

3.1 可微分二值化原理

传统文本检测后处理(如固定阈值二值化)会导致梯度阻断,DBNet提出可学习的阈值图:

概率图P + 阈值图T → 二值图B = 1 / (1 + e^(-k(P-T)))

其中k为放大因子(通常取50),使输出接近0或1。

3.2 网络结构实现

DBNet采用FPN结构提取多尺度特征:

class DBHead(nn.Module):
    def __init__(self, in_channels, k=50):
        super().__init__()
        self.k = k
        self.binarize = nn.Sequential(
            nn.Conv2d(in_channels, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.ConvTranspose2d(64, 64, 2, 2),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.ConvTranspose2d(64, 1, 2, 2),
            nn.Sigmoid()
        )
        self.thresh = nn.Sequential(
            nn.Conv2d(in_channels, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.ConvTranspose2d(64, 64, 2, 2),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.ConvTranspose2d(64, 1, 2, 2),
            nn.Sigmoid()
        )

    def forward(self, x):
        prob = self.binarize(x)
        thresh = self.thresh(x)
        binary = torch.sigmoid(self.k * (prob - thresh))
        return prob, thresh, binary

3.3 损失函数设计

DBNet需要同时优化三个目标:

  1. 概率图损失:带权重的BCE损失
  2. 阈值图损失:L1距离
  3. 二值图损失:近似监督信号
class DBLoss(nn.Module):
    def __init__(self, alpha=1.0, beta=10):
        super().__init__()
        self.alpha = alpha
        self.beta = beta
        self.bce = nn.BCELoss(reduction='none')
        self.l1 = nn.L1Loss(reduction='none')

    def forward(self, preds, targets):
        prob_pred, thresh_pred, _ = preds
        prob_gt, thresh_gt = targets
        
        # 概率图损失
        pos_mask = (prob_gt == 1).float()
        neg_mask = (prob_gt < 1).float()
        prob_loss = self.bce(prob_pred, prob_gt)
        prob_loss = (pos_mask * prob_loss * 5.0 + 
                    neg_mask * prob_loss).mean()
        
        # 阈值图损失
        l1_loss = self.l1(thresh_pred, thresh_gt)
        thresh_loss = l1_loss[prob_gt > 0].mean()
        
        return prob_loss + self.alpha * thresh_loss

4. 训练技巧与实战调优

4.1 CRNN训练注意事项

  • 学习率调度:采用ReduceLROnPlateau策略,当验证集准确率不再提升时降低学习率
  • 批次生成:动态调整图像宽度保持比例,统一高度为32像素
  • 标签处理:构建字符到索引的映射表,建议保留至少5个空白字符作为padding
# 动态宽度批次生成示例
def collate_fn(batch):
    images, labels = zip(*batch)
    heights = [img.shape[0] for img in images]
    max_width = max(img.shape[1] for img in images)
    
    batch_images = torch.zeros(len(images), 3, max(heights), max_width)
    for i, img in enumerate(images):
        batch_images[i, :, :img.shape[0], :img.shape[1]] = img
    
    return batch_images, labels

4.2 DBNet后处理优化

推理阶段需要将二值图转换为文本框:

  1. 使用OpenCV的findContours检测连通域
  2. 对每个轮廓应用多边形近似(approxPolyDP)
  3. 使用最小外接矩形或保持多边形形状
def boxes_from_bitmap(pred, min_area=10):
    contours, _ = cv2.findContours(
        (pred*255).astype(np.uint8),
        cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    
    boxes = []
    for contour in contours:
        if cv2.contourArea(contour) < min_area:
            continue
        epsilon = 0.002 * cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, epsilon, True)
        boxes.append(approx.reshape(-1, 2))
    
    return boxes

4.3 混合精度训练

使用AMP(自动混合精度)加速训练并减少显存占用:

scaler = torch.cuda.amp.GradScaler()

for images, targets in dataloader:
    optimizer.zero_grad()
    
    with torch.cuda.amp.autocast():
        outputs = model(images)
        loss = criterion(outputs, targets)
    
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

在NVIDIA V100显卡上的测试结果显示,混合精度训练可使CRNN的训练速度提升约35%,同时显存占用减少40%。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐