寫(xiě)在前面
在上一篇文章Python徒手實(shí)現(xiàn)手寫(xiě)數(shù)字識(shí)別—大綱中,我們已經(jīng)講過(guò)了我們想要寫(xiě)的全部思路,所以我們不再說(shuō)全部的思路。
我這一次將圖片的讀入與處理的代碼寫(xiě)了一下,和大綱寫(xiě)的過(guò)程一樣,這一段代碼分為以下幾個(gè)部分:
- 讀入圖片;
- 將圖片讀取為灰度值矩陣;
- 圖片背景去噪;
- 切割圖片,得到手寫(xiě)數(shù)字的最小矩陣;
- 拉伸/壓縮圖片,得到標(biāo)準(zhǔn)大小為100x100大小矩陣;
- 將圖片拉為1x10000大小向量,存入訓(xùn)練矩陣中。
所以下面將會(huì)對(duì)這幾個(gè)函數(shù)進(jìn)行詳解。
代碼分析
基礎(chǔ)內(nèi)容
首先我們現(xiàn)在最前面定義基礎(chǔ)變量
1
2
3
4
5
6
7
8
9
|
import os from skimage import io import numpy as np ##Essential vavriable 基礎(chǔ)變量 #Standard size 標(biāo)準(zhǔn)大小 N = 100 #Gray threshold 灰度閾值 color = 100 / 255 |
其中標(biāo)準(zhǔn)大小指的是我們?cè)谧詈蠼?jīng)過(guò)切割、拉伸后得到的圖片的尺寸為NxN。灰度閾值指的是在某個(gè)點(diǎn)上的灰度超過(guò)閾值后則變?yōu)?.
接下來(lái)是這圖像處理的一部分的主函數(shù)
1
2
|
filenames = os.listdir(r "./num/" ) pic = GetTrainPicture(filenames) |
其中filenames得到在num目錄下所有文件的名稱組成的列表。pic則是通過(guò)函數(shù)GetTrainPicture得到所有訓(xùn)練圖像向量的矩陣。這一篇文章主要就是圍繞這個(gè)函數(shù)進(jìn)行講解。
GetTrainPicture函數(shù)
GetTrainPicture函數(shù)內(nèi)容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#Read and save train picture 讀取訓(xùn)練圖片并保存 def GetTrainPicture(files): Picture = np.zeros([ len (files), N * * 2 + 1 ]) #loop all pictures 循環(huán)所有圖片文件 for i, item in enumerate (files): #Read the picture and turn RGB to grey 讀取這個(gè)圖片并轉(zhuǎn)為灰度值 img = io.imread( './num/' + item, as_grey = True ) #Clear the noise 清除噪音 img[img>color] = 1 #Cut the picture and get the picture of handwritten number #將圖片進(jìn)行切割,得到有手寫(xiě)數(shù)字的的圖像 img = CutPicture(img) #Stretch the picture and get the standard size 100x100 #將圖片進(jìn)行拉伸,得到標(biāo)準(zhǔn)大小100x100 img = StretchPicture(img).reshape(N * * 2 ) #Save the picture to the matrix 將圖片存入矩陣 Picture[i, 0 :N * * 2 ] = img #Save picture's name to the matrix 將圖片的名字存入矩陣 Picture[i, N * * 2 ] = float (item[ 0 ]) return Picture |
可以看出這個(gè)函數(shù)的信息量非常大,基本上今天做的所有步驟我都把封裝到一個(gè)個(gè)函數(shù)里面了,所以這里我們可以看到圖片處理的所有步驟都在這里。
提前準(zhǔn)備
首先是創(chuàng)建了一個(gè)用來(lái)存放所有圖像向量的矩陣Picture,大小為fx10001,其中f代表我們擁有的訓(xùn)練圖片的數(shù)目,10001的前10000位代表圖片展開(kāi)后的向量長(zhǎng)度,最后一維代表這一個(gè)向量的類別,比如說(shuō)時(shí)2就代表這個(gè)圖片上面寫(xiě)的數(shù)字是2.
接下來(lái)用的是一個(gè)for循環(huán),將files里面每一個(gè)圖片進(jìn)行一次迭代,計(jì)算出向量后存入picture。
在循環(huán)中的內(nèi)容就是對(duì)每一張圖片進(jìn)行的操作。
讀入圖片并清除背景噪音
首先是io.imread函數(shù),這個(gè)函數(shù)是將圖片導(dǎo)出成為灰度值的矩陣,每一個(gè)像素點(diǎn)是矩陣上的一個(gè)元素。
接下來(lái)是img[img>color]=1這一句。這一句運(yùn)用了邏輯運(yùn)算的技巧,我們可以將其分為兩部分
1
2
|
point = img > color img[point] = 1 |
首先是img>color,img是一個(gè)矩陣,color是一個(gè)數(shù)。意義就是對(duì)img中所有元素進(jìn)行判斷是否大于color這個(gè)數(shù),并輸出一個(gè)與img同等大小的矩陣,對(duì)應(yīng)元素上是該值與color判斷后的結(jié)果,有False與True。如果大于這個(gè)數(shù),那么就是Ture,否則是False。下面舉個(gè)例子,不再贅述。
1
2
3
4
5
|
a = np.array([ 1 , 2 , 3 , 4 ]) print (a> 2 ) #以下為輸出結(jié)果 [ False False True True ] |
之后的img[point] = 1說(shuō)明將所有True的值等于1。舉個(gè)例子
1
2
3
4
5
6
7
|
a = np.array([ 1 , 2 , 3 , 4 ]) p = a > 2 a[p] = 0 print (a) #以下為輸出結(jié)果 [ 1 2 0 0 ] |
因此我通過(guò)這樣的方法來(lái)清除掉了與數(shù)字顏色差別太大的背景噪音。
切割圖像
首先切割圖像的函數(shù)我寫(xiě)的是CutPicture。我們來(lái)說(shuō)一下這個(gè)切割圖像的意思。比如說(shuō)有一個(gè)人寫(xiě)字寫(xiě)的特別小,另一個(gè)人寫(xiě)字寫(xiě)的特別大。就像是下圖所示,所以我們進(jìn)行這樣的操作。沿著圖片的邊進(jìn)行切割,得到了下面切割后的圖片,讓數(shù)字占滿整個(gè)圖片,從而具有可比性。
所以下面貼出代碼,詳細(xì)解釋一下我是怎么做的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#Cut the Picture 切割圖象 def CutPicture(img): #初始化新大小 size = [] #圖片的行數(shù) length = len (img) #圖片的列數(shù) width = len (img[ 0 ,:]) #計(jì)算新大小 size.append(JudgeEdge(img, length, 0 , [ - 1 , - 1 ])) size.append(JudgeEdge(img, width, 1 , [ - 1 , - 1 ])) size = np.array(size).reshape( 4 ) print (size) return img[size[ 0 ]:size[ 1 ] + 1 , size[ 2 ]:size[ 3 ] + 1 ] |
首先函數(shù)導(dǎo)入過(guò)來(lái)的的參數(shù)只有一個(gè)原img。
我第一步做的是把新的大小初始化一下,size一共會(huì)放入四個(gè)值,第一個(gè)值代表原圖片上的手寫(xiě)數(shù)字圖案的最高行,第二個(gè)值代表的是最低行,第三個(gè)值代表數(shù)字圖案的最左列,,第四個(gè)只代表最右列**。這個(gè)還看不明白的話就看上面的圖示,就是沿著圖片切割一下就好了。
接下來(lái)的length和width分別代表著原圖片的行數(shù)與列數(shù),作用在下面。我又創(chuàng)建了一個(gè)JudgeEdge函數(shù),這個(gè)函數(shù)是輸出它的行數(shù)或者列數(shù)的兩位數(shù)字。第一個(gè)append是給size列表放入了兩個(gè)行序號(hào)(最高行和最低行),第二個(gè)append是給size放進(jìn)兩個(gè)列序號(hào)(最左列和最右列)。所以接下來(lái)就看JudgeEdge函數(shù)是干什么的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
def JudgeEdge(img, length, flag, size): for i in range (length): #Cow or Column 判斷是行是列 if flag = = 0 : #Positive sequence 正序判斷該行是否有手寫(xiě)數(shù)字 line1 = img[img[i,:]<color] #Negative sequence 倒序判斷該行是否有手寫(xiě)數(shù)字 line2 = img[img[length - 1 - i,:]<color] else : line1 = img[img[:,i]<color] line2 = img[img[:,length - 1 - i]<color] #If edge, recode serial number 若有手寫(xiě)數(shù)字,即到達(dá)邊界,記錄下行 if len (line1)> = 1 and size[ 0 ] = = - 1 : size[ 0 ] = i if len (line2)> = 1 and size[ 1 ] = = - 1 : size[ 1 ] = length - 1 - i #If get the both of edge, break 若上下邊界都得到,則跳出 if size[ 0 ]! = - 1 and size[ 1 ]! = - 1 : break return size |
JudgeEdge函數(shù)的參數(shù)flag就是用來(lái)判斷是行還是列,當(dāng)flag=0時(shí),說(shuō)明以行為基礎(chǔ)開(kāi)始循環(huán);當(dāng)flag=1時(shí)說(shuō)明以列為基礎(chǔ)進(jìn)行循環(huán)。所以參數(shù)length傳遞的時(shí)候就是行數(shù)和列數(shù)。接下來(lái)的for循環(huán)就是根據(jù)length的大小看似是循環(huán)。
當(dāng)是行時(shí),我這里用line1和line2起到的是一個(gè)指針的作用,即在第i行時(shí),line1的內(nèi)容就是這一行擁有非白色底(數(shù)值為1)的像素的個(gè)數(shù);line2的作用則是反序的,也就是他計(jì)算的是倒數(shù)i行非白色像素個(gè)數(shù),這樣做的目的是能夠快一點(diǎn),讓上下同時(shí)開(kāi)始進(jìn)行尋找,而不用line1把整個(gè)圖片循環(huán)一遍,line2把整個(gè)圖片循環(huán)一遍,大大節(jié)省了時(shí)間。
尋找這一行擁有非白色底的像素的個(gè)數(shù)這一個(gè)語(yǔ)句同樣的運(yùn)用了邏輯判斷,和上文中去噪的原理一模一樣。
當(dāng)line里面有數(shù)的時(shí)候,說(shuō)明已經(jīng)到達(dá)了手寫(xiě)數(shù)字的邊緣。這時(shí)候就記錄下來(lái),然后就不再改變。當(dāng)兩個(gè)line都不等于初始值-1時(shí),說(shuō)明已經(jīng)找到了兩個(gè)邊緣,這時(shí)候就可以跳出循環(huán)并且return了。
這個(gè)函數(shù)就是這樣。所以說(shuō)切割圖像的完整代碼就是這樣子,下面就要把切割的大小不一的圖像給拉伸成標(biāo)準(zhǔn)大小100x100了。
拉伸圖像
因?yàn)榍懈钜院蟮膱D像有大有小,一張圖片的大小可能是21x39(比如說(shuō)數(shù)字2),另一張可能是4x40(比如說(shuō)數(shù)字1)。所以為了能夠讓他們統(tǒng)一大小稱為100x100,我們就要把他們拉伸一下。
大概就是像圖上的一樣。實(shí)際情況的圖案可能會(huì)更復(fù)雜,所以我們下面展示一下代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#Stretch the Picture 拉伸圖像 def StretchPicture(img): newImg = np.ones(N * * 2 ).reshape(N, N) ##Stretch/Compress each cows/columns 對(duì)每一行/列進(jìn)行拉伸/壓縮 #The length of each cows after stretching 每一行拉伸/壓縮的步長(zhǎng) step1 = len (img[ 0 ]) / 100 #Each columns blabla 每一列拉伸/壓縮的步長(zhǎng) step2 = len (img) / 100 #Operate on each cows 對(duì)每一行進(jìn)行操作 for i in range ( len (img)): for j in range (N): newImg[i, j] = img[i, int (np.floor(j * step1))] #Operate on each columns 對(duì)每一列進(jìn)行操作 for i in range ( len (img[ 0 ])): for j in range (N): newImg[j, i] = img[ int (np.floor(j * step2)), i] return newImg |
首先初始化一個(gè)新的圖片矩陣,這個(gè)大小就是標(biāo)準(zhǔn)大小100x100。接下來(lái)才是重頭戲。我這里用的方法是比較簡(jiǎn)單基礎(chǔ)的方法,但是可能依舊比較難。
首先定義兩個(gè)步長(zhǎng)step1和step2,分別代表拉伸/壓縮行與列時(shí)的步長(zhǎng)。這里的原理就是把原來(lái)的長(zhǎng)度給他平均分成100份,然后將這100個(gè)像素點(diǎn)分別對(duì)應(yīng)上原本的像素點(diǎn)。
如下圖所示,圖像1我們假設(shè)為原圖像,圖像2我們假設(shè)為標(biāo)準(zhǔn)圖像,我們需要把圖像1轉(zhuǎn)化為圖像2,其中每一個(gè)點(diǎn)代表一個(gè)像素點(diǎn),也就是圖像1有五個(gè)像素點(diǎn),圖像2有四個(gè)像素點(diǎn)。
所以我的思想就是直接讓圖像2的像素點(diǎn)的值等于距離它最近的圖像1的像素點(diǎn)。
我們?yōu)榱朔奖闫鹨?jiàn),在這里定義一個(gè)語(yǔ)法:圖像2第三個(gè)數(shù)據(jù)點(diǎn)我們可以寫(xiě)為2_3.
所以2_1對(duì)應(yīng)的就是1_1,2_2對(duì)應(yīng)的就是1_2,2_3對(duì)應(yīng)的是1_4,2_4對(duì)應(yīng)的是1_5。就這樣我們就能夠得到了圖像2所有的數(shù)據(jù)點(diǎn)。
利用數(shù)學(xué)的形式表現(xiàn)出來(lái),就是假設(shè)圖像1長(zhǎng)度為l_1,圖像2長(zhǎng)度為l_2,所以令圖像2的步長(zhǎng)為l_1/l_2,也就是說(shuō)當(dāng)圖像2的第一個(gè)像素點(diǎn)對(duì)應(yīng)圖像1第一個(gè)像素點(diǎn),圖像2的最后一個(gè)像素點(diǎn)對(duì)應(yīng)圖像1最后一個(gè)像素點(diǎn)。然后圖像2第二個(gè)像素點(diǎn)位置就是2*l_1/l_2,對(duì)應(yīng)圖像1第floor(2*l_1/l_2)個(gè)像素點(diǎn)。以此類推就行。因此再回頭看一下那一段代碼,這一段是不是就好理解了?
之后對(duì)行與列分別進(jìn)行這個(gè)操作,所以就可以得到標(biāo)準(zhǔn)的圖片大小。然后再返回到GetTrainPicture即可。
再GetTrainPicture函數(shù)中,我用了reshape函數(shù)把原本100x100大小的圖片拉伸成為1x10000大小的向量,然后存入矩陣當(dāng)中,并將這一張圖片的類別存入矩陣最后一個(gè)。
以上就是圖片處理的所有內(nèi)容。
小結(jié)
以上就是把一張圖片經(jīng)過(guò)處理后存入矩陣的內(nèi)容。
本文中的所有算法、代碼均是我自己構(gòu)思的,所以可能存在一些不足之處,我沒(méi)有系統(tǒng)的學(xué)習(xí)過(guò)圖像的相關(guān)知識(shí),也并不是計(jì)算機(jī)專業(yè),因此可能在理論上有一些不合乎情況,所以如果有錯(cuò)誤的話歡迎一起討論,謝謝。
原文鏈接:http://blog.csdn.net/hanpu_liang/article/details/78237913