step 1: 文字方向检测
- 包含:VGG16 的一个四分类算法(0,90,180,270),小角度的检测,estimate_skew_angle
- 该步骤可以跳过,因为算法在一定角度范围内具有鲁棒性
step 2: 文本检测
- 用 yolo 检测出含有文本框的区域 text_proposals;
Note:图像输入固定压缩到 608*608,框的宽度固定为 8,高度 11~283 一共 9 个 anchors,框没框住文字无所谓,主要框的高度与文字的高度 overlap 超过一定的阈值即可定义为正样本- text_proposals 通过文本线构造算法进行同行合并;
Note:文本线构造法就是左右搜索 3 个框,看高度的 overlap 如果大于一定的阈值就将其连接在一起,当然还有一些细节操作,需要看代码逻辑,总之这一步就是将同一行的 proposals 连接一起- 同一行的框的中心拟合一条直线,然后通过 xmin,xmax 再加上直线参数获取框的四个点(带有旋转性质的),boxes;
- 对 boxes 进行 y 排序,相当于将文本行从上到下进行排序;
- 根据 boxes 的每一行一个个的输入给 CRNN。
step 3: 文本识别
- 根据 box 旋转 ROI 区域图像,进入 CNN 前,图像高度压缩到 32,输出大小为(batch,w,512),因为高度固定为 32,经过 CNN 中 5 次下采样之后特征图的高度变为 1,接下来 CNN 的输出要输入到 BLSTM 中,LSTM 输出的维度是 w,也就是对应于原图每 8 个像素就有一个预测,因为横向只下采样了 3 次,也就是相当于每一个 proposal 都有一个预测,这样肯定会有重复的预测,采用 CTC 进行去重生成真实的标签序列
作者通过对 yolo 进行方法改进提取 text_proposals
step 1: 首先第一个改进,是将宽度限制在 8,其余还是一样,一共九个 achors,输出三层,每层 3 个 anchors 预测,不同于 yolo 原文输入固定 resize 到 416*416
,这里是固定在 608*608
,分类为 2,判断是文字与否。
# 文字检测引擎
pwd = os.getcwd()
opencvFlag = 'keras' # keras,opencv,darknet,模型性能 keras>darknet>opencv
IMGSIZE = (608,608) # yolo3 输入图像尺寸
# keras 版本anchors
keras_anchors = '8,11, 8,16, 8,23, 8,33, 8,48, 8,97, 8,139, 8,198, 8,283'
class_names = ['none','text',]
kerasTextModel=os.path.join(pwd, "models", "text.h5") # keras版本模型权重文件
############## darknet yolo ##############
darknetRoot = os.path.join(os.path.curdir,"darknet") # yolo 安装目录
yoloCfg = os.path.join(pwd,"models","text.cfg")
yoloWeights = os.path.join(pwd,"models","text.weights")
yoloData = os.path.join(pwd,"models","text.data")
step 2: 接下来根据 anchors 总数,制造后处理过程,yolo 会在三个输出特征图上做预测,将 anchors_num/3=9/3
,每层上预测 3 种尺度的 anchors。
def yolo_text(num_classes,anchors,train=False):
imgInput = Input(shape=(None,None,3))
darknet = Model(imgInput, darknet_body(imgInput))
num_anchors = len(anchors)//3
x, y1 = make_last_layers(darknet.output, 512, num_anchors*(num_classes+5))
x = compose(DarknetConv2D_BN_Leaky(256, (1,1)),
UpSampling2D(2))(x)
x = Concatenate()([x,darknet.layers[152].output])
x, y2 = make_last_layers(x, 256, num_anchors*(num_classes+5))
x = compose(DarknetConv2D_BN_Leaky(128, (1,1)),
UpSampling2D(2))(x)
x = Concatenate()([x,darknet.layers[92].output])
x, y3 = make_last_layers(x, 128, num_anchors*(num_classes+5))
out = [y1,y2,y3]
if train:
num_anchors = len(anchors)
y_true = [Input(shape=(None, None,num_anchors//3, num_classes+5)) for l in range(3)]
loss = Lambda(yolo_loss,output_shape=(4,),name='loss',
arguments={
'anchors': anchors,
'num_classes': num_classes,
'ignore_thresh': 0.5,})(out+y_true)
def get_loss(loss,index):
return loss[index]
lossName = ['class_loss','xy_loss','wh_loss','confidence_loss']
lossList = [Lambda(get_loss,output_shape=(1,),name=lossName[i],arguments={
'index':i})(loss) for i in range(4)]
textModel = Model([imgInput, *y_true], lossList)
return textModel
else:
textModel = Model([imgInput],out)
return textModel
step 3: 接着,加载预训练模型
textModel.load_weights('models/text.h5') # 加载预训练模型权重
step 4: 接着,读取并增强数据
trainLoad = data_generator(jpgPath[:num_train], anchors, num_classes,splitW)
testLoad = data_generator(jpgPath[num_train:], anchors, num_classes,splitW)
step 5: 从 label 当中得到带旋转尺度的框后,按照行宽为 8 分割 boxes
def get_box_spilt(boxes,im,sizeW,SizeH,splitW=8,isRoate=False,rorateDegree=0):
""" isRoate:是否旋转box """
size = sizeW,SizeH
if isRoate:
# 旋转box
im,boxes = get_rorate(boxes,im,degree=rorateDegree)
# 采用 padding 的方式不改变比例的压缩图像
newIm,f = letterbox_image(im, size)
# 图像压缩后。boxes 也要做相应的压缩
newBoxes = resize_box(boxes,f)
#按照行 8 分割 box,一直分割覆盖包含最后
newBoxes = sum(box_split(newBoxes,splitW),[])
newBoxes = [box+[1] for box in newBoxes]
return newBoxes,newIm
def box_split(boxes,splitW = 15):
newBoxes = []
for box in boxes:
w = box['w']
h = box['h']
cx = box['cx']
cy=box['cy']
angle = box['angle']
x1,y1,x2,y2,x3,y3,x4,y4 = xy_rotate_box(cx,cy,w,h,angle)
splitBoxes =[]
i = 1
tanAngle = tan(-angle)
while True:
flag = 0 if i==1 else 1
xmin = x1+(i-1)*splitW
ymin = y1-tanAngle*splitW*i
xmax = x1+i*splitW
ymax = y4-(i-1)*tanAngle*splitW +flag*tanAngle*(x4-x1)
if xmax>max(x2,x3) and xmin>max(x2,x3):
break
splitBoxes.append([int(xmin),int(ymin),int(xmax),int(ymax)])
i+=1
newBoxes.append(splitBoxes)
return newBoxes
step 6: 确定优化器
adam = tf.keras.optimizers.Adam(lr=0.0005)
step 7: loss 损失设置
def yolo_loss(args, anchors, num_classes, ignore_thresh=.5):
# 详细看代码
pass