一、亮出效果
最近一些軟體的搜題、智能批改類的功能要下線,
退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 生成圖片
代碼如下,可以直接運行,
python學習交流Q群:906715085### 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 = https://www.cnblogs.com/1234567FENG/p/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 訓練資料
執行就完了,
python學習交流Q群:906715085#### # 統計檔案夾下的所有圖片數量 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) # 字體的格式 fontStyle = ImageFont.truetype("fonts/fangzheng_shusong.ttf", textSize, encoding="utf-8") # 繪制文本 draw.text((left, top), text, textColor, font=fontStyle) # 轉換回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)
結果是下面這樣的:

最后
就期末了,暑假又要到了,這是不是大家最開心的事,今天分享的這篇文章非常的長,不過在期末這樣的大工程里,用起來還是
十分香的,提前祝大家暑假愉快了,今天的文章到這里就結束了,下一章見…
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/492251.html
標籤:Python
