自然场景下的文本检测和识别 EAST text detector and recognition

自然场景下的文本检测和识别 EAST text detector and recognition

最近在做巡检机器人和仪表识别算法,巡检机器人拍摄的照片除了指针仪表和状态灯以外,还有一部分是数字显示的仪表,这样对仪表的数值的识别就需要后台代码具备检测文本和识别的功能了.
另外,一些项目中也有对移动的车厢或者罐子上的编号做识别处理,这样一套算法就可以搞定这些问题了.

仪表面板

铁罐编号1

铁罐编号2

1. EAST text detector 模型

自然场景下的文本检测模型,参考了 Zhou et al.的在arxiv上的论文方法. 论文链接

  • 使用ResNet-50残差网络作为基础.
  • 使用dice loss 损失函数.
  • 使用了 AdamW 优化器.
模型定义如下
import keras
from keras import layers, Input, Model
import tensorflow as tf
from east.layers.base_net import resnet50
from east.layers.losses import balanced_cross_entropy, iou_loss, angle_loss
from east.layers.rbox import dist_to_box


def merge_block(f_pre, f_cur, out_channels, index):
    """
    east网络特征合并块
    :param f_pre:
    :param f_cur:
    :param out_channels:输出通道数
    :param index:block index
    :return:
    """
    # 上采样
    up_sample = layers.UpSampling2D(size=2, name="east_up_sample_f{}".format(index - 1))(f_pre)
    # 合并
    merge = layers.Concatenate(name='east_merge_{}'.format(index))([up_sample, f_cur])
    # 1*1 降维
    x = layers.Conv2D(out_channels, (1, 1), padding='same', name='east_reduce_channel_conv_{}'.format(index))(merge)
    x = layers.BatchNormalization(name='east_reduce_channel_bn_{}'.format(index))(x)
    x = layers.Activation(activation='relu', name='east_reduce_channel_relu_{}'.format(index))(x)
    # 3*3 提取特征
    x = layers.Conv2D(out_channels, (3, 3), padding='same', name='east_extract_feature_conv_{}'.format(index))(x)
    x = layers.BatchNormalization(name='east_extract_feature_bn_{}'.format(index))(x)
    x = layers.Activation(activation='relu', name='east_extract_feature_relu_{}'.format(index))(x)
    return x


def east(features):
    """
    east网络头
    :param features: 特征列表: f1, f2, f3, f4分别代表32,16,8,4倍下采样的特征
    :return:
    """
    f1, f2, f3, f4 = features
    # 特征合并分支
    h2 = merge_block(f1, f2, 128, 2)
    h3 = merge_block(h2, f3, 64, 3)
    h4 = merge_block(h3, f4, 32, 4)
    # 提取g4特征
    x = layers.Conv2D(32, (3, 3), padding='same', name='east_g4_conv')(h4)
    x = layers.BatchNormalization(name='east_g4_bn')(x)
    x = layers.Activation(activation='relu', name='east_g4_relu')(x)

    # 预测得分
    predict_score = layers.Conv2D(1, (1, 1), name='predict_score_map')(x)
    # 预测距离
    predict_geo_dist = layers.Conv2D(4, (1, 1), activation='relu', name='predict_geo_dist')(x)  # 距离必须大于零
    # 预测角度
    predict_geo_angle = layers.Conv2D(1, (1, 1), name='predict_geo_angle')(x)

    return predict_score, predict_geo_dist, predict_geo_angle


def east_net(config, stage='train'):
    # 输入
    h, w = list(config.IMAGE_SHAPE)[:2]
    h, w = h / 4, w / 4
    input_image = Input(shape=config.IMAGE_SHAPE, name='input_image')
    input_score_map = Input(shape=(h, w), name='input_score')
    input_geo_dist = Input(shape=(h, w, 4), name='input_geo_dist')  # rbox 4个边距离
    input_geo_angle = Input(shape=(h, w), name='input_geo_angle')  # rbox 角度
    input_mask = Input(shape=(h, w), name='input_mask')
    input_image_meta = Input(shape=(12,), name='input_image_meta')

    # 网络
    features = resnet50(input_image)
    predict_score, predict_geo_dist, predict_geo_angle = east(features)

    if stage == 'train':
        # 增加损失函数层
        score_loss = layers.Lambda(lambda x: balanced_cross_entropy(*x), name='score_loss')(
            [input_score_map, predict_score, input_mask])
        geo_dist_loss = layers.Lambda(lambda x: iou_loss(*x), name='dist_loss')(
            [input_geo_dist, predict_geo_dist, input_score_map, input_mask])
        geo_angle_loss = layers.Lambda(lambda x: angle_loss(*x), name='angle_loss')(
            [input_geo_angle, predict_geo_angle, input_score_map, input_mask])

        return Model(inputs=[input_image, input_score_map, input_geo_dist, input_geo_angle, input_mask],
                     outputs=[score_loss, geo_dist_loss, geo_angle_loss])
    else:
        # 距离和角度转为顶点坐标
        vertex = layers.Lambda(lambda x: dist_to_box(*x))([predict_geo_dist, predict_geo_angle])
        # dual image_meta
        image_meta = layers.Lambda(lambda x: tf.identity(x))(input_image_meta)  # 原样返回
        predict_score = layers.Lambda(lambda x: tf.nn.sigmoid(x))(predict_score)  # logit转为score
        return Model(inputs=[input_image, input_image_meta],
                     outputs=[predict_score, vertex, image_meta])


def compile(keras_model, config, loss_names=[]):
    """
    编译模型,增加损失函数,L2正则化以
    :param keras_model:
    :param config:
    :param loss_names: 损失函数列表
    :return:
    """
    # 优化目标
    optimizer = keras.optimizers.SGD(
        lr=config.LEARNING_RATE, momentum=config.LEARNING_MOMENTUM,
        clipnorm=config.GRADIENT_CLIP_NORM)
    # 增加损失函数,首先清除之前的,防止重复
    keras_model._losses = []
    keras_model._per_input_losses = {}

    for name in loss_names:
        layer = keras_model.get_layer(name)
        if layer is None or layer.output in keras_model.losses:
            continue
        loss = (tf.reduce_mean(layer.output, keepdims=True)
                * config.LOSS_WEIGHTS.get(name, 1.))
        keras_model.add_loss(loss)

    # 增加L2正则化
    # 跳过批标准化层的 gamma 和 beta 权重
    reg_losses = [
        keras.regularizers.l2(config.WEIGHT_DECAY)(w) / tf.cast(tf.size(w), tf.float32)
        for w in keras_model.trainable_weights
        if 'gamma' not in w.name and 'beta' not in w.name]
    keras_model.add_loss(tf.add_n(reg_losses))

    # 编译
    keras_model.compile(
        optimizer=optimizer,
        loss=[None] * len(keras_model.outputs))  # 使用虚拟损失

    # 为每个损失函数增加度量
    for name in loss_names:
        if name in keras_model.metrics_names:
            continue
        layer = keras_model.get_layer(name)
        if layer is None:
            continue
        keras_model.metrics_names.append(name)
        loss = (
                tf.reduce_mean(layer.output, keepdims=True)
                * config.LOSS_WEIGHTS.get(name, 1.))
        keras_model.metrics_tensors.append(loss)


def add_metrics(keras_model, metric_name_list, metric_tensor_list):
    """
    增加度量
    :param keras_model: 模型
    :param metric_name_list: 度量名称列表
    :param metric_tensor_list: 度量张量列表
    :return: 无
    """
    for name, tensor in zip(metric_name_list, metric_tensor_list):
        keras_model.metrics_names.append(name)
        keras_model.metrics_tensors.append(tf.reduce_mean(tensor, keepdims=True))

2. 文本识别

EAST text detector实现了文本定位和检测,下一步需要对检测的文本做识别处理

将图像中的文字转化为真正的文本,就需要用到OCR的技术。OCR领域最著名的、最主流的开源实现是Tesseract-OCR,鉴于本次识别的都是印刷体和简单的数字,直接采用google成熟的OCR识别工具集tesseract-ocr就可以了,尤其是当Tesseract-OCR已经升级到了4.0版本。和传统的版本(3.x)比,4.0时代最突出的变化就是基于LSTM神经网络。

3. 整合成端到端的代码 end to end

把EAST text detector 和 tesseract-ocr整合到一套代码中实现端到端的解决方案,实现图片的文字检测,分割和识别输出的一系列操作.

仪表面板

铁罐编号1

铁罐编号2
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容