告别调包侠:从CRNN到DBNet,手把手教你用PyTorch复现经典OCR模型(附代码)
·
从零构建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由三部分组成:
- CNN特征提取器:通常采用修改版的VGG16
- BiLSTM序列建模:捕捉上下文依赖
- 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需要同时优化三个目标:
- 概率图损失:带权重的BCE损失
- 阈值图损失:L1距离
- 二值图损失:近似监督信号
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后处理优化
推理阶段需要将二值图转换为文本框:
- 使用OpenCV的findContours检测连通域
- 对每个轮廓应用多边形近似(approxPolyDP)
- 使用最小外接矩形或保持多边形形状
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%。
更多推荐



所有评论(0)