做文字OCR首先需要做的是生成字符圖片用于訓(xùn)練。沒(méi)有訓(xùn)練集,一切機(jī)器學(xué)習(xí)都免談了。因此,我們要做的第一件事情就是人工生成可用的數(shù)據(jù)集。
先引入我們需要的頭文件:
from PIL import Image, ImageDraw, ImageFont
from io import StringIO
import random
import os
制作數(shù)據(jù)集,首先要做的是把字符變成圖片。在網(wǎng)上找了很多資料,都是用Pygame生成字符圖片,再用PIL來(lái)讀入,進(jìn)行對(duì)應(yīng)的操作。因?yàn)镻ygame不能直接轉(zhuǎn)PIL,所以要存入磁盤,再用PIL讀出。為了提高速率,直接在內(nèi)存中進(jìn)行操作,網(wǎng)上的資料都是把Pygame生成的圖片讀入StringIO里面,再讀出。但是網(wǎng)上相關(guān)的博客,似乎都是復(fù)制了某個(gè)人的博客,而那份博客是用Python2寫的。當(dāng)我把其改成Python3的時(shí)候,StringIO的地方總是報(bào)錯(cuò),讓我很心煩。
后來(lái)我發(fā)現(xiàn),PIL可以直接生成字符圖片,并且博客上面所說(shuō)的問(wèn)題并沒(méi)有出現(xiàn),于是就可以毅然地拋棄Pygame了。
代碼很簡(jiǎn)單:
# Create Image with text
def addText(text, font):
# 生成純白的50*50的圖片
im = Image.new("RGB", (50, 50), (255, 255, 255))
dr = ImageDraw.Draw(im)
area = (random.randint(10, 15), 10)
# 將字體畫入圖片
dr.text(area, text, font=font, fill="#000000")
return im
這里的area是字體的左上角的坐標(biāo)。因?yàn)槲覀兩傻淖煮w后面需要進(jìn)行一點(diǎn)的扭曲。因此在這里設(shè)置隨機(jī)數(shù)就可以進(jìn)行相應(yīng)的平移操作。
這里需要傳入font參數(shù)。font是這樣生成的:
font = ImageFont.truetype(os.path.join("ttf", font_path), random.randint(18, 20))
os.path.join("ttf", font_path)表示./ttf/font_path。其中ttf是我存字符文件的文件夾。因?yàn)樽詈笠S機(jī)字符,因此字符的路徑font_path是在字符文件中隨機(jī)抽取。后面是字體的大小。在這里使用隨機(jī)數(shù)就可以直接實(shí)現(xiàn)字體的縮放了。
接下來(lái)對(duì)生成的圖片進(jìn)行處理。處理有旋轉(zhuǎn)和扭曲。
需要注意的是,PIL的旋轉(zhuǎn)默認(rèn)黑色為底色,因此直接旋轉(zhuǎn)的結(jié)果,就是四個(gè)角留下了黑色。而且PIL并沒(méi)有提供對(duì)應(yīng)的功能可以選擇底色。因?yàn)槲覀兿氩涣粝潞谏乃慕?,就只能在旋轉(zhuǎn)之后把圖片的邊裁剪掉了。這就是為什么之前生成50*50的圖片了,這樣我們裁剪之后,就變成了30 * 30。
def imageProcess(image):
# 圖像旋轉(zhuǎn)
image = image.rotate(random.randint(-5, 5))
# 圖像扭曲
params = [1 - float(random.randint(1, 2)) / 100,
0,
0,
0,
1 - float(random.randint(1, 10)) / 100,
float(random.randint(1, 2)) / 500,
0.001,
float(random.randint(1, 2)) / 500]
image = image.transform((50, 50), Image.PERSPECTIVE, params)
# 裁剪
image = image.crop([10, 10, 40, 40])
# 轉(zhuǎn)灰度圖
image = image.convert('L')
return image
圖像扭曲里面的params的參數(shù),我到現(xiàn)在都沒(méi)有找到文檔把它弄清楚。但是按這樣扭曲,出來(lái)的效果特別好。所以就不在意這些細(xì)節(jié)了。
如果我們需要把圖片變成其他尺寸,可以使用PIL中的resize,例如在我需要100*100。我就可以:
image = image.resize((100, 100), Image.ANTIALIAS)
后面的參數(shù)默認(rèn)是NEAREST。但是有NEAREST、BILINEAR、BICUBIC、ANTIALIAS。表示圖片縮放的四種算法。ANTIALIAS效果是比較好的。
最后,我們對(duì)圖片進(jìn)行二值化。這一步是我額外加的一步。因?yàn)樵谖沂褂谜鎸?shí)數(shù)據(jù)檢驗(yàn)我的機(jī)器學(xué)習(xí)的模型的時(shí)候,我會(huì)對(duì)真實(shí)數(shù)據(jù)進(jìn)行二值化。那么我在訓(xùn)練數(shù)據(jù)上面二值化,則它會(huì)更接近真實(shí)數(shù)據(jù)。
由于圖片比較簡(jiǎn)單,直接二值化,閾值設(shè)置為200效果就很好了。
def binarizing(image, threshold=200):
pixdata = image.load()
w, h = image.size
for y in range(h):
for x in range(w):
if pixdata[x, y] < threshold:
pixdata[x, y] = 0
else:
pixdata[x, y] = 255
return image
一開(kāi)始,我以為機(jī)器學(xué)習(xí)的速度的瓶頸在IO操作,因?yàn)槲易隽艘患?。就是?500個(gè)漢字,先把30 * 30的圖片,一共2500張,組合成50 * 50的圖片,再保存。這樣讀入2500個(gè)漢字只需要一次IO操作了。代碼如下:
def saveImage(images, font_path):
width = 30 * 40
height = 30 * 50
merge_image = Image.new('L', (width, height), 0xffffff)
xPos, yPos = 0, 0
for image in images:
merge_image.paste(image, (xPos, yPos))
xPos = xPos + 30
if (xPos == width):
xPos = 0
yPos = yPos + 30
imageName = font_path + '.png'
merge_image.save(os.path.join('words', imageName))
也就是先生成一張純白的圖片,再一張張paste上去。但是后來(lái)發(fā)現(xiàn),其實(shí)更大的瓶頸在于把這么大的圖片分割。因?yàn)閳D片的操作,肯定會(huì)比直接IO慢嘛。所以我就放棄了這種方法。
但是生成的圖片乍一看還是很好看的。
