OCR的全稱是Optical Character Recognition,光學字符識別技術。目前應用於各個領域方向,甚至這些應用就在我們的身邊,比如身份證的識別,交通路牌的識別,車牌的自動識別等等。本文就學習一下基於開源軟件和大廠服務的文字識別效果。
關於ocr的簡介,請參考博客:https://www.cnblogs.com/wj-1314/p/9446756.html
剛入門不久,而且還是自己摸着石頭過河,所以學的知識深一點,淺一點的,博客里面記錄的是自己學習的過程,希望記錄自己的學習之路,回頭看也好回顧。
在去年八九月份的時候,學習了好多關於OCR文字識別的文章,並做了筆記,如下。那現在對一些簡單快速識別的接口做下筆記。
方案1:基於開源軟件Tesseract實現
有些時候使用開源軟件比較方便,當然目前最有名的就是tesseract,目前由谷歌在進行維護,貌似使用的還是傳統機器學習的方式。
那這里想了解,可以參考我寫過的文章:
2,深入學習Tesseract-ocr識別中文並訓練字庫的方法
方案2:百度文字識別接口服務
下面主要針對Python開發者,描述百度文字識別接口服務的相關技術內容。OCR接口提供了自然場景下整圖文字檢測、定位、識別等功能。文字識別的結果可以用於翻譯、搜索、驗證碼等代替用戶輸入的場景。
首先我們可以找一個產品服務的定價單:
可以看到,倘若你的服務只有偶爾用一次,完全可以使用這種體驗型的免費服務。如果一天需要調用一萬次,那么一個月基本的花費在5w左右——成本還是很高的,所以很多商用的場景大多數都采用自主研發的方法來做。
下面學習一下百度API的SDK軟件開發工具包(Software Development Kit)的使用。
1,百度AipOcr接口能力
2,安裝接口模型
pip install baidu-aip
3,新建AipOcr
AipOcr是OCR的Python SDK客戶端,為使用OCR的開發人員提供了一系列的交互方法。
參考如下代碼新建一個AipOcr:
from aip import AipOcr """ 你的 APPID AK SK """ APP_ID = '你的 App ID' API_KEY = '你的 Api Key' SECRET_KEY = '你的 Secret Key' client = AipOcr(APP_ID, API_KEY, SECRET_KEY)
在上面代碼中,常量APP_ID
在百度雲控制台中創建,常量API_KEY
與SECRET_KEY
是在創建完畢應用后,系統分配給用戶的,均為字符串,用於標識用戶,為訪問做簽名驗證,可在AI服務控制台中的應用列表中查看。
上面代碼塊里APP_ID 、API_KEY、SECRET_KEY 三個值對應在http://console.bce.baidu.com/ai/#/ai/ocr/app/list 這里找到,需要用百度賬號登錄,然后創建一個應用,如下圖:
4,配置AipOcr
如果用戶需要配置AipOcr的網絡請求參數(一般不需要配置),可以在構造AipOcr之后調用接口設置參數,目前只支持以下參數:
5,通用文字識別接口說明
我們向服務請求識別某張圖中的所有文字
""" 讀取圖片 """ def get_file_content(filePath): with open(filePath, 'rb') as fp: return fp.read() image = get_file_content('example.jpg') """ 調用通用文字識別, 圖片參數為本地圖片 """ client.basicGeneral(image); """ 如果有可選參數 """ options = {} options["language_type"] = "CHN_ENG" options["detect_direction"] = "true" options["detect_language"] = "true" options["probability"] = "true" """ 帶參數調用通用文字識別, 圖片參數為本地圖片 """ client.basicGeneral(image, options) url = "https//www.x.com/sample.jpg" """ 調用通用文字識別, 圖片參數為遠程url圖片 """ client.basicGeneralUrl(url); """ 如果有可選參數 """ options = {} options["language_type"] = "CHN_ENG" options["detect_direction"] = "true" options["detect_language"] = "true" options["probability"] = "true" """ 帶參數調用通用文字識別, 圖片參數為遠程url圖片 """ client.basicGeneralUrl(url, options)
6,通用文字識別 請求參數詳情
7,通用文字識別 返回數據參數詳情
8, 通用文字識別返回示例
{ "log_id": 2471272194, "words_result_num": 2, "words_result": [ {"words": " TSINGTAO"}, {"words": "青島睥酒"} ] }
9,最終代碼:
# -*- coding: UTF-8 -*- from aip import AipOcr # 定義常量 APP_ID = '14544448' API_KEY = 'yRZGUXAlCd0c9vQj1kAjBEfY' SECRET_KEY = 'sc0DKGy7wZ9MeWFGZnbscbRyoDB2IQlj' # 初始化AipFace對象 client = AipOcr(APP_ID, API_KEY, SECRET_KEY) # 讀取圖片 def get_file_content(filePath): with open(filePath, 'rb') as fp: return fp.read() image = get_file_content('binary_best.jpg') # 調用通用文字識別, 圖片為本地圖片 res=client.general(image) print(res) for item in res['words_result']: print(item['words'])
10,識別結果
識別圖片1為:
識別結果1為:
{'log_id': 599780865793529561, 'words_result_num': 7, 'words_result': [{'location': {'width': 321, 'top': 6, 'left': 2, 'height': 23}, 'words': '山有 木兮木有枝,心悅君兮君不知。'}, {'location': {'width': 143, 'top': 46, 'left': 2, 'height': 19}, 'words': '出自先秦的《越人歌》'}, {'location': {'width': 146, 'top': 84, 'left': 1, 'height': 18}, 'words': '今夕何夕兮,拳舟中流'}, {'location': {'width': 181, 'top': 112, 'left': 1, 'height': 19}, 'words': '今日何日兮,得與王 子同舟。'}, {'location': {'width': 152, 'top': 141, 'left': 1, 'height': 17}, 'wo rds': '蒙羞被好兮,不訾詬恥。'}, {'location': {'width': 181, 'top': 168, 'left': 1, 'height': 19}, 'words': '心幾煩而不絕兮,得知王子。'}, {'location': {'width': 222, 'top': 195, 'left': 2, 'height': 20}, 'words': '山有木兮木有枝,心悅君兮君不知。'}]} 山有木兮木有枝,心悅君兮君不知。 出自先秦的《越人歌》 今夕何夕兮,拳舟中流 今日何日兮,得與王子同舟。 蒙羞被好兮,不訾詬恥。 心幾煩而不絕兮,得知王子。 山有木兮木有枝,心悅君兮君不知。 Process finished with exit code 0
識別圖片2為:
識別結果2為:
{'log_id': 7769540578562875641, 'words_result_num': 7, 'words_result': [{'location': {'width': 44, 'top': 20, 'left': 39, 'height': 21}, 'words': 'GTIN'}, {'location': {'width': 54, 'top': 18, 'left': 329, 'height': 20}, 'words': 'Count'}, {'location': {'width': 139, 'top': 45, 'left': 37, 'height': 21}, 'words': '001 90198720566'}, {'location': {'width': 73, 'top': 76, 'left': 36, 'height': 24}, 'words': 'SSCC18:'}, {'location': {'width': 69, 'top': 78, 'left': 329, 'height': 21}, 'words': 'Product'}, {'location': {'width': 178, 'top': 104, 'left': 35, 'height': 21}, 'words': '008859090495625554'}, {'location': {'width': 108, 'top': 104, 'left': 328, 'height': 22}, 'words': 'MRJP2CH/A'}]} GTIN Count 00190198720566 SSCC18: Product 008859090495625554 MRJP2CH/A
參考:http://ai.baidu.com/docs#/OCR-Python-SDK/e4ddca43
方案3:KNN實現簡單的OCR
這次在網上看到KNN在ocr的識別,非常想嘗試一下,原文作者說KNN在ocr的識別過程中能發揮作用的地方就是將圖像中的文字轉化為文本格式,而OCR的其他部分,比如圖像預處理,二值化等操作將丟給OpenCV去操作。
代碼地址:github:https://github.com/hahahaha1997/OCR
1,訓練集簡介
由於我們采用的是KNN來轉換圖像中的文字為文本格式,需要一個龐大的手寫字符訓練集來支撐我們的算法。這里我使用的是《機器學習實戰》2.3實例:手寫識別系統中使用的數據集,其下載地址為:https://www.manning.com/books/machine-learning-in-action,在Source Code\Ch02\digits\trainingDigits中的兩千多個手寫字符既是我所使用的訓練集。
這個訓練集配合上它所提供的測試集,提供了一個准確度非常高的分類器:
訓練集是由0~9十個數字組成的,每個數字有兩百個左右的訓練樣本。所有的訓練樣本統一被處理為一個32*32的0/1矩陣,其中所有值為1的連通區域構成了形象上的數字,如下所示:
所以,在構造我們的測試集的時候,所有的手寫數字圖片必須被處理為這樣的格式才能夠使得分類算法正確地進行,這也是KNN的局限所在。
2、構建測試集
上面已經提到,要想算法正確地進行,測試集的樣式應該和訓練集相同,也就是說我們要把一張包含有手寫數字的圖像,轉換為一個32*32的0/1點陣。
測試集使用我自己手寫的10個數字:
這里存在一個非常大的問題:這個數據集的作者是土耳其人,他們書寫數字的習慣和我們有諸多不同,比如上面的數字4和數字8,下面這樣子的數字就無法識別:4/8。哈哈,也就是說它連印刷體都無法識別,這是這個訓練集的一大缺陷之一。
1)圖像預處理
圖像預處理的過程是一個數字圖像處理(DIP)的過程,觀察上面的10個數字,可以發現每張圖像的大小/對比度的差距都非常大,所以圖像預處理應該消除這些差距。
第一步是進行圖像的放大/縮小。由於我們很難產生一個小於32*32像素的手寫數字圖像,所以這里主要是縮小圖像:
import cv2 def readImage(imagePath): image = cv2.imread(imagePath,cv2.IMREAD_GRAYSCALE) image = cv2.resize(image,(32,32),interpolation = cv2.INTER_AREA) return image
這里我沒有去實現圖像重采樣的方法,而是采用的OpenCV,通過area來確定取樣點的灰度值(推薦用bicubic interpolation,對應的插入函數應該是INTER_CUBIC),在讀入圖像的時候讀入方式位IMRAD_GRAYSCALE,因為我們需要的是識別手寫字符,灰度圖對比彩色圖能更好的突出重點。
進行圖像的縮放是不夠的,因為觀察上面的圖片可以發現:拍攝環境對於對比度的影響非常大,所以我們應該突出深色區域(數字部分),來保證后面的工作順利進行,這里采用的是伽馬變換(也可以采用對數變換):
def imageGamma(image): for i in range(32): for j in range(32): image[i][j]=3*pow(image[i][j],0.8) return image
2)圖像二值化
縮小/放大后的圖像已經是一個32*32的圖像了,下一步則是將非數字區域填充0,數字區域填充1,這里我采用的是閾值二值化處理:
def imageThreshold(image): ret,image = cv2.threshold(image,150,255,cv2.THRESH_BINARY) return image
經過二值化處理,數字部分的灰度值應該為0,而非數字部分的連通區域的灰度值應該為255,如下所示:
3)去噪
圖像去噪的方式有很多種,這里建立使用自適應中值濾波器進行降噪,因為我們的圖像在傳輸過程中可能出現若干的椒鹽噪聲,這個噪聲在上述的二值化處理中有時候是非常棘手的。
到目前為止,一副手機攝像的手寫數字圖像就可以轉換為一個32*32的二值圖像。
4)生成訓練樣本
如何將這個32*32的二值圖像轉換為0/1圖像,這個處理非常簡單:
def imageProcess(image): with open(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits\6_0.txt','w+') as file: for i in range(32): for j in range(32): if image[i][j] == 255: file.write('0') else: file.write('1') file.writelines('\n')
這里我的代碼在掃描這個圖像的同時,將其保存為一個訓練樣本,命名和訓練集的明明要求一樣為N_M.txt,其中N代表這個訓練樣本的實際分類是什么數字,M代表這是這個數字的第幾個樣本。這里對圖像進行灰度變換已經是多此一舉了,我所需要的是0/1矩陣而非一個0/1圖像,所以在掃描過程中一並生成訓練樣本更加省時直觀。
5)形成訓練集
上面的示例只是生成一個圖像的訓練樣本的,而實際上我們往往需要一次性生成一個訓練集,這就要求這個圖像預處理、二值化並且生成0/1矩陣的過程是自動的:
from os import listdir def imProcess(imagePath): testDigits = listdir(imagePath) for i in range(len(testDigits)): imageName = testDigits[i]#圖像命名格式為N_M.png,NM含義見4)生成訓練樣本 #imageClass = int((imageName.split('.')[0]).split('_')[0])#這個圖像的數字是多少 image = cv2.imread(imageName,cv2.IMREAD_GRAYSCALE) image = cv2.resize(image, (32, 32), interpolation=cv2.INTER_AREA) ret, image = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY) with open(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits\\'+imageName.split('.')[0]+'.txt','w+') as file: for i in range(32): for j in range(32): if image[i][j] == 255: file.write('0') else: file.write('1') file.writelines('\n')
這個函數將imagePath文件夾中所有的N_M命名的手寫數字圖像讀取並經過預處理、二值化、最后保存為對應的0/1矩陣,命名為N_M.txt,這就構成一個訓練集了。
3、構建分類器
分類器如下:
def classify(vector,dataSet,labels,k): distance = sqrt(abs(((tile(vec,(dataSet.shape[0],1)) - dataSet) ** 2).sum(axis = 1))); #計算距離 sortedDistance = distance.argsort() dict={} for i in range(k): label = labels[sortedDistance[i]] if not label in dict: dict[label] = 1 else: dict[label]+=1 sortedDict = sorted(dict,key = operator.itemgetter(1),reverse = True) return sortedDict[0][0] def dict2list(dic:dict):#將字典轉換為list類型 keys=dic.keys() values=dic.values() lst=[(key, value)for key,value in zip(keys,values)] return lst
distance的計算和dict2list函數的詳解在上一節,戳上面的classify既可以跳轉過去。
分類器已經構建完成,下一步是提取每一個測試樣本,提取訓練集,提取label的過程:(這個過程大部分用的是《機器學習實戰》中的代碼,對於難以理解的代碼在下文中做了解釋:)
1)讀取0/1矩陣文件:
def img2vector(filename): returnvec = numpy.zeros((1,1024)) file = open(filename) for i in range(32): line = file.readline() for j in range(32): returnvec[0,32*i+j] = int(line[j]) return returnvec
這里要注意:構造一個32*32的全零矩陣的時候,應該是numpy.zeros((1,1024)),雙層括號!雙層括號!雙層括號!代表構造的是一個二維矩陣!
2)讀取訓練集和測試集並求解准確率:
def handWritingClassifyTest(): labels=[] trainingFile = listdir(r'F:\Users\yang\PycharmProjects\OCR_KNN\trainingDigits') m = len(trainingFile) trainingMat = numpy.zeros((m,1024)) for i in range(m): file = trainingFile[i] filestr = file.strip('.')[0] classnum = int(filestr.strip('_')[0]) labels.append(classnum) trainingMat[i,:] = img2vector('trainingDigits/%s' % file) testFileList = listdir(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits') error = 0.0 testnum = len(testFileList) for i in range(testnum): file_test = testFileList[i] filestr_test = file_test.strip('.')[0] classnum_test = int(filestr_test.strip('_')[0]) vector_test = img2vector('testDigits/%s'%file_test) result = classify(vector_test,trainingMat,labels,1) if(result!=classnum_test):error+=1.0 print("准確率:%f"%(1.0-(error/float(testnum))))
代碼其實沒有很難懂的地方,主要任務就是讀取文件,通過img2vctor函數轉換為矩陣,還有切割文件名獲取該測試樣本的類別和該訓練樣本的類別,通過對比獲得准確率。
3、使用分類器
現在為止,我們的分類器已經構建完成,下面就是測試和使用階段:
1)測試《機器學習實戰》中給出的訓練集:
2)測試手寫訓練集:
果然學不出來大佬寫字,附上幾張無法識別的0/1數字矩陣:(0,4,6無法識別的原因是比划太細哈哈,8無法識別的原因……太端正了吧)
4、完整代碼:
from os import listdir import numpy import operator import cv2 def imProcess(imagePath): testDigits = listdir(imagePath) for i in range(len(testDigits)): imageName = testDigits[i]#圖像命名格式為N_M.png,NM含義見4)生成訓練樣本 #imageClass = int((imageName.split('.')[0]).split('_')[0])#這個圖像的數字是多少 image = cv2.imread(imageName,cv2.IMREAD_GRAYSCALE) image = cv2.resize(image, (32, 32), interpolation=cv2.INTER_AREA) ret, image = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY) with open(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits\\'+imageName.split('.')[0]+'.txt','w+') as file: for i in range(32): for j in range(32): if image[i][j] == 255: file.write('0') else: file.write('1') file.writelines('\n') def img2vector(filename): returnvec = numpy.zeros((1,1024)) file = open(filename) for i in range(32): line = file.readline() for j in range(32): returnvec[0,32*i+j] = int(line[j]) return returnvec def handWritingClassifyTest(): labels=[] trainingFile = listdir(r'F:\Users\yang\PycharmProjects\OCR_KNN\trainingDigits') m = len(trainingFile) trainingMat = numpy.zeros((m,1024)) for i in range(m): file = trainingFile[i] filestr = file.strip('.')[0] classnum = int(filestr.strip('_')[0]) labels.append(classnum) trainingMat[i,:] = img2vector('trainingDigits/%s' % file) testFileList = listdir(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits') error = 0.0 testnum = len(testFileList) for i in range(testnum): file_test = testFileList[i] filestr_test = file_test.strip('.')[0] classnum_test = int(filestr_test.strip('_')[0]) vector_test = img2vector('testDigits/%s'%file_test) result = classify(vector_test,trainingMat,labels,1) if(result!=classnum_test):error+=1.0 print("准確率:%f"%(1.0-(error/float(testnum)))) def classify(inX,dataSet,labels,k): size = dataSet.shape[0] distance = (((numpy.tile(inX,(size,1))-dataSet)**2).sum(axis=1))**0.5 sortedDistance = distance.argsort() count = {} for i in range(k): label = labels[sortedDistance[i]] count[label]=count.get(label,0)+1 sortedcount = sorted(dict2list(count),key=operator.itemgetter(1),reverse=True) return sortedcount[0][0] def dict2list(dic:dict):#將字典轉換為list類型 keys=dic.keys() values=dic.values() lst=[(key, value)for key,value in zip(keys,values)] return lst # def imProcess(image): # image = cv2.resize(image, (32, 32), interpolation=cv2.INTER_AREA) # ret, image = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY) # cv2.imshow('result',image) # cv2.waitKey(0) # with open(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits\6_0.txt','w+') as file: # for i in range(32): # for j in range(32): # if image[i][j] == 255: # file.write('0') # else: # file.write('1') # file.writelines('\n') # iamge = cv2.imread(r'C:\Users\yang\Desktop\6.png',cv2.IMREAD_GRAYSCALE) # image = imProcess(iamge) imProcess(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits') handWritingClassifyTest()
5、總結
KNN還是不適合用來做OCR的識別過程的,雖然《機器學習實戰》的作者提到這個系統是美國的郵件分揀系統實際運行的一個系統,但是它肯定無法高准確率地識別中國人寫的手寫文字就對了,畢竟中國有些地方的“9”還會寫成“p”的樣子的。這一節主要是將KNN拓展到實際運用中的,結合上一節的理論,KNN的執行效率還是太低了,比如這個系統,要識別一個手寫數字,它需要和所有的訓練樣本做距離計算,每個距離計算又有1024個(a-b)²,還有運行效率特別低下的sqrt(),如果是一個非常大的測試集,需要的時間就更加龐大,如果訓練集非常龐大,在將0/1矩陣讀入內存中的時候,內存開銷是非常巨大的,所以整個程序可能會非常耗時費力。不過KNN仍舊是一個精度非常高的算法,並且也是機器學習分類算法中最簡單的算法之一。
KNN實現OCR的轉載地址:https://www.cnblogs.com/DawnSwallow/p/9440516.html
知識儲備:使用Python寫入數據到Excel文件中
1 安裝XlsxWriter
python XlsxWriter模塊創建aexcel表格,生成的文件后綴名為.xlsx,最大能夠支持1048576行數據,16384列數據
pip install XlsxWriter
2 在Excel寫數據
# 建立文件 workbook = xlsxwriter.Workbook("text.xlsx") # 可以制定表的名字 # worksheet = workbook.add_worksheet('text') worksheet = workbook.add_worksheet() # 設置列寬 # worksheet.set_column('A:A',10) # 設置祖體 bold = workbook.add_format({'bold':True}) # 定義數字格式 # money = workbook.add_format({'num_format':'$#,##0'}) # 寫入帶粗體的數據 worksheet.write('A1','data',bold) worksheet.write('B1','work') ''' worksheet.write(0, 0, 'Hello') # write_string() worksheet.write(1, 0, 'World') # write_string() worksheet.write(2, 0, 2) # write_number() worksheet.write(3, 0, 3.00001) # write_number() worksheet.write(4, 0, '=SIN(PI()/4)') # write_formula() worksheet.write(5, 0, '') # write_blank() worksheet.write(6, 0, None) # write_blank() ''' worksheet.write('A3',15) worksheet.write('B3',20) worksheet.write('C3',44) worksheet.write('D3',36) # xlsx計算數據 worksheet.write('E3','=SUM(A3:D3)') ''' 建立Chart對象: chart = workbook.add_chart({type, 'column'}) Chart: Area, Bar, Column, Doughnut, Line, Pie, Scatter, Stock, Radar 將圖插入到sheet中: worksheet.insert_chart('A7', chart) ''' # 定義插入的圖標樣式 chart = workbook.add_chart({"type":'column'}) headings = ['a','b','c'] data = [ [1,2,3,4,5], [2,4,6,8,10], [3,6,9,12,15], ] # 按行插入數據 worksheet.write_row('A4',headings) # 按列插入數據 worksheet.write_column('A5',data[0]) worksheet.write_column('B5',data[1]) worksheet.write_column('C5',data[2]) # 圖行的數據區 # name:代表圖例名稱; # categories:是x軸項,也就是類別; # values:是y軸項,也就是值; chart.add_series({ 'name':'=Sheet1!$B$4', 'categories':'=Sheet1!$A$5:$A$9', 'values':'=Sheet1!$B$5:$B$9', }) chart.add_series({ 'name':['Sheet1', 3, 2], 'categories':['Sheet1', 4, 0, 8, 0], 'values':['Sheet1', 4, 2, 8, 2], }) # 圖形的標題 chart.set_title ({'name': 'Percent Stacked Chart'}) # 圖形X軸的說明 chart.set_x_axis({'name': 'Test number'}) # 圖形Y軸的說明 chart.set_y_axis({'name': 'Sample length (mm)'}) # 設置圖表風格 chart.set_style(11) # 插入圖形,帶偏移 worksheet.insert_chart('D12',chart,{'x_offset': 25, 'y_offset': 10}) workbook.close()