本文主要对常用的文本检测模型算法进行总结及分析,有的模型笔者切实run过,有的是通过论文及相关代码的分析,如有错误,请不吝指正。

一下进行各个模型的详细解析

CTPN 详解

代码链接:https://github.com/xiaofengShi/CHINESE-OCR

CTPN是目前应用非常广泛的印刷体文本检测模型算法。

CTPN由fasterrcnn改进而来,可以看下二者的异同

网络结构 FasterRcnn CTPN
basenet Vgg16 ,Vgg19,resnet Vgg16,也可以使用其他CNN结构
RPN预测 basenet的predict layer使用CNN生成 basenet之后使用双向RNN使用FC生成
ROI 模型适用于目标检测,为多分类任务,包含ROI及类别损失和BOX回归 文本提取为二分类任务,不包含ROI及类别损失,只在RPN层计算目标损失及BOX回归
Anchor 一共9种anchor尺寸,3比例,3尺寸 固定anchor宽度,高度为10种
batch 每次只能训练一个样本 每次只能训练一个样本

根据ctpn的网络设计,可以看到看到ctpn一般使用预训练的vggnet,并且只用来检测水平文本,一般可以用来进行标准格式印刷体的检测,在目标框回归预测时,加上回归框的角度信息,就可以用来检测旋转文本,比如EAST模型。

代码分析

网络模型

直接看CTPN的网络代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
copy

class VGGnet_train(Network):# 继承自NetWork,关与NetWork可以看这里:https://github.com/xiaofengShi/CHINESE-OCR/blob/master/ctpn/lib/networks/network.pydef __init__(self, trainable=True):self.inputs = []self.data = tf.placeholder(tf.float32, shape=[None, None, None, 3], name='data')self.im_info = tf.placeholder(tf.float32, shape=[None, 3], name='im_info')self.gt_boxes = tf.placeholder(tf.float32, shape=[None, 5], name='gt_boxes')self.gt_ishard = tf.placeholder(tf.int32, shape=[None], name='gt_ishard')self.dontcare_areas = tf.placeholder(tf.float32, shape=[None, 4], name='dontcare_areas')self.keep_prob = tf.placeholder(tf.float32)self.layers = dict({'data': self.data, 'im_info': self.im_info, 'gt_boxes': self.gt_boxes,'gt_ishard': self.gt_ishard, 'dontcare_areas': self.dontcare_areas})self.trainable = trainableself.setup()def setup(self):# 对于文本提议来说,类别为2,一类为为文字部分,另一类为背景n_classes = cfg.NCLASSES# anchor的初始尺寸,论文中使用的是16anchor_scales = cfg.ANCHOR_SCALES_feat_stride = [16, ]# base net is vgg16# 内部使用的函数(self.feed('data').conv(3, 3, 64, 1, 1, name='conv1_1').conv(3, 3, 64, 1, 1, name='conv1_2').max_pool(2, 2, 2, 2, padding='VALID', name='pool1').conv(3, 3, 128, 1, 1, name='conv2_1').conv(3, 3, 128, 1, 1, name='conv2_2').max_pool(2, 2, 2, 2, padding='VALID', name='pool2').conv(3, 3, 256, 1, 1, name='conv3_1').conv(3, 3, 256, 1, 1, name='conv3_2').conv(3, 3, 256, 1, 1, name='conv3_3').max_pool(2, 2, 2, 2, padding='VALID', name='pool3').conv(3, 3, 512, 1, 1, name='conv4_1').conv(3, 3, 512, 1, 1, name='conv4_2').conv(3, 3, 512, 1, 1, name='conv4_3').max_pool(2, 2, 2, 2, padding='VALID', name='pool4').conv(3, 3, 512, 1, 1, name='conv5_1').conv(3, 3, 512, 1, 1, name='conv5_2').conv(3, 3, 512, 1, 1, name='conv5_3'))# RPN # 该层对上层的feature map进行卷积,生成512通道的的feature map(self.feed('conv5_3').conv(3, 3, 512, 1, 1, name='rpn_conv/3x3'))# 卷积最后一层的的feature_map尺寸为batch*h*w*512# 原来的单层双向LSTM(self.feed('rpn_conv/3x3').Bilstm(512, 128, 512, name='lstm_o'))# bilstm之后输出的尺寸为(N, H, W, 512)""" 和faster—rcnn相似,在ctpn的rpn网络中,使用双向lstm和全连接得到预测的目标概率和回归框,在faster-rcnn中使用的是卷积的方式从basenet的最后一层生成使用LSTM的输出来计算位置偏移和类别概率(判断是否是物体,不判断类别的种类)输入尺寸为(N, H, W, 512)  输出尺寸(N, H, W, int(d_o))可以将这一层当做目标检测中的最后一层feature_maprpn_bbox_pred--对于h*w的尺寸上,每一anchor上生成4个位置偏移量rpn_cls_score--对于h*w的尺寸上,每一anchor上生成2个置信度得分,判断是否为物体"""(self.feed('lstm_o').lstm_fc(512, len(anchor_scales) * 10 * 4, name='rpn_bbox_pred'))(self.feed('lstm_o').lstm_fc(512, len(anchor_scales) * 10 * 2, name='rpn_cls_score'))# generating training labels on the fly# output: rpn_labels(HxWxA, 2) rpn_bbox_targets(HxWxA, 4) rpn_bbox_inside_weights rpn_bbox_outside_weights# 给每个anchor上标签,并计算真值(也是delta的形式),以及内部权重和外部权重(self.feed('rpn_cls_score', 'gt_boxes', 'gt_ishard', 'dontcare_areas', 'im_info').anchor_target_layer(_feat_stride, anchor_scales, name='rpn-data'))# shape is (1, H, W, Ax2) -> (1, H, WxA, 2)# 给之前得到的score进行softmax,得到0-1之间的得分(self.feed('rpn_cls_score').spatial_reshape_layer(2, name='rpn_cls_score_reshape').spatial_softmax(name='rpn_cls_prob'))'''# the below is the rcnn net model from faster_rcnn# 后面的部分是fasterrcnn之后的ROIPooling部分(self.feed('rpn_cls_prob').spatial_reshape_layer(len(anchor_scales) * 10 * 2, name='rpn_cls_prob_reshape'))self.feed('rpn_cls_prob_reshape', 'rpn_bbox_pred', 'im_info').proposal_layer(_feat_stride, anchor_scales, 'TRAIN', name='rpn_rois')(self.feed('rpn_rois', 'gt_boxes').proposal_target_layer(n_classes, name='roi-data'))# ========= RCNN ============(self.feed('conv5_3', 'roi-data').roi_pool(7, 7, 1.0/16, name='pool_5').fc(4096, name='fc6').dropout(0.5, name='drop6').fc(4096, name='fc7').dropout(0.5, name='drop7').fc(n_classes, relu=False, name='cls_score').softmax(name='cls_prob'))(self.feed('drop7').fc(n_classes*4, relu=False, name='bbox_pred'))'''

可以看到CTPN的网络结构有FasterRcnn改变而来,使用vggnet进行图像的特征提取,对得到的最后一层featuremap的尺寸为[N,H,W,C][N,H,W,C],进行维度变换为[NH,W,C][NH,W,C]成为序列,使用BLSTM得到的维度为[NH,W,2D][NH,W,2D]其中DD为单向RNN的隐藏层节点数,转换维度为[NHW,2D][NHW,2D],使用全连接进行维度转换为[NHW,C][NHW,C],最后再reshape成[N,H,W,C][N,H,W,C],在这一步中,使用RNNCNN之后的特征图进行特征图长度方向上的连接;接下来使用lstm_fc函数对anchor进行目标类别预测和边界回归框预测,在这一层的特征图上,每个点生成A个anchor,每个anchor存在目标类别预测和边界回归预测:对于回归预测,每个格点生成2A个目标预测;对于边界回归预测,每个格点生成4A个边界预测。

网络模型结构如下所示

CTPN MODEL STRUCTURE

anchor生成及筛选

在整个模型中,AnchorGen处需要详细说明,这就是大名鼎鼎的RPN,下面结合代码说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
copy

# -*- coding:utf-8 -*-
import numpy as np
import numpy.random as nprfrom ..fast_rcnn.config import cfg
from bbox import bbox_overlaps, bbox_intersectionsDEBUG = False# 生成基础anchor box
def generate_basic_anchors(sizes, base_size=16):base_anchor = np.array([0, 0, base_size - 1, base_size - 1], np.int32)anchors = np.zeros((len(sizes), 4), np.int32)index = 0for h, w in sizes:anchors[index] = scale_anchor(base_anchor, h, w)index += 1return anchors# 根据baseanchor和设定的anchor的高度和宽度进行设定的anchor生成
def scale_anchor(anchor, h, w):x_ctr = (anchor[0] + anchor[2]) * 0.5y_ctr = (anchor[1] + anchor[3]) * 0.5scaled_anchor = anchor.copy()scaled_anchor[0] = x_ctr - w / 2  # xminscaled_anchor[2] = x_ctr + w / 2  # xmaxscaled_anchor[1] = y_ctr - h / 2  # yminscaled_anchor[3] = y_ctr + h / 2  # ymaxreturn scaled_anchor# 生成anchor box
# 此处使用的是宽度固定,高度不同的anchor设置
def generate_anchors(base_size=16, ratios=[0.5, 1, 2],scales=2 ** np.arange(3, 6)):heights = [11, 16, 23, 33, 48, 68, 97, 139, 198, 283]widths = [16]sizes = []for h in heights:for w in widths:sizes.append((h, w))return generate_basic_anchors(sizes)# 生成的anchor和groundtruth之间进行转换,转换方式和论文一致
def bbox_transform(ex_rois, gt_rois):"""computes the distance from ground-truth boxes to the given boxes, normed by their size:param ex_rois: n * 4 numpy array, anchor boxes:param gt_rois: n * 4 numpy array, ground-truth boxes:return: deltas: n * 4 numpy array, ground-truth boxes"""ex_widths = ex_rois[:, 2] - ex_rois[:, 0] + 1.0 # anchor width ex_heights = ex_rois[:, 3] - ex_rois[:, 1] + 1.0 # anchor heightex_ctr_x = ex_rois[:, 0] + 0.5 * ex_widths # anchor center xex_ctr_y = ex_rois[:, 1] + 0.5 * ex_heights # anchor center yassert np.min(ex_widths) > 0.1 and np.min(ex_heights) > 0.1, \'Invalid boxes found: {} {}'. \format(ex_rois[np.argmin(ex_widths), :], ex_rois[np.argmin(ex_heights), :])gt_widths = gt_rois[:, 2] - gt_rois[:, 0] + 1.0 # gt_box widthgt_heights = gt_rois[:, 3] - gt_rois[:, 1] + 1.0 # gt_box heightgt_ctr_x = gt_rois[:, 0] + 0.5 * gt_widths # gt_box center xgt_ctr_y = gt_rois[:, 1] + 0.5 * gt_heights # gt_box center y# warnings.catch_warnings()# warnings.filterwarnings('error')targets_dx = (gt_ctr_x - ex_ctr_x) / ex_widths  # (gt_c_x-a_c_x)targets_dy = (gt_ctr_y - ex_ctr_y) / ex_heightstargets_dw = np.log(gt_widths / ex_widths)targets_dh = np.log(gt_heights / ex_heights)targets = np.vstack((targets_dx, targets_dy, targets_dw, targets_dh)).transpose()return targets# 生成anchors
def anchor_target_layer(rpn_cls_score, gt_boxes, gt_ishard, dontcare_areas, im_info, _feat_stride=[16, ],anchor_scales=[16, ]):"""Assign anchors to ground-truth targets. Produces anchor classificationlabels and bounding-box regression targets.Parameters----------rpn_cls_score: (1, H, W, Ax2) bg/fg scores of previous conv layergt_boxes: (G, 5) vstack of [x1, y1, x2, y2, class]gt_ishard: (G, 1), 1 or 0 indicates difficult or notdontcare_areas: (D, 4), some areas may contains small objs but no labelling. D may be 0im_info: a list of [image_height, image_width, scale_ratios]_feat_stride: the downsampling ratio of feature map to the original input imageanchor_scales: the scales to the basic_anchor (basic anchor is [16, 16])----------Returns----------rpn_labels : (HxWxA, 1), for each anchor, 0 denotes bg, 1 fg, -1 dontcarerpn_bbox_targets: (HxWxA, 4), distances of the anchors to the gt_boxes(may contains some transform)that are the regression objectivesrpn_bbox_inside_weights: (HxWxA, 4) weights of each boxes, mainly accepts hyper param in cfgrpn_bbox_outside_weights: (HxWxA, 4) used to balance the fg/bg,beacuse the numbers of bgs and fgs mays significiantly different"""# anchors is the [x_min,y_min,x_max,y_max]# 生成基本的anchor,一共10个_anchors = generate_anchors(scales=np.array(anchor_scales))  _num_anchors = _anchors.shape[0]  # 10个anchor# allow boxes to sit over the edge by a small amount_allowed_border = 0# 原始图像的信息,图像的高宽及通道数im_info = im_info[0]  # 在feature-map上定位anchor,并加上delta,得到在实际图像中anchor的真实坐标""" Algorithm:for each (H, W) location igenerate 9 anchor boxes centered on cell iapply predicted bbox deltas at cell i to each of the 9 anchorsfilter out-of-image anchorsmeasure GT overlap """assert rpn_cls_score.shape[0] == 1, \'Only single item batches are supported'# map of shape (..., H, W)height, width = rpn_cls_score.shape[1:3]  # feature-map的高宽# 1. Generate proposals from bbox deltas and shifted anchorsshift_x = np.arange(0, width) * _feat_strideshift_y = np.arange(0, height) * _feat_strideshift_x, shift_y = np.meshgrid(shift_x, shift_y)  # in W H order# 生成feature-map和真实图像上anchor之间的偏移量# shifts构建网格结构,shape [height*width,4]shifts = np.vstack((shift_x.ravel(), shift_y.ravel(),shift_x.ravel(), shift_y.ravel())).transpose()  A = _num_anchors  # 10个anchorK = shifts.shape[0]  # feature-map的宽乘高的大小# 为当前的featuremap每个点生成A个anchor,shape is [K,A,4]all_anchors = (_anchors.reshape((1, A, 4)) +shifts.reshape((1, K, 4)).transpose((1, 0, 2)))  all_anchors = all_anchors.reshape((K * A, 4))  # shape is (K*A,4)# 在featuremap上每个点生成A个anchortotal_anchors = int(K * A)# only keep anchors inside the image# 因为生成的anchor尺寸有大有小,因此在边缘处生成的anchor有可能会超过原始图像的边界,# 将这些超过边界的anchor去掉,得到的是这些anchor的在all_anchors中的索引# 仅保留那些还在图像内部的anchor,超出图像的都删掉# anchors[:]=[x_min,y_min,x_max,y_max]inds_inside = np.where((all_anchors[:, 0] >= -_allowed_border) &(all_anchors[:, 1] >= -_allowed_border) &(all_anchors[:, 2] < im_info[1] + _allowed_border) &  # width(all_anchors[:, 3] < im_info[0] + _allowed_border)  # height)[0]# keep only inside anchorsanchors = all_anchors[inds_inside, :]  # 保留那些在图像内的anchor# 至此,anchor准备好了# --------------------------------------------------------------# label: 1 is positive, 0 is negative, -1 is dont care# (A)labels = np.empty((len(inds_inside),), dtype=np.float32)labels.fill(-1)  # 初始化label,均为-1# overlaps between the anchors and the gt boxes# overlaps (ex, gt), shape is A x G# 计算anchor和gt-box的overlap,用来给anchor上标签# anchor box and groundtruth box 交集面积/并集面积# 通过IOU的得分来确定anchor为正样本与否# overlaps shape is [anchor.shape[0],gt_box.shape[0]]overlaps = bbox_overlaps(np.ascontiguousarray(anchors, dtype=np.float),np.ascontiguousarray(gt_boxes, dtype=np.float))  # 存放每一个anchor和每一个gtbox之间的overlap# 找到和每一个gtbox,overlap最大的那个anchorargmax_overlaps = overlaps.argmax(axis=1) max_overlaps = overlaps[np.arange(len(inds_inside)), argmax_overlaps]# 找到每个位置上10个anchor中与gtbox,overlap最大的那个gt_argmax_overlaps = overlaps.argmax(axis=0)  gt_max_overlaps = overlaps[gt_argmax_overlaps,np.arange(overlaps.shape[1])]gt_argmax_overlaps = np.where(overlaps == gt_max_overlaps)[0]if not cfg.TRAIN.RPN_CLOBBER_POSITIVES:# assign bg labels first so that positive labels can clobber them# 先给背景上标签,小于0.3overlap的为负样本label为0labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0  # -----------------------------------## 正样本的确定,iou得分大于0.7和每个位置上具有最大IOU得分的anchor# fg label: for each gt, anchor with highest overlap# 每个位置上的10个个anchor中overlap最大的认为是前景labels[gt_argmax_overlaps] = 1  # fg label: above threshold IOU# overlap大于0.7的认为是前景labels[max_overlaps >= cfg.TRAIN.RPN_POSITIVE_OVERLAP] = 1  if cfg.TRAIN.RPN_CLOBBER_POSITIVES:# assign bg labels last so that negative labels can clobber positiveslabels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0# preclude dontcare areas# 这里我们暂时不考虑有doncare_area的存在if dontcare_areas is not None and dontcare_areas.shape[0] > 0:  # intersec shape is D x Aintersecs = bbox_intersections(np.ascontiguousarray(dontcare_areas, dtype=np.float),  # D x 4np.ascontiguousarray(anchors, dtype=np.float)  # A x 4)intersecs_ = intersecs.sum(axis=0)  # A x 1labels[intersecs_ > cfg.TRAIN.DONTCARE_AREA_INTERSECTION_HI] = -1# 这里我们暂时不考虑难样本的问题# preclude hard samples that are highly occlusioned, truncated or difficult to seeif cfg.TRAIN.PRECLUDE_HARD_SAMPLES and gt_ishard is not None and gt_ishard.shape[0] > 0:assert gt_ishard.shape[0] == gt_boxes.shape[0]gt_ishard = gt_ishard.astype(int)gt_hardboxes = gt_boxes[gt_ishard == 1, :]if gt_hardboxes.shape[0] > 0:# H x Ahard_overlaps = bbox_overlaps(np.ascontiguousarray(gt_hardboxes, dtype=np.float),  # H x 4np.ascontiguousarray(anchors, dtype=np.float))  # A x 4hard_max_overlaps = hard_overlaps.max(axis=0)  # (A)labels[hard_max_overlaps >= cfg.TRAIN.RPN_POSITIVE_OVERLAP] = -1max_intersec_label_inds = hard_overlaps.argmax(axis=1)  # H x 1labels[max_intersec_label_inds] = -1  ## subsample positive labels if we have too many# 对正样本进行采样,如果正样本的数量太多的话# 限制正样本的数量不超过128个,排除的置位dont_Care类# TODO 这个后期可能还需要修改,毕竟如果使用的是字符的片段,那个正样本的数量是很多的。num_fg = int(cfg.TRAIN.RPN_FG_FRACTION * cfg.TRAIN.RPN_BATCHSIZE)fg_inds = np.where(labels == 1)[0]if len(fg_inds) > num_fg:disable_inds = npr.choice(fg_inds, size=(len(fg_inds) - num_fg), replace=False)  # 随机去除掉一些正样本labels[disable_inds] = -1  # 变为-1# subsample negative labels if we have too many# 对负样本进行采样,如果负样本的数量太多的话# 正负样本总数是256,限制正样本数目最多128,# 如果正样本数量小于128,差的那些就用负样本补上,凑齐256个样本num_bg = cfg.TRAIN.RPN_BATCHSIZE - np.sum(labels == 1)bg_inds = np.where(labels == 0)[0]if len(bg_inds) > num_bg:disable_inds = npr.choice(bg_inds, size=(len(bg_inds) - num_bg), replace=False)labels[disable_inds] = -1# print "was %s inds, disabling %s, now %s inds" % (# len(bg_inds), len(disable_inds), np.sum(labels == 0))# 至此, 上好标签,开始计算rpn-box的真值# --------------------------------------------------------------bbox_targets = np.zeros((len(inds_inside), 4), dtype=np.float32)# 根据anchor和gtbox计算得真值(anchor和gtbox之间的偏差)bbox_targets = _compute_targets(anchors, gt_boxes[argmax_overlaps, :])# 内部权重,前景就给1,其他是0bbox_inside_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)bbox_inside_weights[labels == 1, :] = np.array(cfg.TRAIN.RPN_BBOX_INSIDE_WEIGHTS)  bbox_outside_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)if cfg.TRAIN.RPN_POSITIVE_WEIGHT < 0: # 此处使用uniform权重,也就是正样本是1,负样本是0# uniform weighting of examples (given non-uniform sampling)# num_examples = np.sum(labels >= 0) + 1# positive_weights = np.ones((1, 4)) * 1.0 / num_examples# negative_weights = np.ones((1, 4)) * 1.0 / num_examplespositive_weights = np.ones((1, 4))  # 前景为1negative_weights = np.zeros((1, 4))  # 背景为0else:assert ((cfg.TRAIN.RPN_POSITIVE_WEIGHT > 0) &(cfg.TRAIN.RPN_POSITIVE_WEIGHT < 1))positive_weights = (cfg.TRAIN.RPN_POSITIVE_WEIGHT /(np.sum(labels == 1)) + 1)negative_weights = ((1.0 - cfg.TRAIN.RPN_POSITIVE_WEIGHT) /(np.sum(labels == 0)) + 1)# 外部权重,前景是1,背景是0# bbox_outside_weights初始化为0,将label中为0的位置赋值bbox_outside_weights为0,labels为1的位置赋值为1bbox_outside_weights[labels == 1, :] = positive_weightsbbox_outside_weights[labels == 0, :] = negative_weights# map up to original set of anchors# 一开始是将超出图像范围的anchor直接丢掉的,现在在加回来# inds_inside 是原始anchor中的索引labels = _unmap(labels, total_anchors, inds_inside, fill=-1)  # 这些anchor的label是-1,也即dontcarebbox_targets = _unmap(bbox_targets, total_anchors, inds_inside, fill=0)  # 这些anchor的真值是0,也即没有值bbox_inside_weights = _unmap(bbox_inside_weights, total_anchors,inds_inside, fill=0)  # 内部权重以0填充bbox_outside_weights = _unmap(bbox_outside_weights, total_anchors,inds_inside, fill=0)  # 外部权重以0填充# labelslabels = labels.reshape((1, height, width, A))  # reshap一下labelrpn_labels = labels# bbox_targetsbbox_targets = bbox_targets.reshape((1, height, width, A * 4))  # reshaperpn_bbox_targets = bbox_targets# bbox_inside_weightsbbox_inside_weights = bbox_inside_weights.reshape((1, height, width, A * 4))rpn_bbox_inside_weights = bbox_inside_weights# bbox_outside_weightsbbox_outside_weights = bbox_outside_weights.reshape((1, height, width, A * 4))rpn_bbox_outside_weights = bbox_outside_weightsrpn_data=(rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights)return rpn_data# 将排除掉边界之外的anchors之后的anchor补全回来
def _unmap(data, count, inds, fill=0):""" Unmap a subset of item (data) back to the original set of items (ofsize count) """if len(data.shape) == 1:ret = np.empty((count,), dtype=np.float32)ret.fill(fill)ret[inds] = dataelse:ret = np.empty((count,) + data.shape[1:], dtype=np.float32)ret.fill(fill)ret[inds, :] = datareturn ret# 计算anchor和gt之间的矩形框的偏差
def _compute_targets(ex_rois, gt_rois):"""Compute bounding-box regression targets for an image."""assert ex_rois.shape[0] == gt_rois.shape[0]assert ex_rois.shape[1] == 4assert gt_rois.shape[1] == 5return bbox_transform(ex_rois, gt_rois[:, :4]).astype(np.float32, copy=False)

对于bbox使用cpython写成(.pyx文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
copy

import numpy as np
cimport numpy as npDTYPE = np.float
ctypedef np.float_t DTYPE_t# 计算IOU
def bbox_overlaps(np.ndarray[DTYPE_t, ndim=2] boxes,np.ndarray[DTYPE_t, ndim=2] query_boxes):"""Parameters----------boxes: (N, 4) ndarray of float, anchor box numsquery_boxes: (K, 4) ndarray of float, groud_truth object nums,[x_min,y_min,x_max,y_max,class]Returns-------overlaps: (N, K) ndarray of overlap between boxes and query_boxes"""cdef unsigned int N = boxes.shape[0]cdef unsigned int K = query_boxes.shape[0]cdef np.ndarray[DTYPE_t, ndim=2] overlaps = np.zeros((N, K), dtype=DTYPE)cdef DTYPE_t iw, ih, box_areacdef DTYPE_t uacdef unsigned int k, nfor k in range(K):box_area = ((query_boxes[k, 2] - query_boxes[k, 0] + 1) *(query_boxes[k, 3] - query_boxes[k, 1] + 1))for n in range(N):# 水平方向上的交集,如果存在那么iw为正iw = (min(boxes[n, 2], query_boxes[k, 2]) -max(boxes[n, 0], query_boxes[k, 0]) + 1)if iw > 0:# 竖直方向上的交集ih = (min(boxes[n, 3], query_boxes[k, 3]) -max(boxes[n, 1], query_boxes[k, 1]) + 1)if ih > 0:# 如果存在交集,计算并集的面积# union areaua = float((boxes[n, 2] - boxes[n, 0] + 1) *(boxes[n, 3] - boxes[n, 1] + 1) +box_area - iw * ih)# 交集面积/并集面积overlaps[n, k] = iw * ih / uareturn overlaps# anchor与gt交集面积相对于gt面积的比例
def bbox_intersections(np.ndarray[DTYPE_t, ndim=2] boxes,np.ndarray[DTYPE_t, ndim=2] query_boxes):"""For each query box compute the intersection ratio covered by boxes----------Parameters----------boxes: (N, 4) ndarray of floatquery_boxes: (K, 4) ndarray of floatReturns-------overlaps: (N, K) ndarray of intersec between boxes and query_boxes"""cdef unsigned int N = boxes.shape[0]cdef unsigned int K = query_boxes.shape[0]cdef np.ndarray[DTYPE_t, ndim=2] intersec = np.zeros((N, K), dtype=DTYPE)cdef DTYPE_t iw, ih, box_areacdef DTYPE_t uacdef unsigned int k, nfor k in range(K):box_area = ((query_boxes[k, 2] - query_boxes[k, 0] + 1) *(query_boxes[k, 3] - query_boxes[k, 1] + 1))for n in range(N):iw = (min(boxes[n, 2], query_boxes[k, 2]) -max(boxes[n, 0], query_boxes[k, 0]) + 1)if iw > 0:ih = (min(boxes[n, 3], query_boxes[k, 3]) -max(boxes[n, 1], query_boxes[k, 1]) + 1)if ih > 0:intersec[n, k] = iw * ih / box_areareturn intersec

代码中的注释已经写得明明白白了。anchor生成函数为anchor_target_layer.py

Anchors

首先根据设定的anchor高度和宽度在特征图上每个cell生成A个anchors,这些anchors有的会超过原始图像的边界,如上图所示,将这些超出边界的anchors先删除,并记录保留的anchor在原始所有anchors中的索引值,使用内部的anchor和groundtruth进行IOU计算(anchor和gt之间如果存在交集,则使用交集面积和二者并集的面积进行IOU计算),使用两个原则进行anchor正样本的认定:如果anchor和gt之间的IOU大于设定的阈值0.7则认定该anchor为正样本;将具有和任意gt最大的IOU的anchor为正样本,也就是和gt最大的几个anchor最为正样本,这一步选择的anchor数量和gt的数量相同。至此就确定了正样本的anchor和剩余的负样本anchor,使用设定的正负样本数量,来控制正负样本的数量,将正负样本和和gt之间计算偏移量并作为目标框的label。对于anchor和gt之间的偏移量计算如下图所示

Anchor_groudtruth

图中红色表示groundtruth,黑色表示anchor box,首先计算两个矩形框的中心坐标和宽度高度,计算公式为

targetxtragetytragetwtrageth=(GTx−ANx)/ANwidth=(GTy−any)/ANheight=log(GTwidth/ANwidth)=log(GTheight/ANheight)targetx=(GTx−ANx)/ANwidthtragety=(GTy−any)/ANheighttragetw=log⁡(GTwidth/ANwidth)trageth=log⁡(GTheight/ANheight)

整个流程如下图所示

ctpn_anchor_gen

总结

至此,对CTPN网络结构结合代码进行了一些跟人理解的解读,该模型与2016年提出,可以看到收到很多的fastercnn的影响,可以看到CTPN具有如下的一些特点

  • 基础VGG网络的使用,因此一般需要ImageNet数据集的预训练权重会使得训练更快速和平稳
  • Bilstm的使用使得模型无法向CNN那样并行运算,影响了模型的速度
  • Anchor的设定为等宽度变高度,因此这种anchor只能适用于水平方向文本的检测,也可以通过更改anchor使得anchor兼容竖直方向的文本检测
  • 模型中anchor的宽度为15,因此模型的检测粒度收到该设置的影响,有可能存在边界不明确的状况
  • 因为使用的是和fasterrcnn相同的anchor生成及预测方法,因此在inference阶段需要对预测的值进行反向变换得到目标框

EAST

论文关键idea

  • 提出了两段式的文本检测方法,FCN+NMS,消除多过程造成的中间误差累计,减少了检测时间
  • 模型可以进行单词级别检测,又可以进行文本行检测,检测的形状可以是任意形状的四边形也可以是普通的四边形
  • 采用了Locality-Aware NMS的预测框过滤

网络结构如下所示

EAST Model


Pipeline

  • 先用一个通用的网络(论文中采用的是PVAnet,实际在使用的时候可以采用VGG16,Resnet等)作为base net ,用于特征提取

    此处对PAVnet进行一些说明,PAVnet主要是对VGG进行了改进并应用于目标检测任务,主要针对FasterRcnn的基础网络进行了改进,包含mCReLU,Inception,Hyper-feature各个结构

    PVAnet

    在论文总的基础网络用的是PVAnet的基础网络,具体参数如下所示

    PVAnetParam

    对于mCReLU结构和Inception结构如下所示

    PVAnet mCReLU Inception

  • 基于上述主干特征提取网络,抽取不同层的featuremap(它们的尺寸分别是inuput-image的132,116,18,14132,116,18,14,这样可以得到不同尺度的特征图,这样做的目的是解决文本行尺度变换剧烈的问题,ealy-stage可用于预测小的文本行(较大的特征图),late-stage可用于预测大的文本行(较小的特征图)。

  • 特征合并层,将抽取的特征进行merge.这里合并的规则采用了Unet的方法,合并规则:从特征提取网络的顶部特征按照相应的规则向上进行合并,不断增大featuremap的尺寸。

  • 网络输出层,包含文本得分和文本形状.根据不同文本形状(可分为RBOX和QUAD,对于RROX预测的是当前点距离gtbox的四个边的距离以及gtbox的相对图像的x正方向的角度θ​θ​,也就是总共为5个值分别对应着(d1,d2,d3,d4,θ)​(d1,d2,d3,d4,θ)​,而对于QUAD来说预测对应的gtbox的四个交点的坐标,一共8个值),对于RBOX对应的示意图如下所示


  • EAST_RBOX

    图中的didi对应的是当前点到gt的距离,知道了一个固定点到矩形的四条边的距离,就可以的知道这个矩形所在的位置和大小,即确定这个矩形。

    EAST_RBOX_QUAD

    可以看出,对于RBOX输出5个预测值,而QUAD输出8个预测值。

对于层g和h的计算方式如图中公式所示。

  • 对于g为uppooling层,每次操作将featuremap放大到原来的2倍,主要进行特征图的上采样,论文中采取的双线性插值的方法进行上采样,没有使用反卷积的方式,减少了模型的计算量但是有可能降低模型的表达能力
  • 上采样之后的featuremap和下采样同样尺寸的f层进行merge并使用conv1x1降低合并后的模型的通道数
  • 之后使用conv3x3卷积,输出该阶段的featuremap
  • 上述操作重复3次最终模型输出的通道数为32

进行特征图合并之后进行预测输出,也就是针对不同的box形式输出5个或者8个预测值。

Loss计算

总的损失包含分类损失和回归损失,即

L=LS+λgLgL=LS+λgLg

分类损失论文中使用的是平衡交叉熵损失

LS= balanced−xent(Y˙,Y)=−βYlogY˙−(1−β)(1−Y˙)(log(1−Y˙))whereβ=1−∑y∈Yy|Y|LS= balanced−xent(Y˙,Y)=−βYlog⁡Y˙−(1−β)(1−Y˙)(log⁡(1−Y˙))whereβ=1−∑y∈Yy|Y|

其中Y˙​Y˙​为预测值,Y​Y​为label值。相比普通的交叉熵损失,平衡交叉熵损失对正负样本进行了平衡。

对于LgLg损失,由于在对于RBOX信息中包含的是5个预测值即(d1,d2,d3,d4,θ)(d1,d2,d3,d4,θ),那么就可以得到损失为

whereLg=LAABB+λθLθLAABB=−logIoU(R˙,R∗)=−log|R˙∩R∗||R˙∪R∗|Lθ=1−cos(θ˙−θ∗)Lg=LAABB+λθLθwhereLAABB=−log⁡IoU(R˙,R∗)=−log⁡|R˙∩R∗||R˙∪R∗|Lθ=1−cos⁡(θ˙−θ∗)

对于IOU损失的计算是,论文中对交集区域面积的计算方式为

wi=min(d˙2,d∗2)+min(d˙4,d∗4)hi=min(d˙1,d∗1)+min(d˙3,d∗3)wi=min(d˙2,d2∗)+min(d˙4,d4∗)hi=min(d˙1,d1∗)+min(d˙3,d3∗)

实际上这种计算方式是存在问题的,分析如下

east_iou

如上图所示,红色对应gt,蓝色对应predict,如果不考虑角度,那么按照公式所述是正确的,但是考虑角度信息之后就会发现iou的交集面积计算公式存在错误。

Reference

  • 综述

    自然场景文本检测识别技术综述

    白翔::图像OCR年度进展|VALSE2018之十一

    白翔:趣谈“捕文捉字”— 场景文字检测 | VALSE2017之十

    基于深度学习的目标检测及场景文字检测研究进展

    知乎文本检测综述

    优秀论文解读博客

    知乎专栏:小石头的码疯窝

    OCR_Overview_冠军试炼

  • 文本检测

    • CTPN

      场景文字检测—CTPN原理与实现

      CTPN: Tensorflow

    • EAST

      Bolg: EAST

      知乎:文本检测之EAST

      EAST:tensorflow

      EAST: Keras

      EAST: Advanced keras

    • SegLink

      SegLink_Blog

      文本检测之SegLink

    • PixelLink

      文本检测之PixelLink

      Github: PixelLink

    • TextBoxes

      论文笔记:TextBoxes++: A Single-Shot Oriented Scene Text Detector

      Github: TextBoxes++

    • 角定位

    基于角定位于区域分割

  • 文本识别

    • ASTER

      Github: ASTER

  • TextSpotter

    • Mask TextSpotter

      华科白翔教授团队ECCV2018 OCR论文:Mask TextSpotter

深度学习-TextDetection相关推荐

  1. 深度学习的端到端文本OCR:使用EAST模型从自然场景图片中提取文本

    我们生活在这样一个时代:任何一个组织或公司要想扩大规模并保持相关性,就必须改变他们对技术的看法,并迅速适应不断变化的环境.我们已经知道谷歌是如何实现图书数字化的.或者Google earth是如何使用 ...

  2. 从2012年到现在深度学习领域标志成果

    2006年,Hinton 发表了一篇论文<A Fast Learning Algorithm for Deep Belief Nets>,提出了降维和逐层预训练方法,该方法可成功运用于训练 ...

  3. 各种优化算法公式快速回忆优化器-深度学习

    本文是Deep Learning 之 最优化方法系列文章的RMSProp方法.主要参考Deep Learning 一书. 整个优化系列文章列表: Deep Learning 之 最优化方法 Deep ...

  4. 卷积神经网络之卷积计算、作用与思想 深度学习

    博客:blog.shinelee.me | 博客园 | CSDN 卷积运算与相关运算 在计算机视觉领域,卷积核.滤波器通常为较小尺寸的矩阵,比如3×33×3.从这个角度看,多层卷积是在进行逐层映射,整 ...

  5. 矩阵的卷积核运算(一个简单小例子的讲解)深度学习

    卷积运算:假设有一个卷积核h,就一般为3*3的矩阵: 有一个待处理矩阵A: h*A的计算过程分为三步 第一步,将卷积核翻转180°,也就是成为了 第二步,将卷积核h的中心对准x的第一个元素,然后对应元 ...

  6. 深度学习优化函数详解(5)-- Nesterov accelerated gradient (NAG) 优化算法

    深度学习优化函数详解系列目录 深度学习优化函数详解(0)– 线性回归问题 深度学习优化函数详解(1)– Gradient Descent 梯度下降法 深度学习优化函数详解(2)– SGD 随机梯度下降 ...

  7. transformer bert seq2seq 深度学习 编码和解码的逻辑-重点

    参考文献: 详解从 Seq2Seq模型.RNN结构.Encoder-Decoder模型 到 Attention模型 [NLP]Attention Model(注意力模型)学习总结(https://ww ...

  8. 入门指南目录页 -PaddlePaddle 飞桨 入门指南 FAQ合集-深度学习问题

    入门指南目录页 -PaddlePaddle 飞桨 入门指南 FAQ合集 GT_Zhang关注 0.1012019.08.01 18:43:34字数 1,874阅读 795 Hi,欢迎各位来自Paddl ...

  9. 深度学习的分布式训练--数据并行和模型并行

    <div class="htmledit_views"> 在深度学习这一领域经常涉及到模型的分布式训练(包括一机多GPU的情况).我自己在刚刚接触到一机多卡,或者分布式 ...

  10. 1-1 机器学习和深度学习综述-paddle

    课程>我的课程>百度架构师手把手教深度学习>1-1 机器学习和深度学习综述> 1-1 机器学习和深度学习综述 paddle初级课程 王然(学生) Notebook 教育 初级深 ...

最新文章

  1. 互联网企业安全高级指南3.7.1 攻防驱动修改
  2. 使用KiWi Syslog Daemon构建日志服务器
  3. 在编译内核时出现uudecode错误
  4. 【笔记】windows10安装linux双系统教程(可能是现今最简单方法)
  5. [转]20年来我得到的20条编程经验
  6. JAVA多线程和并发基础面试问答(转载)
  7. python 直方图每个bin中的值_使用python中的matplotlib进行绘图分析数据
  8. 超简单炫彩抽象线条感海报PSD分层素材,一切变得简单!
  9. 英语----情态动词---半情态动词
  10. mysql 字符串 反转_MySQL笔记之字符串函数的应用
  11. 树莓派3B通过mentohust登录锐捷校园网有线端,并创建WIFI(开热点)供其他设备使用,同时实现开机自启动
  12. 金税盘专、普红字发票开具步骤及(税盘注销方法)
  13. deb 中标麒麟_中标麒麟linux
  14. FreeBSD安装与配置(转)
  15. 回答一个关于产品经理的入门门槛高不高的问题
  16. 删除linux终端的历史命令,清除Linux终端命令的历史记录
  17. 接入微信提现Api(企业付款到零钱--向微信用户个人付款)
  18. R入门(一)----读取数据、查看数据
  19. [转载] 七龙珠第一部——第041话 玛斯鲁塔的毁灭
  20. MOS驱动电机正反转

热门文章

  1. yolov5样本处理方式
  2. java 将月份、星期转换为英文
  3. HEX编码、Base64编码
  4. Q1:如何用 C# 计算相对时间 ?
  5. vs2008中文版 下载
  6. android7.0 root教程,小米4S(全网通 安卓7.0)一键ROOT详解教程,看教程ROOT
  7. 倡议书格式范文_倡议书的格式及范文
  8. 获取取本月一号、本月月末 日期
  9. Cisco路由器之IPSec 虚拟专用网(内附配置案例)
  10. Baklib每日分享|在线产品手册的制作技巧