1. 程式人生 > 其它 >Python實現識別手寫數字 Python圖片讀入與處理

Python實現識別手寫數字 Python圖片讀入與處理

寫在前面

在上一篇文章 Python徒手實現手寫數字識別―大綱 中,我們已經講過了我們想要寫的全部思路,所以我們不再說全部的思路。

我這一次將圖片的讀入與處理的程式碼寫了一下,和大綱寫的過程一樣,這一段程式碼分為以下幾個部分:

  • 讀入圖片;

  • 將圖片讀取為灰度值矩陣;

  • 圖片背景去噪;

  • 切割圖片,得到手寫數字的最小矩陣;

  • 拉伸/壓縮圖片,得到標準大小為100x100大小矩陣;

  • 將圖片拉為1x10000大小向量,存入訓練矩陣中。

所以下面將會對這幾個函式進行詳解。

程式碼分析

基礎內容

首先我們現在最前面定義基礎變數


import os
from skimage import io
import numpy as np

##Essential vavriable 基礎變數
#Standard size 標準大小
N = 100
#Gray threshold 灰度閾值
color = 100/255

其中標準大小指的是我們在最後經過切割、拉伸後得到的圖片的尺寸為NxN。灰度閾值指的是在某個點上的灰度超過閾值後則變為1.

接下來是這影象處理的一部分的主函式


filenames = os.listdir(r"./num/")
pic = GetTrainPicture(filenames)

其中filenames得到在num目錄下所有檔案的名稱組成的列表。pic則是通過函式GetTrainPicture得到所有訓練影象向量的矩陣。這一篇文章主要就是圍繞這個函式進行講解。

GetTrainPicture函式

GetTrainPicture函式內容如下


#Read and save train picture 讀取訓練圖片並儲存
def GetTrainPicture(files):
Picture = np.zeros([len(files), N**2+1])
#loop all pictures 迴圈所有圖片檔案
for i, item in enumerate(files):
#Read the picture and turn RGB to grey 讀取這個圖片並轉為灰度值
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
#將圖片進行切割,得到有手寫數字的的影象
img = CutPicture(img)
#Stretch the picture and get the standard size 100x100
#將圖片進行拉伸,得到標準大小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

可以看出這個函式的資訊量非常大,基本上今天做的所有步驟我都把封裝到一個個函式裡面了,所以這裡我們可以看到圖片處理的所有步驟都在這裡。

提前準備

首先是建立了一個用來存放所有影象向量的矩陣Picture,大小為fx10001,其中f代表我們擁有的訓練圖片的數目,10001的前10000位代表圖片展開後的向量長度,最後一維代表這一個向量的類別,比如說時2就代表這個圖片上面寫的數字是2.

接下來用的是一個for迴圈,將files裡面每一個圖片進行一次迭代,計算出向量後存入picture。

在迴圈中的內容就是對每一張圖片進行的操作。

讀入圖片並清除背景噪音

首先是io.imread函式,這個函式是將圖片匯出成為灰度值的矩陣,每一個畫素點是矩陣上的一個元素。

接下來是img[img>color]=1這一句。這一句運用了邏輯運算的技巧,我們可以將其分為兩部分


point = img > color
img[point] = 1

首先是img>color,img是一個矩陣,color是一個數。意義就是對img中所有元素進行判斷是否大於color這個數,並輸出一個與img同等大小的矩陣,對應元素上是該值與color判斷後的結果,有False與True。如果大於這個數,那麼就是Ture,否則是False。下面舉個例子,不再贅述。


a = np.array([1, 2, 3, 4])
print(a>2)

#以下為輸出結果
[False False True True]

之後的img[point] = 1說明將所有True的值等於1。舉個例子


a = np.array([1, 2, 3, 4])
p = a > 2
a[p] = 0
print(a)

#以下為輸出結果
[1 2 0 0]

因此我通過這樣的方法來清除掉了與數字顏色差別太大的背景噪音。

切割影象

首先切割影象的函式我寫的是CutPicture。我們來說一下這個切割影象的意思。比如說有一個人寫字寫的特別小,另一個人寫字寫的特別大。就像是下圖所示,所以我們進行這樣的操作。沿著圖片的邊進行切割,得到了下面切割後的圖片,讓數字佔滿整個圖片,從而具有可比性。

所以下面貼出程式碼,詳細解釋一下我是怎麼做的。


#Cut the Picture 切割圖象
def CutPicture(img):
#初始化新大小
size = []
#圖片的行數
length = len(img)
#圖片的列數
width = len(img[0,:])
#計算新大小
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]

首先函式匯入過來的的引數只有一個原img。

我第一步做的是把新的大小初始化一下,size一共會放入四個值,第一個值代表原圖片上的手寫數字圖案的最高行,第二個值代表的是最低行,第三個值代表數字圖案的最左列,,第四個只代表最右列**。這個還看不明白的話就看上面的圖示,就是沿著圖片切割一下就好了。

接下來的length和width分別代表著原圖片的行數與列數,作用在下面。我又建立了一個JudgeEdge函式,這個函式是輸出它的行數或者列數的兩位數字。第一個append是給size列表放入了兩個行序號(最高行和最低行),第二個append是給size放進兩個列序號(最左列和最右列)。所以接下來就看JudgeEdge函式是幹什麼的。


def JudgeEdge(img, length, flag, size):
for i in range(length):
#Cow or Column 判斷是行是列
if flag == 0:
#Positive sequence 正序判斷該行是否有手寫數字
line1 = img[img[i,:]<color]
#Negative sequence 倒序判斷該行是否有手寫數字
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 若有手寫數字,即到達邊界,記錄下行
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函式的引數flag就是用來判斷是行還是列,當flag=0時,說明以行為基礎開始迴圈;當flag=1時說明以列為基礎進行迴圈。所以引數length傳遞的時候就是行數和列數。接下來的for迴圈就是根據length的大小看似是迴圈。

當是行時,我這裡用line1和line2起到的是一個指標的作用,即在第i行時,line1的內容就是這一行擁有非白色底(數值為1)的畫素的個數;line2的作用則是反序的,也就是他計算的是倒數i行非白色畫素個數,這樣做的目的是能夠快一點,讓上下同時開始進行尋找,而不用line1把整個圖片迴圈一遍,line2把整個圖片迴圈一遍,大大節省了時間。

尋找這一行擁有非白色底的畫素的個數這一個語句同樣的運用了邏輯判斷,和上文中去噪的原理一模一樣。

當line裡面有數的時候,說明已經到達了手寫數字的邊緣。這時候就記錄下來,然後就不再改變。當兩個line都不等於初始值-1時,說明已經找到了兩個邊緣,這時候就可以跳出迴圈並且return了。

這個函式就是這樣。所以說切割影象的完整程式碼就是這樣子,下面就要把切割的大小不一的影象給拉伸成標準大小100x100了。

拉伸影象

因為切割以後的影象有大有小,一張圖片的大小可能是21x39(比如說數字2),另一張可能是4x40(比如說數字1)。所以為了能夠讓他們統一大小稱為100x100,我們就要把他們拉伸一下。

大概就是像圖上的一樣。實際情況的圖案可能會更復雜,所以我們下面展示一下程式碼

    #Stretch the Picture 拉伸影象
    def StretchPicture(img):
     newImg = np.ones(N**2).reshape(N, N)
     ##Stretch/Compress each cows/columns 對每一行/列進行拉伸/壓縮
     #The length of each cows after stretching 每一行拉伸/壓縮的步長
     step1 = len(img[0])/100
     #Each columns blabla 每一列拉伸/壓縮的步長
     step2 = len(img)/100
     #Operate on each cows 對每一行進行操作
     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 對每一列進行操作
     for i in range(len(img[0])):
     for j in range(N):
      newImg[j, i] = img[int(np.floor(j*step2)), i]
     return newImg
    

首先初始化一個新的圖片矩陣,這個大小就是標準大小100x100。接下來才是重頭戲。我這裡用的方法是比較簡單基礎的方法,但是可能依舊比較難。

首先定義兩個步長step1和step2,分別代表拉伸/壓縮行與列時的步長。這裡的原理就是把原來的長度給他平均分成100份,然後將這100個畫素點分別對應上原本的畫素點。

如下圖所示,影象1我們假設為原影象,影象2我們假設為標準影象,我們需要把影象1轉化為影象2,其中每一個點代表一個畫素點,也就是影象1有五個畫素點,影象2有四個畫素點。

所以我的思想就是直接讓影象2的畫素點的值等於距離它最近的影象1的畫素點。

我們為了方便起見,在這裡定義一個語法:影象2第三個資料點我們可以寫為2_3.

所以2_1對應的就是1_1,2_2對應的就是1_2,2_3對應的是1_4,2_4對應的是1_5。就這樣我們就能夠得到了影象2所有的資料點。

利用數學的形式表現出來,就是假設影象1長度為l_1,影象2長度為l_2,所以令影象2的步長為l_1/l_2,也就是說當影象2的第一個畫素點對應影象1第一個畫素點,影象2的最後一個畫素點對應影象1最後一個畫素點。然後影象2第二個畫素點位置就是2l_1/l_2,對應影象1第floor(2l_1/l_2)個畫素點。以此類推就行。因此再回頭看一下那一段程式碼,這一段是不是就好理解了?

之後對行與列分別進行這個操作,所以就可以得到標準的圖片大小。然後再返回到GetTrainPicture即可。

再GetTrainPicture函式中,我用了reshape函式把原本100x100大小的圖片拉伸成為1x10000大小的向量,然後存入矩陣當中,並將這一張圖片的類別存入矩陣最後一個。

以上就是圖片處理的所有內容。