(OpenCV 邊緣檢測代替 jTessBoxEditor 手動矯正)
目錄
- 一、概述
- 二、環境搭建
- 1. 下載 Tesseract OCR:[https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-setup-3.05.01.exe](https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-setup-3.05.01.exe)
- 2. 下載 opencv-python 和 pytesseract(可直接在cmd中下載)
- 三、生成資料集和訓練所需檔案
- 1. 匯入需要的包,以及自己寫的一個簡單的data_agumentation.py
- 2. 看一下生成資料集函式的形參和函式內部我預設的引數
- 3. 生成 font_properties.txt 檔案
- 4. 生成.tif影像和.box檔案 (寫字符、擴充資料集、檢測邊緣、獲取最小外接矩形框)
- 5. 生成train.bat批處理檔案
- 四、資料集擴充
- 五、訓練
- 六、使用自己訓練的字符庫進行識別
一、概述
本文詳細介紹在Python下實作Tesseract-OCR訓練字符庫的方法,如果資料集較大,使用jTessBoxEditor對字符進行一一矯正作業量巨大,因此本文講解如何利用opencv-python對字符進行邊緣檢測并自動獲取最小矩形框坐標,最終生成.box檔案,從而完全脫離jTessBoxEditor,
二、環境搭建
1. 下載 Tesseract OCR:https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-setup-3.05.01.exe
2. 下載 opencv-python 和 pytesseract(可直接在cmd中下載)
pip install opencv-python
pip install pytesseract
三、生成資料集和訓練所需檔案
1. 匯入需要的包,以及自己寫的一個簡單的data_agumentation.py
import os
import cv2
import numpy as np
from data_augmentation import *
2. 看一下生成資料集函式的形參和函式內部我預設的引數
def dataset_producing(path, text_list, lang, fontname, exp_num=0, italic=0, bold=0, fixed=0, serif=0, fraktur=0):
# 一、預設引數
space = 80 # 一個字符所占區域大小
row = 64 # 資料增強后排列組合的總數
img_size = (row * space, len(text_list) * space) # 畫布大小(h, w)
img = np.zeros(img_size, np.uint8) # (h, w)
img.fill(255) # 填充白色背景
x_box = 0 # 左上角坐標 x
y_box = 0 # 左上角坐標 y
3. 生成 font_properties.txt 檔案
with open(os.path.join(path, 'font_properties.txt'), 'w') as fp:
fp.write('%s %d %d %d %d %d'
% (fontname, italic, bold, fixed, serif, fraktur))
檔案內的格式:< fontname > < italic > < bold > < fixed > < serif > < fraktur >
此處參考:https://blog.csdn.net/qq_30534935/article/details/83794638
- fontname:字體名稱
- italic:斜體(0/1)
- bold:粗體(0/1)
- fixed:默認字體(0/1)
- serif:襯線字體(0/1)
- fraktur:德文黑體(0/1)
4. 生成.tif影像和.box檔案 (寫字符、擴充資料集、檢測邊緣、獲取最小外接矩形框)
這里我們直接生成一張大圖,并用opencv在圖中依次寫字符并進行增強,最后存盤為.tif,當然如果是識別手寫字符的話,可以掠過下述代碼的1和2,直接從3開始,
下面我對每一個步驟做一個詳細的解釋(由于是生成一張很大的影像,有多行字符,所以這一部分代碼在for回圈中,以下先對for回圈中的代碼片段進行逐個的解釋,這一塊的完整代碼在這一部分的最后)
1) 獲取字體大小和基準線
text_size = cv2.getTextSize(text=text_list[j], fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=get_scale(i) / 22, thickness=get_thickness(i))
w, h, baseline = text_size[0][0], text_size[0][1], text_size[1]
因為是一行一行地寫字符(第 i 行,第 j 列),所以先在寫下字符之前通過 getTextSize() 獲取到當前正準備寫的字符的 w, h, baseline,以供后面計算每個字符在每個小格子里居中時的起始位置,
getTextSize()中各引數的定義可參考這篇博客:https://blog.csdn.net/u010970514/article/details/84075776
我簡單手畫了一個圖,方便理解 getTextSize() 獲取的 baseline 的含義(但真正意義上的baseline指的是第二條紅線):
2) 寫字符
top_left = (x_box, y_box) # 方框左上角絕對坐標
subImg = img[top_left[1]:top_left[1] + space, top_left[0]:top_left[0] + space] # 獲取方框子圖
text_org = (int((space - w) / 2), int((space - baseline + h) / 2)) # 字符轉成左下角相對坐標并居中
cv2.putText(img=subImg, text=text_list[j], org=text_org, fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=get_scale(i) / 22, color=(0, 0, 0), thickness=get_thickness(i))
我們將整張圖看成一個個的小方格,并將每個方格作為一個子圖,然后依次在每個子圖里進行一系列操作:
- 先獲取每個方格左上角的坐標 top_left
- 獲取子圖 subImg 以便直接對每張子圖進行操作
- 計算居中后的字符相對于每個 subImg 的坐標 text_org,轉成左下角坐標(因為 putText() 函式中的 org 為字符左下角的坐標,
下圖中紅色的 baseline 指的是真正意義上的 baseline,而黃色的 baseline 指的是 opencv 中 getTextSize() 函式獲取到的 baseline 這個值,對于英文字母而言,所有大寫字母和絕大部分小寫字母都在 baseline 之上(如下圖的 ‘b’ ),而少數小寫字母會延伸到baseline之下(如下圖的 ‘g’ ),但 getTextSize() 取到的 height 并不包含 baseline 之下的部分,因此如果要使每個字符都居中,則分別對兩類字符進行不同的居中操作即可,但為了使生成的影像和我們平時看到的文字排列相同(以baseline為基準寫的字),我們選擇以延伸到baseline之下的這類字母為標準來居中,居中后的字符左下角坐標計算程序如下圖,
(圖中字符的邊框并不是其最小外接矩形,而是通過 getTextSize() 獲取到的,并沒有緊貼字符,由于此時還沒有開始寫字符,因此起始坐標只能通過這種方式獲得)
- 計算出可以使字符居中的左下角起始坐標后,使用 putText() 函式對 subImg 進行寫字符的操作,org=text_org
3)均值濾波
ksize = (get_ksize(i))
cv2.blur(src=subImg, ksize=ksize, dst=subImg)
使用 blur() 函式對每張 subImg 進行均值濾波(當然也可以選擇其它型別的濾波,并排列組合),ksize 是核的大小,我在另一個檔案 data_augmentation.py 中寫了一個簡陋的 get_ksize() 函式,根據是第幾行來選取不同的 ksize,這里大家可以自己設計,
4)影像二值化
ret, binary = cv2.threshold(src=subImg, thresh=254, maxval=255, type=cv2.THRESH_BINARY_INV)
- 由于濾波之后,字符邊緣會往外擴散,所以如果直接用 getTextSize() 得到的 w, h, baseline 和 2) 中算到的起始坐標 text_org 來定位并框出字符的話,box的位置必然是不準確的:
- 且經過實驗發現,即使不進行濾波,getTextSize() 得到的 w, h, baseline本身也有一定偏差,框出的框并不是字符的(不旋轉的)最小外接矩形:
- 因此,為了找到字符輪廓并畫出最小外接矩形,我們需要先通過影像二值化來凸顯字符的輪廓:
5)尋找字符的邊緣輪廓
contours, hierarchy = cv2.findContours(image=binary, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
binary 是 4) 中得到的二值化影像,**回傳值 contours 是 n 組輪廓坐標( n 指該字符由 n 個連通的部分組成,對于大多數英文字母,n=1,對于 i 和 j,n=2)
6)尋找字符的最小外接矩形
方法一(使用 boundingRect() 函式來找到最小外接矩形)(不推薦):
bounding_boxes = [cv2.boundingRect(cnt) for cnt in contours] # x, y 為矩形左上角坐標
if len(bounding_boxes) > 1: # 組合兩個不連通的圖
x0, y0, w0, h0 = bounding_boxes[0][0], bounding_boxes[0][1], bounding_boxes[0][2], bounding_boxes[0][3]
x1, y1, w1, h1 = bounding_boxes[1][0], bounding_boxes[1][1], bounding_boxes[1][2], bounding_boxes[1][3]
bounding_boxes = [(min(x0, x1), min(y0, y1), max(w0, w1), max(y1 - y0 + h1, y0 - y1 + h0))]
xmin, ymin, w, h = bounding_boxes[0][0], bounding_boxes[0][1], bounding_boxes[0][2], bounding_boxes[0][3]
xmax, ymax = xmin + w, ymin + h
- 使用 boundingRect() 函式獲取最小外接矩形的左上角坐標 x, y 以及 w, h,
- 如果獲取的 bounding box 長度大于1(即有不止一組輪廓最小外接矩形,如 i 和 j ),則我們需要將其組合,計算程序如下,其中 h 的計算,由于不確定兩組坐標的先后,所以選取最大的,(此處只考慮了由兩個不連通的圖組成的情況,因為英文字母中,一個字母最多也只有兩個不連通的圖,而數字 0-9 更是不存在這種情況,因此 > 2 的情況暫時不考慮)
若一個字符由2個以上的不連通的部分組成,則上述方法較為麻煩,因此,我決定不用 boudingRect() 函式,
方法二(尋找xmin, xmax, ymin, ymax)(推薦使用):
pts_x = [] # 存盤所有的輪廓橫坐標
pts_y = [] # 存盤所有的輪廓縱坐標
for part in contours:
for pts in range(len(part)):
pts_x.append(part[pts][0][0])
pts_y.append(part[pts][0][1])
xmin, ymin, xmax, ymax = min(pts_x), min(pts_y), max(pts_x), max(pts_y) # 找到最小外接矩形
該方法直接遍歷在一個subImg中找到的所有輪廓坐標,并找出橫縱坐標分別的最小和最大值,即可確定最小外接矩形,
7) 生成.box檔案
xmin_new, xmax_new = x_box + xmin, x_box + xmax # 將子圖坐標轉成相對于原圖的坐標
ymin_new, ymax_new = img_size[0] - y_box - ymax, img_size[0] - y_box - ymin # 轉換成以左下角為原點的坐標系的坐標 (.box檔案中以左下角為原點)
fp.write('%s %d %d %d %d %d'
% (text_list[j], xmin_new, ymin_new, xmax_new, ymax_new, 0)) # 將(字符、x、y-h、x+w、y、頁碼)寫入txt檔案
fp.write('\n')
這個步驟主要在做一些坐標轉換,由于我們之前的操作都是以左上角為原點的,而 .box 檔案中的坐標是以左下角為原點的,因此需要做一個轉換,
- 首先看一下 .box 檔案的格式:
- 坐標轉換計算:
8)將影像存盤為.tif
tif = font + '.tif'
cv2.imwrite(filename=os.path.join(path, tif), img=img)
print('%s.tif generated successfully!' % font)
5. 生成train.bat批處理檔案
這一部分的問題還未解決,問題寫在最后兩行的注釋里了,如果有人知道原因和解決方法,希望可以與我交流,
train_bat = 'echo Run Tesseract for Training.. \r' \
'tesseract.exe %s.tif %s nobatch box.train \r\n' \
'echo Compute the Character Set.. \r' \
'unicharset_extractor.exe %s.box \r' \
'mftraining -F font_properties.txt -U unicharset -O %s.unicharset %s.tr \r\n' \
'echo Clustering.. \r' \
'cntraining.exe %s.tr \r\n' \
'echo Rename Files.. \r' \
'rename normproto %s.normproto \r' \
'rename inttemp %s.inttemp \r' \
'rename pffmtable %s.pffmtable \r' \
'rename shapetable %s.shapetable \r\n' \
'echo Create Tessdata.. \r' \
'combine_tessdata.exe %s. \r\n' \
'echo. & pause' \
% (font, font, font, lang, font, font, lang, lang, lang, lang, lang)
with open(os.path.join(path, 'train.bat'), 'w') as fp:
fp.write(train_bat)
print('train.bat generated successfully!')
# 上述生成的.bat檔案無法直接執行、具體原因尚不明確,需要將檔案以編輯的形式打開、復制下來,粘貼到新建的txt檔案中,再更改后綴為.bat
# 已經過多次排查,只有上述方法可以得到可以執行的批處理檔案,即使檔案內容由復制粘貼得來完全一樣,檔案大小相差14kb,原因未知
第三部分的完整代碼:
import os
import cv2
import numpy as np
from data_augmentation import *
def dataset_producing(path, text_list, lang, fontname, exp_num=0, italic=0, bold=0, fixed=0, serif=0, fraktur=0):
# 一、預設引數
space = 80 # 一個字符所占區域大小
row = 64 # 資料增強后排列組合的總數
img_size = (row * space, len(text_list) * space) # 畫布大小(h, w)
img = np.zeros(img_size, np.uint8) # (h, w)
img.fill(255) # 填充白色背景
x_box = 0 # 左上角x
y_box = 0 # 左上角y
# 二、生成font_properties.txt檔案
with open(os.path.join(path, 'font_properties.txt'), 'w') as fp:
fp.write('%s %d %d %d %d %d' % (fontname, italic, bold, fixed, serif, fraktur))
print('font_properties.txt generated successfully!')
# 三、生成.tif影像和.box檔案 (寫字符、檢測邊緣、獲取最小外接矩形框)
font = '%s.%s.exp%d' % (lang, fontname, exp_num)
with open(os.path.join(path, '%s.box' % font), 'w') as fp:
for i in range(int(img_size[0] / space)): # 寫第i行
for j in range(int(img_size[1] / space)): # 寫第j列
# 1、獲取字體大小和基準線
text_size = cv2.getTextSize(text=text_list[j], fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=get_scale(i) / 22, thickness=get_thickness(i))
w, h, baseline = text_size[0][0], text_size[0][1], text_size[1]
# 2、寫字符
top_left = (x_box, y_box) # 方框左上角絕對坐標
subImg = img[top_left[1]:top_left[1] + space, top_left[0]:top_left[0] + space] # 獲取方框子圖
text_org = (int((space - w) / 2), int((space - baseline + h) / 2)) # 字符轉成左下角相對坐標并居中
cv2.putText(img=subImg, text=text_list[j], org=text_org, fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=get_scale(i) / 22, color=(0, 0, 0), thickness=get_thickness(i))
# 3、均值濾波 (對子圖進行濾波操作時需要加上dst,否則不對原圖產生改變)
ksize = (get_ksize(i))
cv2.blur(src=subImg, ksize=ksize, dst=subImg)
# 4、影像二值化
ret, binary = cv2.threshold(src=subImg, thresh=254, maxval=255, type=cv2.THRESH_BINARY_INV)
# 5、尋找字符邊緣輪廓
contours, hierarchy = cv2.findContours(image=binary, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
# 6、尋找字符最小外接矩形
# # 方法一:
# bounding_boxes = [cv2.boundingRect(cnt) for cnt in contours] # x, y 為矩形左上角坐標
# if len(bounding_boxes) > 1: # 組合兩個不連通的圖
# x0, y0, w0, h0 = bounding_boxes[0][0], bounding_boxes[0][1], bounding_boxes[0][2], bounding_boxes[0][3]
# x1, y1, w1, h1 = bounding_boxes[1][0], bounding_boxes[1][1], bounding_boxes[1][2], bounding_boxes[1][3]
# bounding_boxes = [(min(x0, x1), min(y0, y1), max(w0, w1), max(y1 - y0 + h1, y0 - y1 + h0))]
# xmin, ymin, w, h = bounding_boxes[0][0], bounding_boxes[0][1], bounding_boxes[0][2], bounding_boxes[0][3]
# xmax, ymax = xmin + w, ymin + h
# 方法二:
pts_x = [] # 存盤所有的輪廓橫坐標
pts_y = [] # 存盤所有的輪廓縱坐標
for part in contours:
for pts in range(len(part)):
pts_x.append(part[pts][0][0])
pts_y.append(part[pts][0][1])
xmin, ymin, xmax, ymax = min(pts_x), min(pts_y), max(pts_x), max(pts_y) # 找到最小外接矩形
# # 畫出最小外接矩形 (測驗時可用)
# cv2.rectangle(img=subImg, pt1=(xmin, ymin), pt2=(xmax, ymax), color=(0, 255, 0), thickness=1)
# 7、生成box檔案
xmin_new, xmax_new = x_box + xmin, x_box + xmax # 將子圖坐標轉成相對于原圖的坐標
ymin_new, ymax_new = img_size[0] - y_box - ymax, img_size[0] - y_box - ymin # 轉換成以左下角為原點的坐標系的坐標 (.box檔案中以左下角為原點)
fp.write('%s %d %d %d %d %d'
% (text_list[j], xmin_new, ymin_new, xmax_new, ymax_new, 0)) # 將(字符、x、y-h、x+w、y、頁碼)寫入txt檔案
fp.write('\n')
x_box += space # 寫下一列
x_box = 0 # x回傳第一列
y_box += space # 寫下一行
print('%s.box generated successfully!' % font)
# # 畫網格(測驗居中時使用)
# y = 0
# for i in range(int(img_size[0] / space)): # 畫橫線
# cv2.line(img=img, pt1=(0, y), pt2=(img_size[1], y), color=(0, 0, 0), thickness=1) # 畫網格
# y += space
# x = 0
# for i in range(int(img_size[1] / space)): # 畫豎線
# cv2.line(img=img, pt1=(x, 0), pt2=(x, img_size[0]), color=(0, 0, 0), thickness=1) # 畫網格
# x += space
# 8、將影像存盤為.tif以供訓練
tif = font + '.tif'
cv2.imwrite(filename=os.path.join(path, tif), img=img)
print('%s.tif generated successfully!' % font)
# 四、生成train.bat批處理檔案
train_bat = 'echo Run Tesseract for Training.. \r' \
'tesseract.exe %s.tif %s nobatch box.train \r\n' \
'echo Compute the Character Set.. \r' \
'unicharset_extractor.exe %s.box \r' \
'mftraining -F font_properties.txt -U unicharset -O %s.unicharset %s.tr \r\n' \
'echo Clustering.. \r' \
'cntraining.exe %s.tr \r\n' \
'echo Rename Files.. \r' \
'rename normproto %s.normproto \r' \
'rename inttemp %s.inttemp \r' \
'rename pffmtable %s.pffmtable \r' \
'rename shapetable %s.shapetable \r\n' \
'echo Create Tessdata.. \r' \
'combine_tessdata.exe %s. \r\n' \
'echo. & pause' \
% (font, font, font, lang, font, font, lang, lang, lang, lang, lang)
with open(os.path.join(path, 'train.bat'), 'w') as fp:
fp.write(train_bat)
print('train.bat generated successfully!')
# 上述生成的.bat檔案無法直接執行、具體原因尚不明確,需要將檔案以編輯的形式打開、復制下來,粘貼到新建的txt檔案中,再更改后綴為.bat
# 已經過多次排查,只有上述方法可以得到可以執行的批處理檔案,即使檔案內容由復制粘貼得來完全一樣,檔案大小相差14kb,原因未知
畫上網格和最小外接矩形后生成的影像大概是這樣的(生成用于訓練的影像時請把網格和矩形框去掉):

去掉網格線和最小外接矩形框,重新生成影像后,為了驗證 .box 檔案的正確性,也可在jTessBoxEditor 中打開該 tif 影像進行查看:

四、資料集擴充
這就是我前面提到的寫的一個簡單的 data_agmentation.py,為了快速實作走通流程,我還沒有使用很多的影像增強方式,之后會完善,目前里面暫時只包含了字體縮放、字體粗細、均值濾波,其中,字體縮放和字體粗細直接傳到 opencv 的 putText() 函式的 fontScale 和 thickness 里,均值濾波則是 blur() 函式,因為寫得比較簡陋,而且大家可以根據自己的想法去設定這些函式,然后排列組合,所以我就不暫時展示我寫的了,
五、訓練
接著第三步的生成了 train.bat 往后講(注意最后兩行我寫的注釋,需要重新新建 txt 檔案,復制并粘貼上生成的 bat 檔案中的代碼,再將檔案后綴改為 .bat,點開就執行了),執行完之后,找到 .traineddata 檔案,把它放入 Tesseract-OCR 檔案夾下的 tessdata 中即可,


六、使用自己訓練的字符庫進行識別
把待測的影像放入一個空檔案下(如我代碼中的 testing_pics 檔案夾),即會遍歷檔案夾下的檔案進行逐個識別,language 的值就是第五步中生成的 .traineddata 檔案的名字,
import os
import pytesseract
from PIL import Image
# 將待測影像放入專案檔案下的testing_pics檔案下,進行依次測驗
pics_path = 'testing_pics'
language = 'num'
def test(path, lang=None):
for img in os.listdir(pics_path):
name = img
img = Image.open(os.path.join(pics_path, img))
text = pytesseract.image_to_string(image=img, lang=lang)
print('testing result for', name, ':')
print(text)
if __name__ == '__main__':
test(path=pics_path, lang=language)
以上就是整個生成資料集、訓練、測驗的流程了,整個流程花了將近兩天時間做完并完善到目前這一步,博客寫了整整一天,寫博客真的非常不容易,轉載請注明出處,歡迎交流討論,
——殷越(2021/12/19)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/387081.html
標籤:AI
上一篇:Java:反射機制溫顧
