本文针对阿里天池《零基础入门CV赛事-街景字符编码识别》,给出了百行代码Baseline,帮助cv学习者更好地结合赛事实践。同时,从赛题数据分析和解题思路分析两方面进行了详细的解读,以便于你们进阶学习。算法
https://tianchi.aliyun.com/competition/entrance/531795/information(阿里天池-零基础入门CV赛事)json
Baseline以定长字符识别为解题思路,进行了必要的注释和代码实现,分数在0.6左右,运用时长:CPU大约须要2小时,GPU大约10分钟。网络
import glob, json from PIL import Image from tqdm import tqdm import numpy as np import pandas as pd import torch import torch.nn as nn import torch.optim as optim import torchvision.models as models import torchvision.transforms as transforms from torch.utils.data.dataset import Dataset class SVHNDataset(Dataset): def __init__(self, img_path, img_label, transform=None): self.img_path, self.img_label, self.transform = img_path, img_label, transform def __getitem__(self, index): img = Image.open(self.img_path[index]).convert('RGB') # 读取数据 img = self.transform(img) # 作相应变换 if self.img_label: lbl = np.array(self.img_label[index], dtype=np.int) # 制做标签 lbl = list(lbl) + (5 - len(lbl)) * [10] # 标签长度少于五的用10来填充 return img, torch.from_numpy(np.array(lbl[:5])) else: return img def __len__(self): return len(self.img_path) # 定义模型 class SVHN_Model1(nn.Module): def __init__(self): super(SVHN_Model1, self).__init__() self.cnn = models.resnet50(pretrained=True) # 加载resnet50 self.cnn.avgpool = nn.AdaptiveAvgPool2d(1) # 将平均池化改成自适应平均池化 self.cnn = nn.Sequential(*list(self.cnn.children())[:-1]) # 去除最后的线性层 self.fc1,self.fc2,self.fc3 = nn.Linear(2048, 11), nn.Linear(2048, 11), nn.Linear(2048, 11) self.fc4,self.fc5 = nn.Linear(2048, 11), nn.Linear(2048, 11) def forward(self, img): feat = self.cnn(img) feat = feat.view(feat.shape[0], -1) c1,c2,c3 = self.fc1(feat), self.fc2(feat), self.fc3(feat) c4,c5 = self.fc4(feat), self.fc5(feat) return c1, c2, c3, c4, c5 def train(train_loader, model, criterion, optimizer): model.train() # 切换模型为训练模式 train_loss = [] for input, target in tqdm(train_loader): # 取出数据与对应标签 if use_cuda: # 若是是gpu版本 input, target = input.cuda(), target.cuda() target = target.long() c0, c1, c2, c3, c4 = model(input) # 获得预测值 loss = criterion(c0, target[:, 0]) + criterion(c1, target[:, 1]) + \ criterion(c2, target[:, 2]) + criterion(c3, target[:, 3]) + \ criterion(c4, target[:, 4]) # 计算loss optimizer.zero_grad() # 梯度清零 loss.backward() # 反向传播 optimizer.step() # 参数更新 train_loss.append(loss.item()) return np.mean(train_loss) def predict(test_loader, model): model.eval() # 切换模型为预测模型 test_pred = [] with torch.no_grad(): # 不记录模型梯度信息 for input in tqdm(test_loader): if use_cuda: input = input.cuda() c0, c1, c2, c3, c4 = model(input) if use_cuda: output = np.concatenate([ c0.data.cpu().numpy(), c1.data.cpu().numpy(), c2.data.cpu().numpy(), # 将结果水平合并,即第一个字符索引为第一列到第十一列, c3.data.cpu().numpy(), c4.data.cpu().numpy()], axis=1) # 第二个字符为第十二列到第二十二列,依次往下 else: output = np.concatenate([ c0.data.numpy(), c1.data.numpy(), c2.data.numpy(), c3.data.numpy(), c4.data.numpy()], axis=1) test_pred.append(output) test_pred = np.vstack(test_pred) # 将每一个batch的结果垂直堆起来 return test_pred train_path, test_path = glob.glob('../input/train/*.png'), glob.glob('../input/test_a/*.png') # 读取训练数据和测试数据 train_path.sort(); test_path.sort() train_json = json.load(open('../input/train.json')) #读取训练集标注文件 train_label = [train_json[x]['label'] for x in train_json] # 拿出训练集的标签 trans_fun = transforms.Compose([ transforms.Resize((64, 128)), # 将图片裁剪为64*128 transforms.ToTensor(), #转为Tensor transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 标准化 ]) train_loader = torch.utils.data.DataLoader( SVHNDataset(train_path, train_label, trans_fun), batch_size=40, shuffle=True) # 批量大小40,打乱顺序 test_loader = torch.utils.data.DataLoader( SVHNDataset(test_path, [], trans_fun), batch_size=40, shuffle=False) model = SVHN_Model1() criterion = nn.CrossEntropyLoss() # 交叉熵损失函数 optimizer = torch.optim.Adam(model.parameters(), 0.001) # Adam优化器 use_cuda = torch.cuda.is_available() if use_cuda: model = model.cuda() for epoch in range(10): train_loss = train(train_loader, model, criterion, optimizer) # 训练 print(epoch, train_loss) test_predict_label = predict(test_loader, model) test_predict_label = np.vstack([ test_predict_label[:, :11].argmax(1), test_predict_label[:, 11:22].argmax(1), test_predict_label[:, 22:33].argmax(1), test_predict_label[:, 33:44].argmax(1), test_predict_label[:, 44:55].argmax(1), ]).T test_label_pred = [] for x in test_predict_label: test_label_pred.append(''.join(map(str, x[x!=10]))) # 取出预字符不为10的字符且顺序排列 df_submit = pd.read_csv('../input/sample_submit_A.csv') df_submit['file_code'] = test_label_pred df_submit.to_csv('submit.csv', index=None) # 保存结果文件
以街道字符为赛题数据,该数据来自收集的SVHN街道字符,并进行了匿名采样处理。 app
训练集数据包括3W张照片,验证集数据包括1W张照片,每张照片包括颜色图像和对应的编码类别和具体位置。
须要注意的是本赛题须要选手识别图片中全部的字符,为了下降比赛难度,比赛提供了训练集、验证集和测试集中全部字符的位置框。框架
对于训练数据每张图片将给出对于的编码标签,和具体的字符框的位置(训练集、测试集和验证集都给出字符位置),可用于模型训练:
字符的坐标具体以下所示:
在比赛数据(训练集、测试集和验证集)中,同一张图片中可能包括一个或者多个字符,所以在比赛数据的JSON标注中,会有两个字符的边框信息:ide
为了方便你们进行数据读取,在此给出JSON中标签的读取方式:函数
import json train_json = json.load(open('../input/train.json')) # 数据标注处理 def parse_json(d): arr = np.array([ d['top'], d['height'], d['left'], d['width'], d['label'] ]) arr = arr.astype(int) return arr img = cv2.imread('../input/train/000000.png') arr = parse_json(train_json['000000.png']) plt.figure(figsize=(10, 10)) plt.subplot(1, arr.shape[1]+1, 1) plt.imshow(img) plt.xticks([]); plt.yticks([]) for idx in range(arr.shape[1]): plt.subplot(1, arr.shape[1]+1, idx+2) plt.imshow(img[arr[0, idx]:arr[0, idx]+arr[1, idx],arr[2, idx]:arr[2, idx]+arr[3, idx]]) plt.title(arr[4, idx]) plt.xticks([]); plt.yticks([])
结果示例:学习
赛题本质是分类问题,须要对图片的字符进行识别。但赛题给定的数据,不一样图片中包含的字符数量不相同。以下图所示,有的图片的字符个数为2,有的图片字符个数为3,有的图片字符个数为4。
所以本次赛题的难点是须要对不定长的字符进行识别,与传统的图像分类任务有所不一样。下文将给出三种难度从低到高的解决思路,具体以下:测试
能够将赛题抽象为一个定长字符识别问题,在赛题数据集中大部分图像中字符个数为2-4个,最多的字符个数为6个。所以能够对于全部的图像都抽象为6个字符的识别问题,字符23填充为23XXXX,字符241填充为241XXX。优化
通过填充以后,原始的赛题能够简化了6个字符的分类问题。在每一个字符的分类中会进行11个类别的分类:0-9及空字符,假如分类为填充字符,则代表该字符为空。
下文将给出本思路的具体Baseline实现,先来看一下Baseline中的检测框架ResNet18。
ResNet系列的每一个网络都包括三个主要部分:输入部分、输出部分和中间卷积部分(中间卷积部分包括如图所示的Stage1到Stage4共计四个stage)。下图表示的是ResNet系列的结构:
在字符识别研究中,有特定的方法来解决此种不定长的字符识别问题,比较典型的有CRNN字符识别模型。在本次赛题中给定的图像数据都比较规整,能够视为一个单词或者一个句子。
CRNN网络结构能够分为三个部分:特征提取、序列建模、转录
在赛题数据中已经给出了训练集、验证集中全部图片中字符的位置,所以能够首先将字符的位置进行识别,利用物体检测的思路完成。
此种思路须要参赛选手构建字符检测模型,对测试集中的字符进行识别。选手能够参考物体检测模型SSD或者YOLO来完成。为方便你们学习,对两种算法进行简单的介绍。
是一种直接预测目标类别和bounding box的多目标检测算法。与faster rcnn相比,该算法没有生成 proposal 的过程,这就极大提升了检测速度。
针对不一样大小的目标检测,传统的作法是先将图像转换成不一样大小(图像金字塔),而后分别检测,最后将结果综合起来(NMS)。而SSD算法则利用不一样卷积层的 feature map 进行综合也能达到一样的效果。
算法的主网络结构是VGG16,将最后两个全链接层改为卷积层,并随后增长了4个卷积层来构造网络结构。
You Only Look Once: Unified, Real-Time Object Detection,是Joseph Redmon和Ali Farhadi等人于2015年提出的基于单个神经网络的目标检测系统。
YOLO是一个能够一次性预测多个Box位置和类别的卷积神经网络,可以实现端到端的目标检测和识别,其最大的优点就是速度快。事实上,目标检测的本质就是回归,所以一个实现回归功能的CNN并不须要复杂的设计过程。
YOLO没有选择滑动窗口(silding window)或提取proposal的方式训练网络,而是直接选用整图训练模型。这样作的好处在于能够更好的区分目标和背景区域,相比之下,采用proposal训练方式的Fast-R-CNN经常把背景区域误检为特定目标。
延伸阅读:
书籍:《深度实践OCR:基于深度学习的文字识别》
做者:刘树春 阿里巴巴本地生活研究院算法专家,前复旦七牛云联合实验室OCR算法负责人