全棧AI工程師指南,DIY一個識別手寫數字的web應用

AI科技大本營發表於2019-02-11

640?wx_fmt=jpeg


作者 | shadow chi

本文經授權轉載自 無界社群mixlab(ID:mix-lab)


網上大量教程都是教如何訓練模型,

往往我們只學會了訓練模型,

而實際應用的環節是缺失的。


def AIFullstack( ):


本文從「全棧」的角度,通過訓練模型、部署成後端服務、前端頁面開發等內容的介紹,幫大家更快地把深度學習的模型應用到實際場景中。


用到的技術:

  • keras+tensorflow+flask

  • web開發相關


指南分為5篇。


第一篇

介紹開發環境--訓練模型--儲存至本地;


第二篇

介紹匯入訓練好的模型--識別任意的手寫數字圖片;


第三篇

介紹用Flask整合keras訓練好的模型,並開發後端服務;


第四篇

介紹前端web單頁應用的開發。


第五篇

介紹影像處理相關知識。


return 學以致用



第一篇

介紹開發環境--訓練模型--儲存至本地


為了方便入門,下面採用docker的方式進行實驗。


01/01

採用docker部署開發環境


首先安裝好docker,本指南使用的是mac系統,window使用者請查閱官方安裝教程,執行docker,終端輸入:


docker pull floydhub/dl-docker:cpu


在本地電腦新建一個目錄,我這邊是kerasStudy,路徑是


/Users/shadow/Documents/02-coding/kerasStudy


大家可以改成自己本機對應的路徑。


終端執行:


docker run -it  -p 6006:6006 -p 8888:8888 -v /Users/shadow/Documents/02-coding/kerasStudy:/root/kerasStudy floydhub/dl-docker:cpu bash


-p 6006:6006,表示將Docker主機的6006埠與容器的6006介面繫結;


-v 引數中,冒號":"前面的目錄是宿主機目錄,後面的目錄是容器內目錄。


記得,還需要在docker中配置宿主機的與映象共享的目錄地址



640?wx_fmt=png


將新建一個容器,並在容器中開啟一個互動模式的終端,結果如下:


640?wx_fmt=jpeg



01/02 

啟動jupyter notebook


終端輸入:


mkdir  $HOME/.keras/

cd $HOME/.keras/

vim keras.json


鍵盤按 i ,按回車及方向鍵控制游標,把floydhub/dl-docker:cpu映象預設的使用Theano作為後端,改為如下:


{
    "image_data_format""channels_last",
    "epsilon"1e-07,
    "floatx""float32",
    "backend""tensorflow"
}


按下esc鍵;輸入:wq,儲存修改結果。


終端輸入:


jupyter notebook


顯示jupyter notebook已經執行成功,如下圖:

640?wx_fmt=png


開啟瀏覽器,在位址列中輸入:


localhost:8888


即可訪問jupyter,如下圖:


640?wx_fmt=png


01/03 

Hello Jupyter Notebook


上文提到的jupyter notebook到底是什麼東西?


Jupyter Notebook 是一款集程式設計和寫作於一體的效率工具,優點:

  • 分享便捷

  • 遠端執行

  • 互動式展現


在瀏覽器可以訪問Jupyter Notebook,也就是說,我可以部署成web應用的形式,使用者可以分享,通過域名訪問,並且可以利用web的任何互動方式。


繼續我們的教程,在瀏覽器開啟Jupyter Notebook後,找到我們與本地共享的專案目錄kerasStudy,點選進入,然後點選jupyter右上角的new,選擇python2,如下圖所示:


640?wx_fmt=png


新建一個notebook。


先來做個小實驗:

輸入:


import numpy as np

np.random.seed(1337)

np.random.rand(5)


然後在選單中,選擇Cell--Run Cells,執行程式碼:


640?wx_fmt=png


如下圖所示,輸出了一些結果:


640?wx_fmt=png


第一行程式碼:


import numpy as np


引入 numpy ,一個用python實現的科學計算包。提供了許多高階的數值程式設計工具,如:矩陣資料型別、向量處理,以及精密的運算庫。專為進行嚴格的數字處理而產生。numpy的教程可以參看官網http://www.numpy.org/


np.random.seed()


使得隨機資料可預測。相當於給隨機數賦了個id,下次呼叫隨機數的時候,只要再次取這個id,再呼叫隨機數,即可產生相同的隨機數


可以做下這個練習:


練習1

np.random.seed(0)

np.random.rand(5)


#控制檯輸出結果(隨機生成,每次生成的都不一樣)

 

array([ 0.5488135 ,  0.71518937,  0.60276338,  0.54488318,  0.4236548 ])


練習2

np.random.seed(1676)

np.random.rand(5)


#控制檯輸出結果 


array([ 0.39983389,  0.29426895,  0.89541728,  0.71807369,  0.3531823 ])


練習3

np.random.seed(1676)

np.random.rand(5)


#控制檯輸出結果


array([ 0.39983389,  0.29426895,  0.89541728,  0.71807369,  0.3531823 ])  

  



01/04 

Keras訓練模型


這裡結合keras的官方案例,訓練一個多層感知器。


步驟1

重新建一個notebook,

輸入:

from __future__ import print_function

'''

 Python 3.x引入了一些與Python 2不相容的關鍵字和特性,在Python 2中,
 可以通過內建的__future__模組匯入這些新內容。


 如果你希望在Python 2環境下寫的程式碼也可以在Python 3.x中執行,那麼建議使用__future__模組。


 import print_function 這裡使用3.x的 print方法


 在Python 3中必須用括號將需要輸出的物件括起來。
 在Python 2中使用額外的括號也是可以的。
 但反過來在Python 3中想以Python2的形式不帶括號呼叫print函式時,
 會觸發SyntaxError。


'''


import numpy as np

 


步驟2

from keras.datasets import mnist

# 匯入mnist資料庫, mnist是常用的手寫數字庫

from keras.models import Sequential

#匯入序貫模型,Sequential是多個網路層的線性堆疊,也就是“一條路走到黑”。

from keras.layers.core import Dense, Dropout, Activation

#匯入全連線層Dense,啟用層Activation 以及 Dropout層

from keras.optimizers import SGD, Adam, RMSprop

#匯入優化器 SGD, Adam, RMSprop

from keras.utils import np_utils

#匯入numpy工具,主要是用to_categorical來轉換類別向量

 


步驟3

#變數初始化

batch_size = 128

#設定batch的大小

nb_classes = 10

#設定類別的個數

nb_epoch = 20

#設定迭代的次數



步驟4

#準備資料 

(X_train, y_train), (X_test, y_test) = mnist.load_data()

print(X_test.shape,X_test[0])

#keras中的mnist資料集已經被劃分成了60,000個訓練集,10,000個測試集的形式,按以上格式呼叫即可

X_train = X_train.reshape(60000784)

#X_train原本是一個60000*28*28的三維向量,將其轉換為60000*784的二維向量

X_test = X_test.reshape(10000784)

print(X_test.shape)

#X_test原本是一個10000*28*28的三維向量,將其轉換為10000*784的二維向量

X_train = X_train.astype('float32')

X_test = X_test.astype('float32')

#將X_train, X_test的資料格式轉為float32儲存

X_train /= 255

X_test /= 255

#歸一化

print(X_train.shape[0], 'train samples')

print(X_test.shape[0], 'test samples')



步驟5

# 轉換類標號 convert class vectors to binary class matrices

Y_train = np_utils.to_categorical(y_train, nb_classes)

Y_test = np_utils.to_categorical(y_test, nb_classes)



步驟6

#建立模型 使用Sequential()

'''

模型需要知道輸入資料的shape,

因此,Sequential的第一層需要接受一個關於輸入資料shape的引數,

後面的各個層則可以自動推匯出中間資料的shape,

因此不需要為每個層都指定這個引數

'''


model = Sequential()

model.add(Dense(512, input_shape=(784,)))

model.add(Activation('relu'))

model.add(Dropout(0.2))

# 輸入層有784個神經元

# 第一個隱層有512個神經元,啟用函式為ReLu,Dropout比例為0.2

model.add(Dense(512))

model.add(Activation('relu'))

model.add(Dropout(0.2))

# 第二個隱層有512個神經元,啟用函式為ReLu,Dropout比例為0.2

model.add(Dense(10))

model.add(Activation('softmax'))

# 輸出層有10個神經元,啟用函式為SoftMax,得到分類結果

# 輸出模型的整體資訊

# 總共引數數量為784*512+512 + 512*512+512 + 512*10+10 = 669706

model.summary()



步驟7

#列印模型

model.summary()



步驟8

#配置模型的學習過程

'''

compile接收三個引數:

1.優化器optimizer:引數可指定為已預定義的優化器名,如rmsprop、adagrad,

或一個Optimizer類物件,如此處的RMSprop()

2.損失函式loss:引數為模型試圖最小化的目標函式,可為預定義的損失函式,

如categorical_crossentropy、mse,也可以為一個損失函式

3.指標列表:對於分類問題,一般將該列表設定為metrics=['accuracy']

'''


model.compile(loss='categorical_crossentropy',

              optimizer=RMSprop(),

              metrics=['accuracy'])


步驟9

#訓練模型

'''

batch_size:指定梯度下降時每個batch包含的樣本數

nb_epoch:訓練的輪數,nb指number of

verbose:日誌顯示,0為不在標準輸出流輸出日誌資訊,1為輸出進度條記錄,2為epoch輸出一行記錄

validation_data:指定驗證集

fit函式返回一個History的物件,其History.history屬性記錄了損失函式和其他指標的數值隨epoch變化的情況,

如果有驗證集的話,也包含了驗證集的這些指標變化情況

'''


history = model.fit(X_train, Y_train,

                    batch_size=batch_size, nb_epoch=nb_epoch,

                    verbose=2, validation_data=(X_test, Y_test))



這個時候,可以點選選單欄 Cell--Run All 一下,可以看到訓練過程,如下圖:


640?wx_fmt=png


步驟10

#模型評估

score = model.evaluate(X_test, Y_test, verbose=0)

print('Test score:', score[0])

print('Test accuracy:', score[1])


步驟11

#儲存神經網路的結構與訓練好的引數

json_string = model.to_json()  

open('my_model_architecture.json','w').write(json_string)    

model.save_weights('my_model_weights.h5')




第二篇

介紹匯入訓練好的模型--識別任意的手寫數字圖片



02/01

再次進入docker容器


接著上一篇,我們繼續使用上次新建好的容器,可以終端輸入 :


docker ps -a


顯示如下圖,找到上次run的容器:


640?wx_fmt=png


我這邊是容器名(NAMES)為suspicious_cori,啟動它,可以終端輸入:


docker start  suspicious_cori


然後,終端再輸入:


docker exec -i -t  suspicious_cori bash


即可在容器中開啟一個互動模式的終端。


然後終端輸入


jupyter notebook


新建一個notebook


02/02

載入訓練好的模型


載入上一篇訓練好的模型,在新建的notebook裡輸入:


from keras.models import model_from_json

model=model_from_json(open('my_model_architecture.json').read())  

model.load_weights('my_model_weights.h5')



02/03

讀取需要識別的手寫字圖片


引入用於讀取圖片的庫:


import matplotlib.image as mpimg


讀取位於kerasStudy目錄下的圖片:


img = mpimg.imread('test.png'


matplotlib只支援PNG影像,讀取和程式碼處於同一目錄下的 test.png ,注意,讀取後的img 就已經是一個 np.array 了,並且已經歸一化處理。


上文的png圖片是單通道圖片(灰度),如果test.png是rgb通道的圖片,可以rgb2gray進行轉化,程式碼如下:


def rgb2gray(rgb):

    return np.dot(rgb[...,:3], [0.2990.5870.114])

img = rgb2gray(img)

 

關於圖片的通道,我們可以在photoshop裡直觀的檢視:


640?wx_fmt=png


先檢視下讀取的圖片陣列維度:


print(img.shape)


輸出是(28, 28)


轉化成正確的輸入格式:


img = img.reshape(1784)


列印出來看看:


print(img.shape)


輸出是(1, 784)



02/04

識別的手寫字圖片


輸入:

pre=model.predict_classes(img)   


列印出來即可:


print(pre)  


識別出來是6:


1/1 [==========================] - 0s
[6]  



至此,你已經學會了從訓練模型到使用模型進行識別任務的全過程啦。

有興趣可以試著替換其他的手寫字圖片進行識別看看。

當然也可以寫個後端服務,部署成web應用。




第三篇

介紹用Flask整合keras訓練好的模型,並開發後端服務



03/01   

目錄結構


新建一個web全棧專案的資料夾,我在kerasStudy下建了個app的資料夾,app下的檔案構成如下:


640?wx_fmt=png


app.py是專案的主入口,主要是用flask寫的一些路由;

predict.py是識別手寫字的python模組;

static是放置前端頁面的目錄;

model存放訓練好的模型;

test是一些測試圖片;

tmp是前端上傳到伺服器的圖片存放地址。



03/02   

前端程式碼


新建一個簡單的index.html檔案,放置於static目錄下,寫一個form表單:


<form 
action="./predict" 
method="post" 
enctype="multipart/form-data">


upload:
<input 
type="file" 
name="predictImg">


<input 
type="submit" 
name="upload">


</form> 


這裡的前端程式碼比較簡單,只是一個把手寫字圖片提交到伺服器的表單,下一篇文章將實現一個手寫字的輸入工具。



03/03   

後端程式碼


app.py裡,用flask設定路由,返回靜態html頁面:


@app.route('/')
def hello_world():
    return app.send_static_file('index.html')


其餘flask的相關配置程式碼可以參考往期文章:

用Flask寫後端介面


這個時候,我們啟動docker,把映象啟動,並進入docker映象的終端中(檢視第2篇),找到app目錄,終端輸入:


python app.py


等終端提示相關的啟動資訊後,在瀏覽器裡試下,輸入:


http://localhost:8888/


成功開啟index.html頁面:


640?wx_fmt=jpeg


再次編輯app.py檔案,寫一個predict的介面,接受前端提交的圖片,並返回識別結果給前端:


@app.route('/predict',methods=["POST"])
def predictFromImg():
    if request.method=="POST":
        predictImg=request.files["predictImg"]
        
        predictImg.save(
                os.path.join(
                app.config["UPLOAD_FOLDER"],
                predictImg.filename))

        imgurl='./tmp/'+predictImg.filename
        
        result=predict.img2class(imgurl)
        
        print(result)
        
        return '<h1>Hello~~~:%s</h1>' % result


其中predict.img2class(imgurl)是一個python模組。


接下來,我們編寫識別手寫字的python模組。



03/04   

編寫識別手寫字的python模組


在Python中,每個Python檔案都可以作為一個模組,模組的名字就是檔案的名字。比如有這樣一個檔案test.py,在test.py中定義了函式add:


#test.py

def add(a,b): 
    return a+b


那麼在其他檔案中就可以先import test,然後通過test.add(a,b)來呼叫了,當然也可以通過from test import add來引入。


回到本篇的例子,我們在第2篇中已經寫過識別手寫字的程式碼了,現在只需稍微調整下就可以形成一個python模組,供其他檔案呼叫了。


如本篇中,在app.py中通過:


import predict 


引入predict.py模組,使用的時候呼叫:


predict.img2class(imgurl)


#predict.py檔案

詳情可以參考第2篇內容


這邊把上次實現過的程式碼,書寫出一個python模組,以供其他檔案呼叫:


import matplotlib.image as mpimg
import numpy as np
from keras.models import model_from_json

model=model_from_json(open('./model/my_model_architecture.json').read())  

model.load_weights('./model/my_model_weights.h5')


def rgb2gray(rgb):
    return np.dot(rgb[...,:3], [0.2990.5870.114])


def img2class(imgFile): 

    img = mpimg.imread(imgFile) 
    print(img.shape)
    img = rgb2gray(img)

    print(img.shape)
    img = img.reshape(1784)

    pre=model.predict_classes(img)   
    result=pre[0]

    return result


在docker映象中啟動偽終端,進入app目錄,輸入:


python app.py


上傳測試圖片試試:


640?wx_fmt=png

成功返回識別結果,至此,一個迷你的識別手寫字web全棧應用已經完成。




第四篇

介紹前端web單頁應用的開發


如果你練習裡前面三篇,相信你已經熟悉了Docker和Keras,以及Flask了,接下來我們實現一個提供給使用者輸入手寫字的前端web頁面。


前端畫板我們可以自己用最基本的canvas寫,也可以選擇封裝好的開源庫:

下面介紹2個比較好的模擬手寫效果的畫板庫:


signature_pad

https://github.com/szimek/signature_pad/


drawingboard.js

https://github.com/Leimi/drawingboard.js


這邊我選擇的是signature_pad。

HTML程式碼:

<!doctype html>
<html lang="zh"><head>  
<meta charset="utf-8">  
<title>mnist demo</title>  
<meta name="viewport" content="width=device-width,initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">  
<link rel="stylesheet" href="./static/css/main.css"></head>

<body onselectstart="return false">
 
<div id="mnist-pad">

<div class="mnist-pad-body"><canvas></canvas>
</div>
   
<div class="mnist-pad-footer">

<div class="mnist-pad-result">
    <h5>識別結果:</h5>     <h5 id="mnist-pad-result"></h5></div>

 
<div class="mnist-pad-actions">   <button type="button" id="mnist-pad-clear">清除</button>   <button type="button" id="mnist-pad-save">識別</button>
</div>

</div></div>
 
<script src="./static/js/signature_pad.js"></script>
<script src="./static/js/mnist.js"></script>  
<script src="./static/js/app.js"></script>

</body>

</html>

移動端注意要寫這句標籤,把螢幕縮放設為no,比例設為1:

<meta name="viewport" content="width=device-width,initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">


CSS程式碼:

body {  display: flex;  justify-content: center;  align-items: center;  height: 100vh;  width: 100%;  user-select: none;  margin: 0;  padding: 0; }
h5 {  margin: 0;  padding: 0

}
#mnist-pad {  position: relative;  display: flex;  flex-direction: column;  font-size: 1em;  width: 100%;  height: 100%;  background-color: #fff;  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.08) inset;  padding: 16px; }
.mnist-pad-body {  position: relative;  flex: 1;  border: 1px solid #f4f4f4; }
.mnist-pad-body canvas {  position: absolute;  left: 0;  top: 0;  width: 100%;  height: 100%;  border-radius: 4px;  box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset; }
.mnist-pad-footer {  color: #C3C3C3;  font-size: 1.2em;  margin-top: 8px;  margin-bottom: 8px; }
.mnist-pad-result {  display: flex;  justify-content: center;  align-items: center;  margin-bottom: 8px; }
.mnist-pad-actions {  display: flex;  justify-content: space-between;  margin-bottom: 8px; }
#mnist-pad-clear {  height: 44px;  background-color: #eeeeee;  width: 98px;  border: none;  font-size: 16px;  color: #4a4a4a; }
#mnist-pad-save {  height: 44px;  background-color: #3b3b3b;  width: 98px;  border: none;  font-size: 16px;  color: #ffffff; }

CSS樣式都是一些常用的,有興趣可以自己實現個簡單的UI。


JS程式碼,有3個檔案:

signature_pad.js 這是引用的開源庫;

mnist.js 這是我們給開源庫寫的一些擴充套件,下文會介紹;

app.js主要是一些初始化,事件繫結,請求後端介面的處理。


先來看看app.js:


步驟1 

初始化畫板,繫結按鈕事件;

var clearBtn = document.getElementById("mnist-pad-clear");
var saveBtn = document.getElementById("mnist-pad-save");

var
canvas = document.querySelector("canvas");

var
mnistPad = new SignaturePad(canvas, {
   backgroundColor: 'transparent',
   minWidth: 6,
   maxWidth: 8
});
   
clearBtn.addEventListener("click", function (event) {  mnistPad.clear(); });

saveBtn.addEventListener("click", function (event) {
     if (mnistPad.isEmpty()) {        alert("請書寫一個數字");    } else {         mnistPad.getMNISTGridBySize(true,28,img2text);    } });

注意minWidth及MaxWidth的設定,我試驗下來,比較好的數值是6跟8,識別效果較好,也可以自行試驗修改。


ministPad的方法,getMNISTGridBySize將把擷取畫板上的手寫數字,並縮放成28x28的尺寸,然後呼叫img2text函式。


img2text主要是把28x28的圖片傳給後端,獲取識別結果,這邊由於canvas的資料是base64,需要用到轉化為blob的函式,dataURItoBlob(github上有寫好的),轉化後通過構造一個表單,注意檔名predictImg一定要與後端flask接受函式裡的寫的一致。呼叫XMLHttpRequest請求後端介面即可。


步驟2

這一步“如何把canvas生成的圖片上傳至後端”是個很典型的問題。

function img2text(b64img){

 var formData = new FormData();
 var blob = dataURItoBlob(b64img);
   formData.append("predictImg", blob);
   var request = new XMLHttpRequest();
     request.onreadystatechange = function () {
      if (request.readyState == 4) {
         if ((request.status >= 200 && request.status < 300) || request.status == 304) {
         console.log(request.response)
         document.querySelector('#mnist-pad-result').innerHTML=request.response;            };        }    };  request.open("POST", "./predict");  request.send(formData); };


步驟3 

還有一個比較重要的函式:

畫板根據螢幕尺寸自適應的程式碼(尤其是PC端,記得加):

function resizeCanvas() {
 var ratio =  Math.max(window.devicePixelRatio || 1, 1);  canvas.width = canvas.offsetWidth;  canvas.height = canvas.offsetHeight;
//  canvas.getContext("2d").scale(ratio, ratio);  mnistPad.clear(); };
window.onresize = resizeCanvas; resizeCanvas();


到這一步可以試一下前端的輸入效果先:


640?wx_fmt=png


接下來完成mnist.js


步驟4

signature_pad有個方法是toData,可以獲取所有手寫輸入的座標點。

var ps=mnistPad.toData()[0];
mnistPad._ctx.strokeStyle='red';
ps.forEach((p,i)=>{    mnistPad._ctx.beginPath();    mnistPad._ctx.arc(p.x, p.y, 4, 0, 2 * Math.PI);    mnistPad._ctx.stroke(); })


我們可以在chrome的控制檯直接試驗。


640?wx_fmt=png


紅色的圈圈就是所有的座標點,只要求出如下圖所示的紫色框,第一步也就完成了。

640?wx_fmt=jpeg


步驟5

給signature_pad擴充套件個getArea方法:

SignaturePad.prototype.getArea = function() {
var xs = [],    ys = [];

var
orign = this.toData();

for
(var i = 0; i < orign.length; i++) {
   var orignChild = orign[i];
   for (var j = 0; j < orignChild.length; j++) {        xs.push(orignChild[j].x);        ys.push(orignChild[j].y);      } };

var paddingNum = 30;
var min_x = Math.min.apply(null, xs) - paddingNum;
var min_y = Math.min.apply(null, ys) - paddingNum;
var max_x = Math.max.apply(null, xs) + paddingNum;
var max_y = Math.max.apply(null, ys) + paddingNum;
  
var width = max_x - min_x,      height = max_y - min_y;
  
var grid = {      x: min_x,      y: min_y,      w: width,      h: height    };
  
   return grid;
    };

測試下:


640?wx_fmt=png



注意paddingNum,我設定了個30的值,把邊框稍微放大了下,原因見mnist手寫字訓練集的圖片就知道啦。


到這一步,我們的手寫字資料集是下圖這樣的:


640?wx_fmt=png


步驟6 

我們還需要把邊框變成方形。

再寫個轉換函式:

SignaturePad.prototype.change2grid = function(area) {
var w = area.w,      h = area.h,      x = area.x,      y = area.y;
var xc = x,      yc = y,      wc = w,      hc = h;

if (h >= w) {      xc = x - (h - w) * 0.5;      wc = h;    } else {      yc = y - (w - h) * 0.5;      hc = w;    };

return {      x: xc,      y: yc,      w: wc,      h: hc    }  }

原理如下圖,判斷下長邊是哪個,然後計算出x,y,width,height即可。


640?wx_fmt=jpeg

寫好程式碼後,試一下:


640?wx_fmt=png


紅框是最後要提交的範圍。

這個時候,還要處理下,把圖片變成黑底白字的圖片,因為MNIST資料集是這樣的。



步驟7 

主要程式碼如下:

ctx.fillStyle = "white"; ctx.fillRect(0, 0, grid.w, grid.h);
ctx.drawImage(img, grid.x, grid.y, grid.w, grid.h, 0, 0, size, size);
   
var imgData = ctx.getImageData(0, 0, size, size);
   
for (var i = 0; i < imgData.data.length; i += 4) {  imgData.data[i] = 255 - imgData.data[i];  imgData.data[i + 1] = 255 - imgData.data[i + 1];  imgData.data[i + 2] = 255 - imgData.data[i + 2];  imgData.data[i + 3] = 255; }
    ctx.putImageData(imgData, 0, 0);

畫上背景,遍歷畫素,把顏色反色下就ok啦。


最後都測試下:


640?wx_fmt=jpeg


最後,注意下MNIST資料集裡的資料,對應的是灰度圖,28x28的尺寸,黑底白字,並且數字是畫素的重心居中處理的。本文沒有介紹如何把web前端的手寫字根據重心居中處理這一內容,將會挑選合適時機介紹,用上了可以提高識別率哦!




第五篇

影像處理


再回顧下MNIST手寫字資料集的特點:每個資料經過歸一化處理,對應一張灰度圖片,圖片以畫素的重心居中處理,28x28的尺寸。


上一篇中,對canvas手寫對數字僅做了簡單對居中處理,嚴格來說,應該做一個重心居中的處理。


本篇主要介紹:

如何實現前端的手寫數字按重心居中處理成28x28的圖片格式。


我們先把前端canvas中的手寫數字處理成二值圖,求重心主要運用了二值圖的一階矩,先來看下零階矩:


640?wx_fmt=jpeg


二值圖在某點上的灰度值只有0或者1兩個值,因此零階矩為二值圖的白色面積總和。


640?wx_fmt=jpeg


只要把上文的公式轉為JS程式碼,即可求出重心座標:

SignaturePad.prototype.getGravityCenter = function() {

 var w = this._ctx.canvas.width,    h = this._ctx.canvas.height;  
 
 var mM = 0,    mX = 0,    mY = 0;  
 
var imgData = this._ctx.getImageData(0, 0, w, h);
 
 
for (var i = 0; i < imgData.data.length; i += 4) {  

 var t = imgData.data[i + 3] / 255;
 var pos = this.pixel2Pos(i);    
     mM = mM + t;    mX = pos.x * t + mX;    mY = pos.y * t + mY;
    };  
       var center = {             x: mX / mM,             y: mY / mM               }  
         return center
     };


pixel2Pos是我另外寫的根據i求出點座標的函式:

SignaturePad.prototype.pixel2Pos = function(p) {

 var w = this._ctx.canvas.width,    h = this._ctx.canvas.height;
 
  var y = Math.ceil((p + 1) / 4 / w);  
  var x = Math.ceil((p + 1) / 4 - (y - 1) * w);
  
   return {    x: x,    y: y  }
    }


這裡要注意下:

getImageData() 方法返回 ImageData 物件,該物件拷貝了畫布指定矩形的畫素資料。

對於 ImageData 物件中的每個畫素,都存在著四方面的資訊,即 RGBA 值:

  • R - 紅色 (0-255)

  • G - 綠色 (0-255)

  • B - 藍色 (0-255)

  • A - alpha 通道 (0-255; 0 是透明的,255 是完全可見的)


根據以上的程式碼就可以找出重心,如下圖紅點所示位置:

640?wx_fmt=jpeg


以重心為中心,把數字放置於28x28的正方形中,剪下出來,傳給後端即可。

640?wx_fmt=jpeg


以上為指南全文。


本文整理自往期 MixLab無界社群文章。


(本文為AI科技大本營轉載文章,轉載請聯絡作者。)

徵稿

640?wx_fmt=png


推薦閱讀:

                         640?wx_fmt=png

點選“閱讀原文”,開啟CSDN APP 閱讀更貼心!

相關文章