當前位置: 妍妍網 > 碼農

用Python寫個自動批改作業系統!

2024-04-18碼農

來源丨金塊

https://juejin.cn/user/615370768790158

一、亮出效果

最近一些軟體的搜題、智慧批改類的功能要下線。

退1024步講,要不要自己做一個自動批改的功能啊?萬一哪天孩子要用呢!

昨晚我做了一個夢,夢見我實作了這個功能,如下圖所示:

功能簡介 :作對了,能打對號;做錯了,能打叉號;沒做的,能補上答案。

醒來後,我環顧四周,趕緊再躺下,希望夢還能接上。

二、實作步驟

基本思路

其實,搞定兩點就成,第一是能辨識數位,第二是能切分數位。

首先得能認識5是5,這是前提條件,其次是能找到5、6、7、8這些數位區域的位置。

前者是 影像辨識 ,後者是 影像切割

  • 對於影像辨識,一般的套路是下面這樣的(CNN摺積神經網路):

  • 對於影像切割,一般的套路是下面的這樣(橫向縱向投影法):

  • 既然思路能走得通,那麽咱們先搞影像辨識。 準備數據->訓練數據並保存模型->使用訓練模型預測結果

    2.1 準備數據

    對於男友,找一個油嘴滑舌的花花公子,不如找一個悶葫蘆IT男,親手把他培養成你期望的樣子。

    咱們不用什麽官方的mnist數據集,因為那是官方的,不是你的,你想要添加±×÷它也沒有。

    有些通用的數據集,雖然很強大,很方便,但是一旦放到你的場景中,效果一點也不如你的願。

    只有訓練自己手裏的數據,然後自己用起來才順手。更重要的是,我們享受創造的過程。

    假設,我們只給口算做辨識,那麽我們需要的圖片數據有如下幾類:

    索引:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
    字元:0 1 2 3 4 5 6 7 8 9 = + - × ÷

    如果能辨識這些,基本上能滿足整數的加減乘除運算了。

    好了,圖片哪裏來?!

    是啊,圖片哪裏來?

    嚇得我差點從夢裏醒來,500萬都規劃好該怎麽花了,居然雙色球還沒有選號!

    夢裏,一個老者跟我說,圖片要自己生成。我問他如何生成,他呵呵一笑,消失在迷霧中……

    仔細一想,其實也不難,打字我們總會吧,生成數位無非就是用程式碼把字寫在圖片上。

    字之所以能展示,主要是因為有字型的支撐。

    如果你用的是windows系統,那麽開啟KaTeX parse error: Undefined control sequence: \Windows at position 3: C:\̲W̲i̲n̲d̲o̲w̲s̲\Fonts這個資料夾,你會發現好多字型。

    我們寫程式碼呼叫這些字型,然後把它打印到一張圖片上,是不是就有數據了。

    而且這些數據完全是由我們控制的,想多就多,想少就少,想數位、字母、漢字、符號都可以,今天你搞出來數位辨識,也就相當於你同時擁有了所有辨識!想想還有點小激動呢!

    看看,這就是打工和創業的區別。你用別人的數據相當於打工,你是不用操心,但是他給你什麽你才有什麽。自己造數據就相當於創業,雖然前期辛苦,你可以完全自己把握節奏,需要就加上,沒用就去掉。

    2.1.1 準備字型

    建一個fonts資料夾,從字型柯瑞拷一部份字型放進來,我這裏是拷貝了13種字型檔。

    好的,準備工作做好了,肯定很累吧,休息休息休息,一會兒再搞!

    2.1.2 生成圖片

    程式碼如下,可以直接執行。

    from __future__ import print_function
    from PIL import Image
    from PIL import ImageFont
    from PIL import ImageDraw
    import os
    import shutil
    import time
    # %% 要生成的文本
    label_dict = {0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '=', 11: '+', 12: '-', 13: '×', 14: '÷'}
    # 文本對應的資料夾,給每一個分類建一個檔
    for value,char in label_dict.items():
    train_images_dir = "dataset"+"/"+str(value)
    if os.path.isdir(train_images_dir):
    shutil.rmtree(train_images_dir)
    os.makedirs(train_images_dir)
    # %% 生成圖片
    def makeImage(label_dict, font_path, width=24, height=24, rotate = 0):
    # 從字典中取出鍵值對
    for value,char in label_dict.items():
    # 建立一個黑色背景的圖片,大小是24*24
    img = Image.new("RGB", (width, height), "black"
    draw = ImageDraw.Draw(img)
    # 載入一種字型,字型大小是圖片寬度的90%
    font = ImageFont.truetype(font_path, int(width*0.9))
    # 獲取字型的寬高
    font_width, font_height = draw.textsize(char, font)
    # 計算字型繪制的x,y座標,主要是讓文字畫在圖示中心
    x = (width - font_width-font.getoffset(char)[0]) / 2
    y = (height - font_height-font.getoffset(char)[1]) / 2
    # 繪制圖片,在那裏畫,畫啥,什麽顏色,什麽字型
    draw.text((x,y), char, (255, 255, 255), font)
    # 設定圖片傾斜角度
    img = img.rotate(rotate)
    # 命名檔保存,命名規則:dataset/編號/img-編號_r-選擇角度_時間戳.png
    time_value = int(round(time.time() * 1000))
    img_path = "dataset/{}/img-{}_r-{}_{}.png".format(value,value,rotate,time_value)
    img.save(img_path)
    # %% 存放字型的路徑
    font_dir = "./fonts"
    for font_name in os.listdir(font_dir):
    # 把每種字型都取出來,每種字型都生成一批圖片
    path_font_file = os.path.join(font_dir, font_name)
    # 傾斜角度從-10到10度,每個角度都生成一批圖片
    for k in range(-10, 10, 1): 
    # 每個字元都生成圖片
    makeImage(label_dict, path_font_file, rotate = k)



    上面純程式碼不到30行,相信大家應該能看懂!看不懂不是我的讀者。

    核心程式碼就是畫文字。

    draw.text((x,y), char, (255, 255, 255), font)

    轉譯一下就是:使用某字型在黑底圖片的(x,y)位置寫白色的char符號。

    核心邏輯就是三層迴圈。

    如果程式碼你執行的沒有問題,最終會生成如下結果:

    好了,數據準備好了。總共15個資料夾,每個資料夾下對應的各種字型各種傾斜角的字元圖片3900個(字元15類×字型13種×角度20個),圖片的大小是 24×24 像素。

    有了 數據 ,我們就可以再進行下一步了,下一步是 訓練 使用 數據。

    2.2 訓練數據

    2.2.1 構建模型

    你先看程式碼,外行感覺好深奧,內行偷偷地笑。

    # %% 匯入必要的包 
    import tensorflow as tf
    import numpy as np
    from tensorflow.keras import layers
    from tensorflow.keras.models import Sequential
    import pathlib
    import cv2
    # %% 構建模型
    def create_model():
    model = Sequential([
    layers.experimental.preprocessing.Rescaling(1./255, input_shape=(24, 24, 1)),
    layers.Conv2D(24,3,activation='relu'),
    layers.MaxPooling2D((2,2)),
    layers.Conv2D(64,3, activation='relu'),
    layers.MaxPooling2D((2,2)),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dense(15)]
    )
    model.compile(optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'])
    return model

    這個模型的序列是下面這樣的,作用是輸入一個圖片數據,經過各個層揉搓,最終預測出這個圖片屬於哪個分類。

    這麽多層都是幹什麽的,有什麽用?和衣服一樣,肯定是有用的,內衣、襯衣、毛衣、棉衣各有各的用處。

    2.2.2 摺積層 Conv2D

    各個職能部門的調查員,搜集和整理某單位區域內的特定數據。我們輸入的是一個影像,它是由像素組成的,這就是R e s c a l i n g ( 1. / 255 , i n p u t s h a p e = ( 24 , 24 , 1 ) ) Rescaling(1./255, input_shape=(24, 24, 1))Rescaling(1./255,input shape=(24,24,1))中,input_shape輸入形狀是24*24像素1個通道(彩色是RGB 3個通道)的影像。

    摺積層程式碼中的定義是Conv2D(24,3),意思是用3*3像素的摺積核,去提取24個特征。

    我把圖轉到地圖上來,你就能理解了。以我大濟南的市中區為例子。

    摺積的作用就相當於從地圖的某級單位區域中收集多組特定資訊。比如以小區為單位去提取住宅數量、車位數量、學校數量、人口數、年收入、學歷、年齡等等24個維度的資訊。小區相當於摺積核。

    提取完成之後是這樣的。

    第一次摺積之後,我們從市中區得到N個小區的數據。

    摺積是可以進行多次的。

    比如在小區摺積之後,我們還可在小區的基礎上再來一次摺積,在摺積就是街道了。

    透過再次以街道為單位摺積小區,我們就從市中區得到了N個街道的數據。

    這就是摺積的作用。

    透過一次次摺積,就把一張大圖,透過特定的方法卷起來,最終留下來的是固定幾組有目的數據,以此方便後續的評選決策。這是評選一個區的數據,要是評選濟南市,甚至山東省,也是這麽摺積。這和現實生活中評選文明城市、經濟強省也是一個道理。

    2.2.3 池化層 MaxPooling2D

    說白了就是四舍五入。

    電腦的計算能力是強大的,比你我快,但也不是不用考慮成本。我們當然希望它越快越好,如果一個方法能省一半的時間,我們肯定願意用這種方法。

    池化層幹的就是這個事情。池化的程式碼定義是這樣的M a x P o o l i n g 2 D ( ( 2 , 2 ) ) MaxPooling2D((2,2))MaxPooling2D((2,2)),這裏是最大值池化。其中(2,2)是池化層的大小,其實就是在2*2的區域內,我們認為這一片可以合成一個單位。

    再以地圖舉個例子,比如下面的16個格子裏的數據,是16個街道的學校數量。

    為了進一步提高計算效率,少計算一些數據,我們用2*2的池化層進行池化。

    池化的方格是4個街道合成1個,新單位學校數量取成員中學校數量最大(也有取最小,取平均多種池化)的那一個。池化之後,16個格子就變為了4個格子,從而減少了數據。

    這就是池化層的作用。

    2.2.4 全連線層 Dense

    弱水三千,只取一瓢。

    在這裏,它其實是一個分類器。

    我們構建它時,程式碼是這樣的D e n s e ( 15 ) Dense(15)Dense(15)。

    它所做的事情,不管你前面是怎麽樣,有多少維度,到我這裏我要強行轉化為固定的通道。

    比如辨識字母a~z,我有500個神經元參與判斷,但是最終輸出結果就是26個通道(a,b,c,……,y,z)。

    我們這裏總共有15類字元,所以是15個通道。給定一個輸入後,輸出為每個分類的機率。

    註意:上面都是二維的輸入,比如24×24,但是全連線層是一維的,所以程式碼中使用了l a y e r s . F l a t t e n ( ) layers.Flatten()layers.Flatten()將二維數據拉平為一維數據([[11,12],[21,22]]->[11,12,21,22])。

    對於總體的模型,呼叫m o d e l . s u m m a r y ( ) model.summary()model.summary()打印序列的網路結構如下:

    _________________________________________________________________
    Layer (type) Output Shape Param #
    =================================================================
    rescaling_2 (Rescaling) (None, 24, 24, 1) 0
    _________________________________________________________________
    conv2d_4 (Conv2D) (None, 22, 22, 24) 240
    _________________________________________________________________
    max_pooling2d_4 (MaxPooling2 (None, 11, 11, 24) 0
    _________________________________________________________________
    conv2d_5 (Conv2D) (None, 9, 9, 64) 13888
    _________________________________________________________________
    max_pooling2d_5 (MaxPooling2 (None, 4, 4, 64) 0
    _________________________________________________________________
    flatten_2 (Flatten) (None, 1024) 0
    _________________________________________________________________
    dense_4 (Dense) (None, 128) 131200
    _________________________________________________________________
    dense_5 (Dense) (None, 15) 1935
    =================================================================
    Total params: 147,263
    Trainable params: 147,263
    Non-trainable params: 0
    _________________________________________________________________

    我們看到conv2d_5 (Conv2D) (None, 9, 9, 64) 經過2*2的池化之後變為max_pooling2d_5 (MaxPooling2 (None, 4, 4, 64)。(None, 4, 4, 64) 再經過F l a t t e n FlattenFlatten拉成一維之後變為(None, 1024),經過全連線變為(None, 128)再一次全連線變為(None, 15),15就是我們的最終分類。這一切都是我們設計的。

    m o d e l . c o m p i l e model.compilemodel.compile就是配置模型的幾個參數,這個現階段記住就可以。

    2.2.5 訓練數據

    執行就完了。

    # 統計資料夾下的所有圖片數量
    data_dir = pathlib.Path('dataset')
    # 從資料夾下讀取圖片,生成數據集
    train_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir, # 從哪個檔獲取數據
    color_mode="grayscale"# 獲取數據的顏色為灰度
    image_size=(24, 24), # 圖片的大小尺寸
    batch_size=32 # 多少個圖片為一個批次
    )
    # 數據集的分類,對應dataset資料夾下有多少圖片分類
    class_names = train_ds. class_names
    # 保存數據集分類
    np.save(" class_name.npy", class_names)
    # 數據集緩存處理
    AUTOTUNE = tf.data.experimental.AUTOTUNE
    train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
    # 建立模型
    model = create_model()
    # 訓練模型,epochs=10,所有數據集訓練10遍
    model.fit(train_ds,epochs=10)
    # 保存訓練後的權重
    model.save_weights('checkpoint/char_checkpoint')

    執行之後會輸出如下資訊:

    Found 3900 files belonging to 15 classes. 
    Epoch 1/10 122/122 [=========] - 2s 19ms/step - loss: 0.5795 - accuracy: 0.8615 
    Epoch 2/10 122/122 [=========] - 2s 18ms/step - loss: 0.0100 - accuracy: 0.9992 
    Epoch 3/10 122/122 [=========] - 2s 19ms/step - loss: 0.0027 - accuracy: 1.0000 
    Epoch 4/10 122/122 [=========] - 2s 19ms/step - loss: 0.0013 - accuracy: 1.0000 
    Epoch 5/10 122/122 [=========] - 2s 20ms/step - loss: 8.4216e-04 - accuracy: 1.0000 
    Epoch 6/10 122/122 [=========] - 2s 18ms/step - loss: 5.5273e-04 - accuracy: 1.0000 
    Epoch 7/10 122/122 [=========] - 3s 21ms/step - loss: 4.0966e-04 - accuracy: 1.0000 
    Epoch 8/10 122/122 [=========] - 2s 20ms/step - loss: 3.0308e-04 - accuracy: 1.0000 
    Epoch 9/10 122/122 [=========] - 3s 23ms/step - loss: 2.3446e-04 - accuracy: 1.0000 
    Epoch 10/10 122/122 [=========] - 3s 21ms/step - loss: 1.8971e-04 - accuracy: 1.0000

    我們看到,第3遍時候,準確率達到100%了。最後結束的時候,我們發現資料夾checkpoint下多了幾個檔:

    char_checkpoint.data-00000-of-00001
    char_checkpoint.index
    checkpoint

    上面那幾個檔是訓練結果,訓練保存之後就不用動了。後面可以直接用這些數據進行預測。

    2.3 預測數據

    終於到了享受成果的時候了。

    # 設定待辨識的圖片
    img1=cv2.imread('img1.png',0) 
    img2=cv2.imread('img2.png',0) 
    imgs = np.array([img1,img2])
    # 構建模型
    model = create_model()
    # 載入前期訓練好的權重
    model.load_weights('checkpoint/char_checkpoint')
    # 讀出圖片分類
    class_name = np.load(' class_name.npy')
    # 預測圖片,獲取預測值
    predicts = model.predict(imgs) 
    results = [] # 保存結果的陣列
    for predict in predicts: #遍歷每一個預測結果
    index = np.argmax(predict) # 尋找最大值
    result = class_name[index] # 取出字元
    results.append(result)
    print(results)

    我們找兩張圖片img1.png,img2.png,一張是數位6,一張是數位8,兩張圖放到程式碼同級目錄下,驗證一下辨識效果如何。

    圖片要透過cv2.imread('img1.png',0) 轉化為二維陣列結構,0參數是灰度圖片。經過處理後,圖片轉成的陣列是如下所示(24,24)的結構:

    我們要同時驗證兩張圖,所以把兩張圖再組成imgs放到一起,imgs的結構是(2,24,24)。

    下面是構建模型,然後載入權重。透過呼叫predicts = model.predict(imgs)將imgs傳遞給模型進行預測得出predicts。

    predicts的結構是(2,15),數值如下面所示:

    [[ 16.134243 -12.10675 -1.1994154 -27.766754 -43.4324 -9.633694 -12.214878 1.6287893 2.562174 3.2222707 13.834648 28.254173 -6.102874 16.76582 7.2586184] [ 5.022571 -8.762314 -6.7466817 -23.494259 -30.170597 2.4392672 -14.676962 5.8255725 8.855118 -2.0998626 6.820853 7.6578817 1.5132296 24.4664 2.4192357]]

    意思是有2個預測結果,每一個圖片的預測結果有15種可能。

    然後根據 index = np.argmax(predict) 找出最大可能的索引。

    根據索引找到字元的數值結果是[‘6’, ‘8’]。

    下面是數據在記憶體中的監控:

    可見,我們的預測是準確的。

    下面,我們將要把圖片中數位切割出來,進行辨識了。

    之前我們準備了數據,訓練了數據,並且拿圖片進行了辨識,辨識結果正確。

    到目前為止,看來問題不大……沒有大問題,有問題也大不了。

    下面就是把圖片進行切割辨識了。

    下面這張大圖片,怎麽把它搞一搞,搞成單個小數位的圖片。

    2.4 切割影像

    上帝說要有光,就有了光。

    於是,當光投過來時,物體的背後就有了影。

    我們就知道了,有影的地方就有東西,沒影的地方是空白。

    這就是投影。

    這個簡單的道理放在影像切割上也很實用。

    我們把文字的像素做個投影,這樣我們就知道某個區間有沒有文字,並且知道這個區間文字是否集中。

    下面是示意圖:

    2.4.1 投影大法

    最有效的方法,往往都是用迴圈實作的。

    要計算投影,就得一個像素一個像素地數,檢視有幾個像素,然後記錄下這一行有N個像素點。如此迴圈。

    首先匯入包:

    import numpy as np
    import cv2
    from PIL import Image, ImageDraw, ImageFont
    import PIL
    import matplotlib.pyplot as plt
    import os
    import shutil
    from numpy.core.records import array
    from numpy.core.shape_base import block
    import time

    比如說要看垂直方向的投影,程式碼如下:

    # 整幅圖片的Y軸投影,傳入圖片陣列,圖片經過二值化並反色
    def img_y_shadow(img_b):
    ### 計算投影 ###
    (h,w)=img_b.shape
    # 初始化一個跟影像高一樣長度的陣列,用於記錄每一行的黑點個數
    a=[0 for z in range(0,h)]
    # 遍歷每一列,記錄下這一列包含多少有效像素點
    for i in range(0,h):
    for j in range(0,w):
    if img_b[i,j]==255:
    a[i]+=1
    return a

    最終得到是這樣的結構:[0, 79, 67, 50, 50, 50, 109, 137, 145, 136, 125, 117, 123, 124, 134, 71, 62, 68, 104, 102, 83, 14, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, ……38, 44, 56, 106, 97, 83, 0, 0, 0, 0, 0, 0, 0]表示第幾行總共有多少個像素點,第1行是0,表示是空白的白紙,第2行有79個像素點。

    如果我們想要從視覺呈現出來怎麽處理呢?那可以把它立起來拉直畫出來。

    # 展示圖片
    def img_show_array(a):
    plt.imshow(a)
    plt.show()
    # 展示投影圖, 輸入參數arr是圖片的二維陣列,direction是x,y軸
    def show_shadow(arr, direction = 'x'):
    a_max = max(arr)
    if direction == 'x'# x軸方向的投影
    a_shadow = np.zeros((a_max, len(arr)), dtype=int)
    for i in range(0,len(arr)):
    if arr[i] == 0:
    continue
    for j in range(0, arr[i]):
    a_shadow[j][i] = 255
    elif direction == 'y'# y軸方向的投影
    a_shadow = np.zeros((len(arr),a_max), dtype=int)
    for i in range(0,len(arr)):
    if arr[i] == 0:
    continue
    for j in range(0, arr[i]):
    a_shadow[i][j] = 255
    img_show_array(a_shadow)

    我們來試驗一下效果:

    我們將上面的原圖片命名為question.jpg放到程式碼同級目錄。

    # 讀入圖片
    img_path = 'question.jpg'
    img=cv2.imread(img_path,0) 
    thresh = 200 
    # 二值化並且反色
    ret,img_b=cv2.threshold(img,thresh,255,cv2.THRESH_BINARY_INV) 

    二值化並反色後的變化如下所示:

    上面的操作很有作用,透過二值化,過濾掉雜色,透過反色將黑白對調,原來白紙區域都是255,現在黑色都是0,更利於計算。

    計算投影並展示的程式碼:

    img_y_shadow_a = img_y_shadow(img_b)
    show_shadow(img_y_shadow_a, 'y'# 如果要顯示投影

    下面的圖是上面圖在Y軸上的投影

    從視覺上看,基本上能區分出來哪一行是哪一行。

    2.4.2 根據投影找區域

    最有效的方法,往往還得用迴圈來實作。

    上面投影那張圖,你如何計算哪裏到哪裏是一行,雖然肉眼可見,但是電腦需要規則和演算法。

    # 圖片獲取文字塊,傳入投影列表,返回標記的陣列區域座標[[左,上,右,下]]
    def img2rows(a,w,h):
    ### 根據投影切分圖塊 ### 
    inLine = False # 是否已經開始切分
    start = 0 # 某次切分的起始索引
    mark_boxs = []
    for i in range(0,len(a)):
    if inLine == False and a[i] > 10:
    inLine = True
    start = i
    # 記錄這次選中的區域[左,上,右,下],上下就是圖片,左右是start到當前
    elif i-start >5 and a[i] < 10 and inLine:
    inLine = False
    if i-start > 10:
    top = max(start-1, 0)
    bottom = min(h, i+1)
    box = [0, top, w, bottom]
    mark_boxs.append(box) 
    return mark_boxs

    透過投影,計算哪些區域在一定範圍內是連續的,如果連續了很長時間,我們就認為是同一區域,如果斷開了很長一段時間,我們就認為是另一個區域。

    透過這項操作,我們就可以獲得Y軸上某一行的上下兩個邊界點的座標,再結合圖片寬度,其實我們也就知道了一行圖片的四個頂點的座標了mark_boxs存下的是[坐,上,右,下]。

    如果呼叫如下程式碼:

    (img_h,img_w)=img.shape
    row_mark_boxs = img2rows(img_y_shadow_a,img_w,img_h)
    print(row_mark_boxs)

    我們獲取到的是所有辨識出來每行圖片的座標,格式是這樣的:[[0, 26, 596, 52], [0, 76, 596, 103], [0, 130, 596, 155], [0, 178, 596, 207], [0, 233, 596, 259], [0, 282, 596, 311], [0, 335, 596, 363], [0, 390, 596, 415]]

    2.4.3 根據區域切圖片

    最有效的方法,最終也得用迴圈來實作。這也是電腦體現它強大的地方。

    # 裁剪圖片,img 圖片陣列, mark_boxs 區域標記
    def cut_img(img, mark_boxs):
    img_items = [] # 存放裁剪好的圖片
    for i in range(0,len(mark_boxs)):
    img_org = img.copy()
    box = mark_boxs[i]
    # 裁剪圖片
    img_item = img_org[box[1]:box[3], box[0]:box[2]]
    img_items.append(img_item)
    return img_items

    這一步驟是拿著方框,從大圖上用小刀劃下小圖,核心程式碼是img_org[box[1]:box[3], box[0]:box[2]]圖片裁剪,參數是陣列的[上:下,左:右],獲取的數據還是二維的陣列。

    如果保存下來:

    # 保存圖片
    def save_imgs(dir_name, imgs):
    if os.path.exists(dir_name):
    shutil.rmtree(dir_name) 
    if not os.path.exists(dir_name):
    os.makedirs(dir_name)
    img_paths = []
    for i in range(0,len(imgs)):
    file_path = dir_name+'/part_'+str(i)+'.jpg'
    cv2.imwrite(file_path,imgs[i])
    img_paths.append(file_path)
    return img_paths
    # 切圖並保存
    row_imgs = cut_img(img, row_mark_boxs)
    imgs = save_imgs('rows', row_imgs) # 如果要保存切圖
    print(imgs)


    圖片是下面這樣的:

    2.4.4 迴圈可去油膩

    還是迴圈。橫著行我們掌握了,那麽針對每一行圖片,我們豎著切成三塊是不是也會了,一個道理。

    需要註意的是,橫豎是稍微有區別的,下面是上圖的x軸投影。

    橫著的時候,字與字之間本來就是有空隙的,然後塊與塊也有空隙,這個空隙的度需要掌握好,以便更好地區分出來是字的間距還是算式塊的間距。

    幸好,有種方法叫膨脹。

    膨脹對人來說不積極,但是對於技術來說,不管是膨脹(dilate),還是腐蝕(erode),只要能達到目的,都是好的。

    kernel=np.ones((3,3),np.uint8) # 膨脹核大小
    row_img_b=cv2.dilate(img_b,kernel,iterations=6) # 影像膨脹6次

    膨脹之後再投影,就很好地區分出了塊。

    根據投影裁剪之後如下圖所示:

    同理,不膨脹可截取單個字元。

    這樣,這是一塊區域的字元。

    一行的,一頁的,透過迴圈,都可以截取出來。

    有了圖片,就可以辨識了。有了位置,就可以判斷辨識結果的關系了。

    下面提供一些程式碼,這些程式碼不全,有些函式你可能找不到,但是思路可以參考,詳細的程式碼可以去我的github去看。

    def divImg(img_path, save_file = False):
    img_o=cv2.imread(img_path,1) 
    # 讀入圖片
    img=cv2.imread(img_path,0) 
    (img_h,img_w)=img.shape
    thresh = 200
    # 二值化整個圖,用於分行
    ret,img_b=cv2.threshold(img,thresh,255,cv2.THRESH_BINARY_INV) 
    # 計算投影,並截取整個圖片的行
    img_y_shadow_a = img_y_shadow(img_b)
    row_mark_boxs = img2rows(img_y_shadow_a,img_w,img_h)
    # 切行的圖片,切的是原圖
    row_imgs = cut_img(img, row_mark_boxs)
    all_mark_boxs = []
    all_char_imgs = []
    # ===============從行切塊======================
    for i in range(0,len(row_imgs)):
    row_img = row_imgs[i]
    (row_img_h,row_img_w)=row_img.shape
    # 二值化一行的圖,用於切塊
    ret,row_img_b=cv2.threshold(row_img,thresh,255,cv2.THRESH_BINARY_INV)
    kernel=np.ones((3,3),np.uint8)
    #影像膨脹6次
    row_img_b_d=cv2.dilate(row_img_b,kernel,iterations=6)
    img_x_shadow_a = img_x_shadow(row_img_b_d)
    block_mark_boxs = row2blocks(img_x_shadow_a, row_img_w, row_img_h)
    row_char_boxs = []
    row_char_imgs = []
    # 切塊的圖,切的是原圖
    block_imgs = cut_img(row_img, block_mark_boxs)
    if save_file:
    b_imgs = save_imgs('cuts/row_'+str(i), block_imgs) # 如果要保存切圖
    print(b_imgs)
    # =============從塊切字====================
    for j in range(0,len(block_imgs)):
    block_img = block_imgs[j]
    (block_img_h,block_img_w)=block_img.shape
    # 二值化塊,因為要切字元圖片了
    ret,block_img_b=cv2.threshold(block_img,thresh,255,cv2.THRESH_BINARY_INV)
    block_img_x_shadow_a = img_x_shadow(block_img_b)
    row_top = row_mark_boxs[i][1]
    block_left = block_mark_boxs[j][0]
    char_mark_boxs,abs_char_mark_boxs = block2chars(block_img_x_shadow_a, block_img_w, block_img_h,row_top,block_left)
    row_char_boxs.append(abs_char_mark_boxs)
    # 切的是二值化的圖
    char_imgs = cut_img(block_img_b, char_mark_boxs, True)
    row_char_imgs.append(char_imgs)
    if save_file:
    c_imgs = save_imgs('cuts/row_'+str(i)+'/blocks_'+str(j), char_imgs) # 如果要保存切圖
    print(c_imgs)
    all_mark_boxs.append(row_char_boxs)
    all_char_imgs.append(row_char_imgs)

    return all_mark_boxs,all_char_imgs,img_o

    最後返回的值是3個,all_mark_boxs是標記的字元位置的座標集合。[左,上,右,下]是指某個字元在一張大圖裏的座標,打印一下是這樣的:

    [[[[19, 26, 34, 53], [36, 26, 53, 53], [54, 26, 65, 53], [66, 26, 82, 53], [84, 26, 101, 53], [102, 26, 120, 53], [120, 26, 139, 53]], [[213, 26, 229, 53], [231, 26, 248, 53], [249, 26, 268, 53], [268, 26, 285, 53]], [[408, 26, 426, 53], [427, 26, 437, 53], [438, 26, 456, 53], [456, 26, 474, 53], [475, 26, 492, 53]]], [[[20, 76, 36, 102], [38, 76, 48, 102], [50, 76, 66, 102], [67, 76, 85, 102], [85, 76, 104, 102]], [[214, 76, 233, 102], [233, 76, 250, 102], [252, 76, 268, 102], [270, 76, 287, 102]], [[411, 76, 426, 102], [428, 76, 445, 102], [446, 76, 457, 102], [458, 76, 474, 102], [476, 76, 493, 102], [495, 76, 511, 102]]]]

    它是有結構的。它的結構是:

    all_char_imgs這個返回值,裏面是上面座標結構對應位置的圖片。img_o就是原圖了。

    2.5 辨識

    迴圈,迴圈,還是TM迴圈!

    對於辨識,2.3 預測數據已經講過了,那次是對於2張獨立圖片的辨識,現在我們要對整張大圖切分後的小圖集合進行辨識,這就又用到了迴圈。

    翠花,上程式碼!

    all_mark_boxs,all_char_imgs,img_o = divImg(path,save)
    model = cnn.create_model()
    model.load_weights('checkpoint/char_checkpoint')
    class_name = np.load(' class_name.npy')
    # 遍歷行
    for i in range(0,len(all_char_imgs)):
    row_imgs = all_char_imgs[i]
    # 遍歷塊
    for j in range(0,len(row_imgs)):
    block_imgs = row_imgs[j]
    block_imgs = np.array(block_imgs)
    results = cnn.predict(model, block_imgs, class_name)
    print('recognize result:',results)

    上面程式碼做的就是以塊為單位,傳遞給神經網路進行預測,然後返回辨識結果。

    針對這張圖,我們來進行裁剪和辨識。

    看底部的最後一行

    recognize result: ['1''0''12''2''10']
    recognize result: ['8''12''6''10']
    recognize result: ['1''0''12''7''10']

    結果是索引,不是真實的字元,我們根據字典10: '=', 11: '+', 12: '-', 13: '×', 14: '÷'轉換過來之後結果是:

    recognize result: ['1''0''-''2''=']
    recognize result: ['8''-''6''=']
    recognize result: ['1''0''-''7''=']

    和圖片是對應的:

    2.6 計算並反饋

    迴圈……

    我們獲取到了10-2=、8-6=2,也獲取到了他們在原圖的位置座標[左,上,右,下],那麽怎麽把結果反饋到原圖上呢?

    往往到這裏就剩最後一步了。

    再來溫習一遍需求:作對了,能打對號;做錯了,能打叉號;沒做的,能補上答案。

    實作分兩步走:計算(是作對做錯還是沒錯)和反饋(把預期結果寫到原圖上)。

    2.6.1 計算 python有個函式很強大,就是eval函式,能計算字串算式,比如直接計算eval("5+3-2")。

    所以,一切都靠它了。

    # 計算數值並返回結果 參數chars:['8', '-', '6', '=']
    def calculation(chars):
    cstr = ''.join(chars)
    result = ''
    if("="in cstr): # 有等號
    str_arr = cstr.split('=')
    c_str = str_arr[0]
    r_str = str_arr[1]
    c_str = c_str.replace("×","*")
    c_str = c_str.replace("÷","/"
    try:
    c_r = int(eval(c_str))
    except Exception as e:
    print("Exception",e)
    if r_str == "":
    result = c_r
    else:
    if str(c_r) == str(r_str):
    result = "√"
    else:
    result = "×"
    return result

    執行之後獲得的結果是:

    recognize result: ['8''×''4''=']
    calculate result: 32
    recognize result: ['2''-''1''=''1']
    calculate result: √
    recognize result: ['1''0''-''5''=']
    calculate result: 5

    2.6.2 反饋

    有了結果之後,把結果寫到圖片上,這是最後一步,也是最簡單的一步。

    但是實作起來,居然很繁瑣。

    得找座標吧,得計算結果呈現的位置吧,我們還想標記不同的顏色,比如對了是綠色,錯了是紅色,補齊答案是灰色。

    下面程式碼是在一個圖img上,把文本內容text畫到(left,top)位置,以特定顏色和大小。

    # 繪制文本
    def cv2ImgAddText(img, text, left, top, textColor=(255, 0, 0), textSize=20):
    if (isinstance(img, np.ndarray)): # 判斷是否OpenCV圖片型別
    img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    # 建立一個可以在給定影像上繪圖的物件
    draw = ImageDraw.Draw(img)
    # 字型的格式
    font style = ImageFont.truetype("fonts/fangzheng_shusong.ttf", textSize, encoding="utf-8")
    # 繪制文本
    draw.text((left, top), text, textColor, font=font style)
    # 轉換回OpenCV格式
    return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)

    結合著切圖的資訊、計算的資訊,下面程式碼提供思路參考:

    # 獲取切圖示註,切圖圖片,原圖圖圖片
    all_mark_boxs,all_char_imgs,img_o = divImg(path,save)
    # 恢復模型,用於圖片辨識
    model = cnn.create_model()
    model.load_weights('checkpoint/char_checkpoint')
    class_name = np.load(' class_name.npy')
    # 遍歷行
    for i in range(0,len(all_char_imgs)):
    row_imgs = all_char_imgs[i]
    # 遍歷塊
    for j in range(0,len(row_imgs)):
    block_imgs = row_imgs[j]
    block_imgs = np.array(block_imgs)
    # 圖片辨識
    results = cnn.predict(model, block_imgs, class_name)
    print('recognize result:',results)
    # 計算結果
    result = calculation(results)
    print('calculate result:',result)
    # 獲取塊的標註座標
    block_mark = all_mark_boxs[i][j]
    # 獲取結果的座標,寫在塊的最後一個字
    answer_box = block_mark[-1]
    # 計算最後一個字的位置
    x = answer_box[2] 
    y = answer_box[3]
    iw = answer_box[2] - answer_box[0]
    ih = answer_box[3] - answer_box[1]
    # 計算字型大小
    textSize = max(iw,ih)
    # 根據結果設定字型顏色
    if str(result) == "√":
    color = (0, 255, 0)
    elif str(result) == "×":
    color = (255, 0, 0)
    else:
    color = (192, 192,192)
    # 將結果寫到原圖上
    img_o = cv2ImgAddText(img_o, str(result), answer_box[2], answer_box[1],color, textSize)
    # 將寫滿結果的原圖保存
    cv2.imwrite('result.jpg', img_o)

    結果是下面這樣的: