【AI實戰】手把手教你文字識別(識別篇:LSTM+CTC, CRNN, chineseocr方法)

文字識別是AI的一個重要應用場景,文字識別過程通常由圖像輸入、預處理、文本檢測、文本識別、結果輸出等環節組成。
 
其中,文本檢測、文本識別是最核心的環節。文本檢測方面,在前面的文章中已介紹過了多種基於深度學習的方法,可針對各類場景實現對文字的檢測,詳見如下文章:python

【AI實戰】手把手教你文字識別(檢測篇:MSER、CTPN、SegLink、EAST等方法)
git

 

【AI實戰】手把手教你文字識別(檢測篇:AdvancedEAST、PixelLink方法)
 
github


而本文主要就是介紹在「文本識別」方面的實戰方法,只要掌握了這些方法,那麼跟前面介紹的文本檢測方法結合起來,就能夠輕鬆應對各類文字識別的任務了。話很少說,立刻來學習「文本識別」的方法。web

文字識別可根據待識別的文字特色採用不一樣的識別方法,通常分爲定長文字、不定長文字兩大類別。算法

  • 定長文字(例如驗證碼),因爲字符數量固定,採用的網絡結構相對簡單,識別也比較容易;
  • 不定長文字(例如印刷文字、廣告牌文字等),因爲字符數量是不固定的,所以須要採用比較複雜的網絡結構和後處理環節,識別也具備必定的難度。

下面按照定長文字、不定長文字分別介紹識別方法。bash

1、定長文字識別
定長文字的識別相對簡單,應用場景也比較侷限,最典型的場景就是驗證碼的識別了。因爲字符數量是已知的、固定的,所以,網絡結構比較簡單,通常構建3層卷積層,2層全鏈接層便能知足「定長文字」的識別。
具體方法在以前介紹驗證碼識別的文章中已詳細介紹,在此再也不贅述。詳見文章:微信

【AI實戰】文字識別(驗證碼識別)網絡


 
2、不定長文字識別
不定長文字在現實中大量存在,例如印刷文字、廣告牌文字等,因爲字符數量不固定、不可預知,所以,識別的難度也較大,這也是目前研究文字識別的主要方向。下面介紹不定長文字識別的經常使用方法:LSTM+CTC、CRNN、chinsesocr。
一、LSTM+CTC 方法
(1)什麼是LSTM
爲了實現對不定長文字的識別,就須要有一種能力更強的模型,該模型具備必定的記憶能力,可以按時序依次處理任意長度的信息,這種模型就是「循環神經網絡」(Recurrent Neural Networks,簡稱RNN)。
LSTM(Long Short Term Memory,長短時間記憶網絡)是一種特殊結構的RNN(循環神經網絡),用於解決RNN的長期依賴問題,也即隨着輸入RNN網絡的信息的時間間隔不斷增大,普通RNN就會出現「梯度消失」或「梯度爆炸」的現象,這就是RNN的長期依賴問題,而引入LSTM便可以解決這個問題。LSTM單元由輸入門(Input Gate)、遺忘門(Forget Gate)和輸出門(Output Gate)組成,具體的技術原理的工做過程詳見以前的文章(文章:白話循環神經網絡(RNN)),LSTM的結構以下圖所示。
 
(2)什麼是CTC
CTC(Connectionist Temporal Classifier,聯接時間分類器),主要用於解決輸入特徵與輸出標籤的對齊問題。例以下圖,因爲文字的不一樣間隔或變形等問題,致使同個文字有不一樣的表現形式,但實際上都是同一個文字。在識別時會將輸入圖像分塊後再去識別,得出每塊屬於某個字符的機率(沒法識別的標記爲特殊字符」-」),以下圖:
 
因爲字符變形等緣由,致使對輸入圖像分塊識別時,相鄰塊可能會識別爲同個結果,字符重複出現。所以,經過CTC來解決對齊問題,模型訓練後,對結果中去掉間隔字符、去掉重複字符(若是同個字符連續出現,則表示只有1個字符,若是中間有間隔字符,則表示該字符出現屢次),以下圖所示
 
(3)LSTM+CTC實現:常量定義
定義一些常量,在模型訓練和預測中使用,定義以下:session

# 數據集,可根據須要增長英文或其它字符
DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

# 分類數量
num_classes = len(DIGITS) + 1     # 數據集字符數+特殊標識符

# 圖片大小,32 x 256
OUTPUT_SHAPE = (32, 256)

# 學習率
INITIAL_LEARNING_RATE = 1e-3
DECAY_STEPS = 5000
REPORT_STEPS = 100
LEARNING_RATE_DECAY_FACTOR = 0.9
MOMENTUM = 0.9

# LSTM網絡層次
num_hidden = 128
num_layers = 2

# 訓練輪次、批量大小
num_epochs = 50000
BATCHES = 10
BATCH_SIZE = 32
TRAIN_SIZE = BATCHES * BATCH_SIZE

# 數據集目錄、模型目錄
data_dir = '/tmp/lstm_ctc_data/'
model_dir = '/tmp/lstm_ctc_model/'

(4)LSTM+CTC實現:隨機生成不定長圖片數據
爲了訓練和測試LSTM+CTC識別模型,先要準備好基礎數據,可根據須要準備好已標註的文本圖片集。在這裏,爲了方便訓練和測試模型,隨機生成10000張不定長的圖片數據集。經過使用Pillow生成圖片和繪上文字,並對圖片隨機疊加椒鹽噪聲,以更加貼近現實場景。核心代碼以下:app

# 生成椒鹽噪聲
def img_salt_pepper_noise(src,percetage):
    NoiseImg=src
    NoiseNum=int(percetage*src.shape[0]*src.shape[1])
    for i in range(NoiseNum):
        randX=random.randint(0,src.shape[0]-1)
        randY=random.randint(0,src.shape[1]-1)
        if random.randint(0,1)==0:
            NoiseImg[randX,randY]=0
        else:
            NoiseImg[randX,randY]=255
    return NoiseImg

# 隨機生成不定長圖片集
def gen_text(cnt):
    # 設置文字字體和大小
    font_path = '/data/work/tensorflow/fonts/arial.ttf'
    font_size = 30
    font=ImageFont.truetype(font_path,font_size)

for i in range(cnt):
        # 隨機生成1到10位的不定長數字
        rnd = random.randint(1, 10)
        text = ''
        for j in range(rnd):
            text = text + DIGITS[random.randint(0, len(DIGITS) - 1)]

# 生成圖片並繪上文字
        img=Image.new("RGB",(256,32))
        draw=ImageDraw.Draw(img)
        draw.text((1,1),text,font=font,fill='white')
        img=np.array(img)

# 隨機疊加椒鹽噪聲並保存圖像
        img = img_salt_pepper_noise(img, float(random.randint(1,10)/100.0))
        cv2.imwrite(data_dir + text + '_' + str(i+1) + '.jpg',img)

隨機生成的不定長數據效果以下:
 
執行 gen_text(10000) 後生成的圖片集以下,文件名由序號和文字標籤組成:

(5)LSTM+CTC實現:標籤向量化(稀疏矩陣)
因爲文字是不定長的,所以,若是讀取圖片並獲取標籤,而後將標籤存放在一個緊密矩陣中進行向量化,那將會出現大量的零元素,很浪費空間。所以,使用稀疏矩陣對標籤進行向量化。所謂「稀疏矩陣」就是矩陣中的零元素遠遠多於非零元素,採用這種方式存儲可有效節約空間。
稀疏矩陣有3個屬性,分別是:

  • indices:二維矩陣,表明非零的座標點
  • values:二維tensor,表明indice位置的數據值
  • dense_shape:一維,表明稀疏矩陣的大小(取行數和列的最大長度)


例如讀取了如下圖片和相應的標籤,那麼存儲爲稀疏矩陣的結果以下:
 
將標籤轉爲稀疏矩陣,對標籤進行向量化,核心代碼以下:

# 序列轉爲稀疏矩陣
# 輸入:序列
# 輸出:indices非零座標點,values數據值,shape稀疏矩陣大小
def sparse_tuple_from(sequences, dtype=np.int32):
    indices = []
    values = []

for n, seq in enumerate(sequences):
        indices.extend(zip([n] * len(seq), range(len(seq))))
        values.extend(seq)

indices = np.asarray(indices, dtype=np.int64)
    values = np.asarray(values, dtype=dtype)
    shape = np.asarray([len(sequences), np.asarray(indices).max(0)[1] + 1], dtype=np.int64)

return indices, values, shape

將稀疏矩陣轉爲標籤,用於輸出結果,核心代碼以下:

# 稀疏矩陣轉爲序列
# 輸入:稀疏矩陣
# 輸出:序列
def decode_sparse_tensor(sparse_tensor):
    decoded_indexes = list()
    current_i = 0
    current_seq = []

for offset, i_and_index in enumerate(sparse_tensor[0]):
        i = i_and_index[0]
        if i != current_i:
            decoded_indexes.append(current_seq)
            current_i = i
            current_seq = list()
        current_seq.append(offset)
    decoded_indexes.append(current_seq)

result = []
    for index in decoded_indexes:
        result.append(decode_a_seq(index, sparse_tensor))
    return result

# 序列編碼轉換
def decode_a_seq(indexes, spars_tensor):
    decoded = []
    for m in indexes:
        str = DIGITS[spars_tensor[1][m]]
        decoded.append(str)
    return decoded

(6)LSTM+CTC實現:讀取數據
讀取圖像數據以及進行標籤向量化,以便於輸入到模型進行訓練,核心代碼以下:

# 將文件和標籤讀到內存,減小磁盤IO
def get_file_text_array():
    file_name_array=[]
    text_array=[]

for parent, dirnames, filenames in os.walk(data_dir):
        file_name_array=filenames

for f in file_name_array:
        text = f.split('_')[0]
        text_array.append(text)

return file_name_array,text_array

# 獲取訓練的批量數據
def get_next_batch(file_name_array,text_array,batch_size=64):
    inputs = np.zeros([batch_size, OUTPUT_SHAPE[1], OUTPUT_SHAPE[0]])
    codes = []

# 獲取訓練樣本
    for i in range(batch_size):
        index = random.randint(0, len(file_name_array) - 1)
        image = cv2.imread(data_dir + file_name_array[index])
        image = cv2.resize(image, (OUTPUT_SHAPE[1], OUTPUT_SHAPE[0]), 3)
        image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY)
        text = text_array[index]

# 矩陣轉置
        inputs[i, :] = np.transpose(image.reshape((OUTPUT_SHAPE[0], OUTPUT_SHAPE[1])))
        # 標籤轉成列表
        codes.append(list(text))

# 標籤轉成稀疏矩陣
    targets = [np.asarray(i) for i in codes]
    sparse_targets = sparse_tuple_from(targets)
    seq_len = np.ones(inputs.shape[0]) * OUTPUT_SHAPE[1]

return inputs, sparse_targets, seq_len

(7)LSTM+CTC實現:構建網絡
利用tensorflow內置的LSTM單元構建網絡,核心代碼以下:

def get_train_model():
    # 輸入
    inputs = tf.placeholder(tf.float32, [None, None, OUTPUT_SHAPE[0]]) 

# 稀疏矩陣
    targets = tf.sparse_placeholder(tf.int32)

# 序列長度 [batch_size,]
    seq_len = tf.placeholder(tf.int32, [None])

# 定義LSTM網絡
    cell = tf.contrib.rnn.LSTMCell(num_hidden, state_is_tuple=True)
    stack = tf.contrib.rnn.MultiRNNCell([cell] * num_layers, state_is_tuple=True)      # old
    outputs, _ = tf.nn.dynamic_rnn(cell, inputs, seq_len, dtype=tf.float32)
    shape = tf.shape(inputs)
    batch_s, max_timesteps = shape[0], shape[1]

outputs = tf.reshape(outputs, [-1, num_hidden])
    W = tf.Variable(tf.truncated_normal([num_hidden,
                                         num_classes],
                                        stddev=0.1), name="W")
    b = tf.Variable(tf.constant(0., shape=[num_classes]), name="b")
    logits = tf.matmul(outputs, W) + b
    logits = tf.reshape(logits, [batch_s, -1, num_classes])

# 轉置矩陣
    logits = tf.transpose(logits, (1, 0, 2))

return logits, inputs, targets, seq_len, W, b

(8)LSTM+CTC實現:模型訓練
在訓練以前,先定義好準確率評估方法,以便於在訓練過程當中不斷評估模型的準確性,核心代碼以下:

# 準確性評估
# 輸入:預測結果序列 decoded_list ,目標序列 test_targets
# 返回:準確率
def report_accuracy(decoded_list, test_targets):
    original_list = decode_sparse_tensor(test_targets)
    detected_list = decode_sparse_tensor(decoded_list)

# 正確數量
    true_numer = 0

# 預測序列與目標序列的維度不一致,說明有些預測失敗,直接返回
    if len(original_list) != len(detected_list):
        print("len(original_list)", len(original_list), "len(detected_list)", len(detected_list),
              " test and detect length desn't match")
        return

# 比較預測序列與結果序列是否一致,並統計準確率        
    print("T/F: original(length) <-------> detectcted(length)")
    for idx, number in enumerate(original_list):
        detect_number = detected_list[idx]
        hit = (number == detect_number)
        print(hit, number, "(", len(number), ") <-------> ", detect_number, "(", len(detect_number), ")")
        if hit:
            true_numer = true_numer + 1
    accuracy = true_numer * 1.0 / len(original_list)
    print("Test Accuracy:", accuracy)

return accuracy

接着開始對模型進行訓練,核心代碼以下:

def train():
    # 獲取訓練樣本數據
    file_name_array, text_array = get_file_text_array()

# 定義學習率
    global_step = tf.Variable(0, trainable=False)
    learning_rate = tf.train.exponential_decay(INITIAL_LEARNING_RATE,
                                               global_step,
                                               DECAY_STEPS,
                                               LEARNING_RATE_DECAY_FACTOR,
                                               staircase=True)
    # 獲取網絡結構
    logits, inputs, targets, seq_len, W, b = get_train_model()

# 設置損失函數
    loss = tf.nn.ctc_loss(labels=targets, inputs=logits, sequence_length=seq_len)
    cost = tf.reduce_mean(loss)

# 設置優化器
    optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss, global_step=global_step)
    decoded, log_prob = tf.nn.ctc_beam_search_decoder(logits, seq_len, merge_repeated=False)
    acc = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32), targets))

init = tf.global_variables_initializer()
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True

with tf.Session() as session:
        session.run(init)
        saver = tf.train.Saver(tf.global_variables(), max_to_keep=10)

for curr_epoch in range(num_epochs):
            train_cost = 0
            train_ler = 0
            for batch in range(BATCHES):
                # 訓練模型
                train_inputs, train_targets, train_seq_len = get_next_batch(file_name_array, text_array, BATCH_SIZE)
                feed = {inputs: train_inputs, targets: train_targets, seq_len: train_seq_len}
                b_loss, b_targets, b_logits, b_seq_len, b_cost, steps, _ = session.run(
                    [loss, targets, logits, seq_len, cost, global_step, optimizer], feed)

# 評估模型
                if steps > 0 and steps % REPORT_STEPS == 0:
                    test_inputs, test_targets, test_seq_len = get_next_batch(file_name_array, text_array, BATCH_SIZE)
                    test_feed = {inputs: test_inputs,targets: test_targets,seq_len: test_seq_len}
                    dd, log_probs, accuracy = session.run([decoded[0], log_prob, acc], test_feed)
                    report_accuracy(dd, test_targets)

# 保存識別模型
                    save_path = saver.save(session, model_dir + "lstm_ctc_model.ctpk",global_step=steps)

c = b_cost
                train_cost += c * BATCH_SIZE

train_cost /= TRAIN_SIZE
            # 計算 loss
            train_inputs, train_targets, train_seq_len = get_next_batch(file_name_array, text_array, BATCH_SIZE)
            val_feed = {inputs: train_inputs,targets: train_targets,seq_len: train_seq_len}
            val_cost, val_ler, lr, steps = session.run([cost, acc, learning_rate, global_step], feed_dict=val_feed)

log = "{} Epoch {}/{}, steps = {}, train_cost = {:.3f}, val_cost = {:.3f}"
            print(log.format(curr_epoch + 1, num_epochs, steps, train_cost, val_cost))

通過一段時間的訓練,執行了600多步後,評估的準確性已所有預測正確,以下圖:

(8)LSTM+CTC實現:能力封裝
爲了方便其它程序調用LSTM+CTC的識別能力,對識別能力進行封裝,只須要輸入一張圖片,便可識別後返回結果。核心代碼以下:

# LSTM+CTC 文字識別能力封裝
# 輸入:圖片
# 輸出:識別結果文字
def predict(image):

# 獲取網絡結構
    logits, inputs, targets, seq_len, W, b = get_train_model()
    decoded, log_prob = tf.nn.ctc_beam_search_decoder(logits, seq_len, merge_repeated=False)

saver = tf.train.Saver()
    with tf.Session() as sess:
        # 加載模型
        saver.restore(sess, tf.train.latest_checkpoint(model_dir))
        # 圖像預處理
        image = cv2.resize(image, (OUTPUT_SHAPE[1], OUTPUT_SHAPE[0]), 3)
        image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
        pred_inputs = np.zeros([1, OUTPUT_SHAPE[1], OUTPUT_SHAPE[0]])
        pred_inputs[0, :] = np.transpose(image.reshape((OUTPUT_SHAPE[0], OUTPUT_SHAPE[1])))
        pred_seq_len = np.ones(1) * OUTPUT_SHAPE[1]
        # 模型預測
        pred_feed = {inputs: pred_inputs,seq_len: pred_seq_len}
        dd, log_probs = sess.run([decoded[0], log_prob], pred_feed)
        # 識別結果轉換
        detected_list = decode_sparse_tensor(dd)[0]
        detected_text = ''
        for d in detected_list:
            detected_text = detected_text + d

return detected_text

 

二、CRNN 方法
CRNN(Convolutional Recurrent Neural Network,卷積循環神經網絡)是目前比較流行的文字識別模型,不須要對樣本數據進行字符分割,可識別任意長度的文本序列,模型速度快、性能好。網絡結構以下圖所示,主要由卷積層、循環層、轉錄層3部分組成,具體技術原理請詳見以前的文章(文章:大話文本識別經典模型 CRNN
 
那麼該如何使用CRNN訓練和識別呢?
github上實現CRNN的代碼有不少,這裏面選擇一個相對簡單的CRNN源代碼進行研究。
(1)下載源代碼
 
首先,在github上下載CRNN源代碼(https://github.com/Belval/CRNN),可直接下載成zip壓縮包或者git克隆

git clone https://github.com/Belval/CRNN.git

(2)準備基礎數據
使用第1節LSTM+CTC介紹的方法隨機生成10000張不定長圖片+椒鹽噪聲做爲基礎數據集,具體詳見第1節的生成基礎數據代碼,在此再也不重複。注意,因爲該CRNN源代碼在讀取圖片時默認文件名第1位爲標籤(如下劃線 」_」 隔開),因而注意按照文件命名規則生成圖片。

(3)訓練模型
參考CRNN/run.py裏面的代碼,編寫模型訓練的調用代碼以下:

# 模型訓練
def train():

# 設置基本屬性
    batch_size=32    # 批量大小
    max_image_width=400   # 最大圖片寬度
    train_test_ratio=0.75    # 訓練集、測試集劃分比例
    restore=True    # 是否恢復加載模型,可用於屢次加載訓練
    iteration_count=1000    # 迭代次數
    # 初始化調用CRNN
    crnn = CRNN(
        batch_size,
        model_dir,
        data_dir,
        max_image_width,
        train_test_ratio,
        restore
    )
    # 模型訓練
    crnn.train(iteration_count)

通過了5個小時左右,迭代訓練了263次,使得loss(損失值)已下降至接近1,模型也已基本上可用。
 
CRNN的訓練過程很長,本案例隨機生成的文字仍是比較簡單的,但每步的迭代就已耗時很長。若是是實際應用中,須要使用背景更加複雜、文字形態更加多樣的數據集,對訓練loss的要求也更高,這時會使得整個訓練過程更長。所以,通常會採用「遷移學習」的方式來提高訓練效率和模型效果(詳見文章:瞭解什麼是「遷移學習」),「遷移學習」的實現方式後面會再單獨進行介紹。

(4)模型測試
參考CRNN/run.py裏面的代碼,編寫模型測試的代碼,可輸出測試結果,代碼以下:

# 模型測試
def test():

# 設置基本屬性
    batch_size=32
    max_image_width=400
    restore=True
    # 初始化CRNN
    crnn = CRNN(
        batch_size,
        model_dir,
        data_dir,
        max_image_width,
        0,
        restore
    )
    # 測試模型
    crnn.test()

測試的結果以下,程序會批量讀入數據後,輸入原始結果(第一行)和預測結果(第二行),便於比較二者是否一致。
 
做者提供的這種測試方式太考驗人眼了,咱們可將CRNN裏面的test函數進行個小修改,自動計算準確率,將會方便不少。修改的代碼以下:

def test(self):
with self.__session.as_default():
    print('Testing')
    for batch_y, _, batch_x in self.__data_manager.test_batches:
        decoded = self.__session.run(
            self.__decoded,
            feed_dict={
                self.__inputs: batch_x,
                self.__seq_len: [self.__max_char_count] * self.__data_manager.batch_size
            }
        )

        # 修改,統計準確率
        true_cnt = 0
        for i, y in enumerate(batch_y):
            if batch_y[i] == ground_truth_to_word(decoded[i]):
                true_cnt = true_cnt + 1
            else:                  
                # 預測結果不一致的,才顯示出來
                print('target:',batch_y[i])
                print('predict:',ground_truth_to_word(decoded[i]))
        print('acc:',float(true_cnt)/float(len(batch_y)))
return None

(5)能力封裝
爲了方便將CRNN識別能力提供給其它程序調用,在CRNN/crnn.py代碼的基礎上進行修改,對CRNN識別能力進行封裝,即只需輸入指定的圖片,便可返回識別結果。
首先是重寫crnn.py裏面加載CRNN網絡結構的方式,因爲原先的代碼在初始化時只支持批量的圖片進行訓練和測試,爲了實現對指定的某張圖片進行識別,對網絡模型的初始化和調用方式進行修改,核心代碼以下:

# CRNN 網絡結構
def crnn_network(max_width, batch_size):
    # 雙向RNN
    def BidirectionnalRNN(inputs, seq_len):
        # rnn-1
        with tf.variable_scope(None, default_name="bidirectional-rnn-1"):
            # Forward
            lstm_fw_cell_1 = rnn.BasicLSTMCell(256)
            # Backward
            lstm_bw_cell_1 = rnn.BasicLSTMCell(256)
            inter_output, _ = tf.nn.bidirectional_dynamic_rnn(lstm_fw_cell_1, lstm_bw_cell_1, inputs, seq_len, dtype=tf.float32)
            inter_output = tf.concat(inter_output, 2)
        # rnn-2
        with tf.variable_scope(None, default_name="bidirectional-rnn-2"):
            # Forward
            lstm_fw_cell_2 = rnn.BasicLSTMCell(256)
            # Backward
            lstm_bw_cell_2 = rnn.BasicLSTMCell(256)
            outputs, _ = tf.nn.bidirectional_dynamic_rnn(lstm_fw_cell_2, lstm_bw_cell_2, inter_output, seq_len, dtype=tf.float32)
            outputs = tf.concat(outputs, 2)
        return outputs
    # CNN,用於提取特徵
    def CNN(inputs):
        # 64 / 3 x 3 / 1 / 1
        conv1 = tf.layers.conv2d(inputs=inputs, filters = 64, kernel_size = (3, 3), padding = "same", activation=tf.nn.relu)
        # 2 x 2 / 1
        pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)
        # 128 / 3 x 3 / 1 / 1
        conv2 = tf.layers.conv2d(inputs=pool1, filters = 128, kernel_size = (3, 3), padding = "same", activation=tf.nn.relu)
        # 2 x 2 / 1
        pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)
        # 256 / 3 x 3 / 1 / 1
        conv3 = tf.layers.conv2d(inputs=pool2, filters = 256, kernel_size = (3, 3), padding = "same", activation=tf.nn.relu)
        # Batch normalization layer
        bnorm1 = tf.layers.batch_normalization(conv3)
        # 256 / 3 x 3 / 1 / 1
        conv4 = tf.layers.conv2d(inputs=bnorm1, filters = 256, kernel_size = (3, 3), padding = "same", activation=tf.nn.relu)
        # 1 x 2 / 1
        pool3 = tf.layers.max_pooling2d(inputs=conv4, pool_size=[2, 2], strides=[1, 2], padding="same")
        # 512 / 3 x 3 / 1 / 1
        conv5 = tf.layers.conv2d(inputs=pool3, filters = 512, kernel_size = (3, 3), padding = "same", activation=tf.nn.relu)
        # Batch normalization layer
        bnorm2 = tf.layers.batch_normalization(conv5)
        # 512 / 3 x 3 / 1 / 1
        conv6 = tf.layers.conv2d(inputs=bnorm2, filters = 512, kernel_size = (3, 3), padding = "same", activation=tf.nn.relu)
        # 1 x 2 / 2
        pool4 = tf.layers.max_pooling2d(inputs=conv6, pool_size=[2, 2], strides=[1, 2], padding="same")
        # 512 / 2 x 2 / 1 / 0
        conv7 = tf.layers.conv2d(inputs=pool4, filters = 512, kernel_size = (2, 2), padding = "valid", activation=tf.nn.relu)
        return conv7

# 定義輸入、輸出、序列長度
    inputs = tf.placeholder(tf.float32, [batch_size, max_width, 32, 1])
    targets = tf.sparse_placeholder(tf.int32, name='targets')
    seq_len = tf.placeholder(tf.int32, [None], name='seq_len')

# 卷積層提取特徵
    cnn_output = CNN(inputs)
    reshaped_cnn_output = tf.reshape(cnn_output, [batch_size, -1, 512])
    max_char_count = reshaped_cnn_output.get_shape().as_list()[1]

# 循環層處理序列
    crnn_model = BidirectionnalRNN(reshaped_cnn_output, seq_len)
    logits = tf.reshape(crnn_model, [-1, 512])

# 轉錄層預測結果
    W = tf.Variable(tf.truncated_normal([512, config.NUM_CLASSES], stddev=0.1), name="W")
    b = tf.Variable(tf.constant(0., shape=[config.NUM_CLASSES]), name="b")
    logits = tf.matmul(logits, W) + b
    logits = tf.reshape(logits, [batch_size, -1, config.NUM_CLASSES])
    logits = tf.transpose(logits, (1, 0, 2))

# 定義損失函數、優化器
    loss = tf.nn.ctc_loss(targets, logits, seq_len)
    cost = tf.reduce_mean(loss)
    optimizer = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost)
    decoded, log_prob = tf.nn.ctc_beam_search_decoder(logits, seq_len, merge_repeated=False)
    dense_decoded = tf.sparse_tensor_to_dense(decoded[0], default_value=-1)
    acc = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32), targets))

# 初始化
    init = tf.global_variables_initializer()

return inputs, targets, seq_len, logits, dense_decoded, optimizer, acc, cost, max_char_count, init

# CRNN 識別文字
# 輸入:圖片路徑
# 輸出:識別文字結果
def predict(img_path):
    # 定義模型路徑、最長圖片寬度
    batch_size = 1
    model_path = '/tmp/crnn_model/'
    max_image_width = 400

# 建立會話
    __session = tf.Session()
    with __session.as_default():
        (
            __inputs,
            __targets,
            __seq_len,
            __logits,
            __decoded,
            __optimizer,
            __acc,
            __cost,
            __max_char_count,
            __init
        ) = crnn_network(max_image_width, batch_size)
        __init.run()

# 加載模型
    with __session.as_default():
        __saver = tf.train.Saver()
        ckpt = tf.train.latest_checkpoint(model_path)
        if ckpt:
            __saver.restore(__session, ckpt)

# 讀取圖片做爲輸入
    arr, initial_len = utils.resize_image(img_path,max_image_width)
    batch_x = np.reshape(
        np.array(arr),
        (-1, max_image_width, 32, 1)
    )

# 利用模型識別文字
    with __session.as_default():
        decoded = __session.run(
            __decoded,
            feed_dict={
                __inputs: batch_x,
                __seq_len: [__max_char_count] * batch_size
            }
        )
        pred_result = utils.ground_truth_to_word(decoded[0])

return pred_result

將CRNN能力封裝後,便能很方便地進行調用識別,以下:

img_path = '/tmp/crnn_data/728591_532.jpg'
pred_result = predict(img_path)
print('predict result:',pred_result)

調用結果以下圖

 

三、chineseocr項目
最後再介紹github上一個很不錯的文字識別項目chineseocr,這個項目是基於yolo3(用於文字檢測)、crnn(用於文字識別)的天然場景文字識別項目。該項目支持darknet / opencv dnn / keras 的文字檢測,支持0、90、180、270度的方向檢測,支持不定長的英文、中英文識別,同時支持通用OCR、身份證識別、火車票識別等多種場景。
該模型功能完善,使用簡單,入手容易,很是適合於新手或者比較通用的場景使用。下面介紹如何使用chineseocr項目。

(1)下載源代碼
 
首先,在github上下載chineseocr源代碼(https://github.com/chineseocr/chineseocr),可直接下載成zip壓縮包或者git克隆

git clone https://github.com/chineseocr/chineseocr.git

(2)下載darknet
 
chineseocr項目默認使用keras yolo3進行文字檢測,該項目同時支持opencv dnn、darknet進行文字檢測。
① 下載源代碼
若是要使用darknet來進行文字檢測,那麼就須要再下載darknet源代碼(如直接使用項目默認的keras yolo3檢測方法,則跳過該步驟),在github上下載chineseocr源代碼(https://github.com/pjreddie/darknet),可直接下載成zip壓縮包或者git克隆

git clone https://github.com/pjreddie/darknet.git

② 放置目錄
下載後,將darknet的源代碼放到chineseocr項目中的darknet目錄中。

mv darknet chineseocr/

③ 編譯
而後修改Makefile,增長對GPU、cudnn的支持

#GPU=1
#CUDNN=1
#OPENCV=0
#OPENMP=0

執行 make 進行編譯

④ 指定libdarknet.so路徑
修改 darknet/python/darknet.py 的第48行,指定libdarknet.so所在的目錄

lib = CDLL(root+"chineseocr/darknet/libdarknet.so", RTLD_GLOBAL)

其中root表示chineseocr所在的路徑

(3)準備基礎環境
在源代碼文件中的setup.md中列舉了該項目依賴的基礎環境,若是是在cpu上運行則查看setup-cpu.md文件。
① 建立虛擬環境

# 建立虛擬環境
conda create -n chineseocr python=3.6 pip scipy numpy jupyter ipython
# 激活虛擬環境
source activate chineseocr


② 安裝依賴包

git submodule init && git submodule update
pip install easydict opencv-contrib-python==4.0.0.21 Cython h5py lmdb mahotas pandas requests bs4 matplotlib lxml
pip install -U pillow
pip install keras==2.1.5 tensorflow==1.8 tensorflow-gpu==1.8
pip install web.py==0.40.dev0
conda install pytorch torchvision -c pytorch
pip install torch torchvision

(4)下載模型文件
在百度網盤上面下載預訓練好的模型文件,並將全部文件複製到models目錄中,下載地址爲 https://pan.baidu.com/s/1gTW9gwJR6hlwTuyB6nCkzQ

(5)啓動web服務
經過執行app.py啓動web服務,啓動後便能直接上傳圖片進行文字識別,執行命令爲

ipython app.py 8080

其中,8080爲端口號,可根據實際須要進行修改。

 


啓動後的界面以下,界面中提供了是否進行文字方向檢測、是否做單行文字識別,以及通用OCR(默認)、火車票、身份證的識別類型。
 
在chineseocr項目中的test目錄裏面自帶了一些測試圖片,經過上傳一些圖片測試識別效果,以下圖:
 
從識別效果上看還不錯,接下來試一下火車票、身份證類型的識別
 
從上圖可看出,對火車票的識別結果進行了處理,將出發地點、到達地點、車次、時間、價格、姓名等信息提取了出來。
 
身份證的識別也是將姓名、性別、民族、出生年月、身份證號、住址這些信息提取了出來。
咱們再比較一下,有使用文字方向檢測和沒有使用文字方向檢測時的識別效果區別,以下圖:
 
從識別的結果能夠看出,對於一張顛倒的圖片(或者具備必定的旋轉角度),若是沒有加上文字方向檢測,則識別出來的結果文字會出現很大的誤差,而加上方向檢測後則會正確地識別出來。

(6)識別能力封裝
chineseocr項目支持多種方式的文字檢測與識別,提供了多種模型可供選擇,致使整個項目比較龐大。若是要將該項目的檢測與識別能力抽離出來,提供給其它項目使用,則需根據實際業務場景進行簡化,將識別能力進行封裝。
例如咱們選擇keras yolo3進行文字檢測,選擇pytorch進行文字識別,去掉文字方向檢測(假定輸入的圖片絕大多數是方向正確的),那麼便可對chineseocr的源代碼進行大幅精簡。在model.py代碼的基礎上進行修改,去繁存簡,對識別能力進行封裝,方便提供給其它應用程序使用。修改後的核心代碼以下:

# 文字檢測
def text_detect(img,MAX_HORIZONTAL_GAP=30,MIN_V_OVERLAPS=0.6,MIN_SIZE_SIM=0.6,TEXT_PROPOSALS_MIN_SCORE=0.7,TEXT_PROPOSALS_NMS_THRESH=0.3,TEXT_LINE_NMS_THRESH=0.3,):
    boxes, scores = detect.text_detect(np.array(img))
    boxes = np.array(boxes, dtype=np.float32)
    scores = np.array(scores, dtype=np.float32)
    textdetector = TextDetector(MAX_HORIZONTAL_GAP, MIN_V_OVERLAPS, MIN_SIZE_SIM)
    shape = img.shape[:2]
    boxes = textdetector.detect(boxes,scores[:, np.newaxis],shape,TEXT_PROPOSALS_MIN_SCORE,TEXT_PROPOSALS_NMS_THRESH,TEXT_LINE_NMS_THRESH,)
    text_recs = get_boxes(boxes)
    newBox = []
    rx = 1
    ry = 1
    for box in text_recs:
        x1, y1 = (box[0], box[1])
        x2, y2 = (box[2], box[3])
        x3, y3 = (box[6], box[7])
        x4, y4 = (box[4], box[5])
        newBox.append([x1 * rx, y1 * ry, x2 * rx, y2 * ry, x3 * rx, y3 * ry, x4 * rx, y4 * ry])
    return newBox

# 文字識別
def crnnRec(im, boxes, leftAdjust=False, rightAdjust=False, alph=0.2, f=1.0):
    results = []
    im = Image.fromarray(im)
    for index, box in enumerate(boxes):
        degree, w, h, cx, cy = solve(box)
        partImg, newW, newH = rotate_cut_img(im, degree, box, w, h, leftAdjust, rightAdjust, alph)
        text = crnnOcr(partImg.convert('L'))
        if text.strip() != u'':
            results.append({'cx': cx * f, 'cy': cy * f, 'text': text, 'w': newW * f, 'h': newH * f,
                            'degree': degree * 180.0 / np.pi})
    return results

# 文字檢測、文字識別的能力封裝
def ocr_model(img, leftAdjust=True, rightAdjust=True, alph=0.02):
    img, f = letterbox_image(Image.fromarray(img), IMGSIZE)
    img = np.array(img)
    config = dict(MAX_HORIZONTAL_GAP=50,  ##字符之間的最大間隔,用於文本行的合併
                  MIN_V_OVERLAPS=0.6,
                  MIN_SIZE_SIM=0.6,
                  TEXT_PROPOSALS_MIN_SCORE=0.1,
                  TEXT_PROPOSALS_NMS_THRESH=0.3,
                  TEXT_LINE_NMS_THRESH=0.7,  ##文本行之間測iou值
                  )
    config['img'] = img
    text_recs = text_detect(**config)  ##文字檢測
    newBox = sort_box(text_recs)  ##行文本識別
    result = crnnRec(np.array(img), newBox, leftAdjust, rightAdjust, alph, 1.0 / f)
    return result

通過以上從新改造封裝後,只須要調用ocr_model函數,輸入圖片,便可調用chineseocr項目的檢測與識別能力。調用結果以下圖:

以上介紹的就是LSTM+CTC、CRNN、chineseocr三種文字識別方法的實戰操做,在實際生產中通常會根據業務場景,對識別方法進行改造或增長預處理、後處理環節。若是有興趣瞭解的,可私信我再進行交流。

 

歡迎關注本人的微信公衆號「大數據與人工智能Lab」(BigdataAILab),獲取 完整源代碼

 

推薦相關閱讀

一、AI 實戰系列

二、大話深度學習系列

三、圖解 AI 系列

四、AI 雜談

五、大數據超詳細系列

相關文章
相關標籤/搜索