全棧AI工程師指南,DIY一個識別手寫數字的web應用
作者 | 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中配置宿主機的與映象共享的目錄地址
將新建一個容器,並在容器中開啟一個互動模式的終端,結果如下:
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已經執行成功,如下圖:
開啟瀏覽器,在位址列中輸入:
localhost:8888
即可訪問jupyter,如下圖:
01/03
Hello Jupyter Notebook
上文提到的jupyter notebook到底是什麼東西?
Jupyter Notebook 是一款集程式設計和寫作於一體的效率工具,優點:
分享便捷
遠端執行
互動式展現
在瀏覽器可以訪問Jupyter Notebook,也就是說,我可以部署成web應用的形式,使用者可以分享,通過域名訪問,並且可以利用web的任何互動方式。
繼續我們的教程,在瀏覽器開啟Jupyter Notebook後,找到我們與本地共享的專案目錄kerasStudy,點選進入,然後點選jupyter右上角的new,選擇python2,如下圖所示:
新建一個notebook。
先來做個小實驗:
輸入:
import numpy as np
np.random.seed(1337)
np.random.rand(5)
然後在選單中,選擇Cell--Run Cells,執行程式碼:
如下圖所示,輸出了一些結果:
第一行程式碼:
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(60000, 784)
#X_train原本是一個60000*28*28的三維向量,將其轉換為60000*784的二維向量
X_test = X_test.reshape(10000, 784)
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 一下,可以看到訓練過程,如下圖:
步驟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的容器:
我這邊是容器名(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.299, 0.587, 0.114])
img = rgb2gray(img)
關於圖片的通道,我們可以在photoshop裡直觀的檢視:
先檢視下讀取的圖片陣列維度:
print(img.shape)
輸出是(28, 28)
轉化成正確的輸入格式:
img = img.reshape(1, 784)
列印出來看看:
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下的檔案構成如下:
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頁面:
def hello_world():
return app.send_static_file('index.html')
其餘flask的相關配置程式碼可以參考往期文章:
這個時候,我們啟動docker,把映象啟動,並進入docker映象的終端中(檢視第2篇),找到app目錄,終端輸入:
python app.py
等終端提示相關的啟動資訊後,在瀏覽器裡試下,輸入:
http://localhost:8888/
成功開啟index.html頁面:
再次編輯app.py檔案,寫一個predict的介面,接受前端提交的圖片,並返回識別結果給前端:
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.299, 0.587, 0.114])
def img2class(imgFile):
img = mpimg.imread(imgFile)
print(img.shape)
img = rgb2gray(img)
print(img.shape)
img = img.reshape(1, 784)
pre=model.predict_classes(img)
result=pre[0]
return result
在docker映象中啟動偽終端,進入app目錄,輸入:
python app.py
上傳測試圖片試試:
成功返回識別結果,至此,一個迷你的識別手寫字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();
到這一步可以試一下前端的輸入效果先:
接下來完成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的控制檯直接試驗。
紅色的圈圈就是所有的座標點,只要求出如下圖所示的紫色框,第一步也就完成了。
步驟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;
};
測試下:
注意paddingNum,我設定了個30的值,把邊框稍微放大了下,原因見mnist手寫字訓練集的圖片就知道啦。
到這一步,我們的手寫字資料集是下圖這樣的:
步驟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即可。
寫好程式碼後,試一下:
紅框是最後要提交的範圍。
這個時候,還要處理下,把圖片變成黑底白字的圖片,因為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啦。
最後都測試下:
最後,注意下MNIST資料集裡的資料,對應的是灰度圖,28x28的尺寸,黑底白字,並且數字是畫素的重心居中處理的。本文沒有介紹如何把web前端的手寫字根據重心居中處理這一內容,將會挑選合適時機介紹,用上了可以提高識別率哦!
第五篇
影像處理
再回顧下MNIST手寫字資料集的特點:每個資料經過歸一化處理,對應一張灰度圖片,圖片以畫素的重心居中處理,28x28的尺寸。
上一篇中,對canvas手寫對數字僅做了簡單對居中處理,嚴格來說,應該做一個重心居中的處理。
本篇主要介紹:
如何實現前端的手寫數字按重心居中處理成28x28的圖片格式。
我們先把前端canvas中的手寫數字處理成二值圖,求重心主要運用了二值圖的一階矩,先來看下零階矩:
二值圖在某點上的灰度值只有0或者1兩個值,因此零階矩為二值圖的白色面積總和。
只要把上文的公式轉為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 是完全可見的)
根據以上的程式碼就可以找出重心,如下圖紅點所示位置:
以重心為中心,把數字放置於28x28的正方形中,剪下出來,傳給後端即可。
以上為指南全文。
本文整理自往期 MixLab無界社群文章。
(本文為AI科技大本營轉載文章,轉載請聯絡作者。)
徵稿推薦閱讀:
給Chrome“捉蟲”16000個,Google開源bug自檢工具
2019全球AI 100強,中國佔獨角獸半壁江山,但憂患暗存
“百練”成鋼:NumPy 100練
這4門AI網課極具人氣,逆天好評!(附程式碼+答疑)
點選“閱讀原文”,開啟CSDN APP 閱讀更貼心!
相關文章
- 全棧工程師如何快速構建一個Web應用全棧工程師Web
- Web全棧工程師應該會什麼Web全棧工程師
- 【Get】用深度學習識別手寫數字深度學習
- 全棧工程師和架構師的區別全棧工程師架構
- 手寫數字圖片識別-全連線網路
- 如何才能成為一名Python web全棧工程師?PythonWeb全棧工程師
- web全棧開發工程師的趨勢、價值Web全棧工程師
- tensorflow.js 手寫數字識別JS
- 用tensorflow2實現mnist手寫數字識別
- 瀏覽器中的手寫數字識別瀏覽器
- 【機器學習】手寫數字識別機器學習
- Web前端技術分享:全棧工程師常用的開發工具Web前端全棧工程師
- Web全棧工程師前景+學習指南-CSDN就業班-專題視訊課程Web全棧工程師就業
- Tensorflow實現RNN(LSTM)手寫數字識別RNN
- Tensorflow2.0-mnist手寫數字識別示例
- 一文了解前端與全棧工程師!前端全棧工程師
- 你想當全棧工程師嗎?全棧工程師
- [譯] 如何編寫全棧 JavaScript 應用全棧JavaScript
- Pytorch搭建MyNet實現MNIST手寫數字識別PyTorch
- OpenCV + sklearnSVM 實現手寫數字分割和識別OpenCV
- [ 招聘 | 上海 ] 軟體工程師 / 全棧工程師 / 晶片設計工程師軟體工程工程師全棧晶片
- 如何成為一名優秀的全棧工程師全棧工程師
- 【引向】全棧開發工程師之路全棧工程師
- 全棧工程師學習路線全棧工程師
- 要不要做全棧工程師全棧工程師
- AI 聊天應用開發實戰:從構思到上線的全棧開發指南AI全棧
- 識別這5個錯誤,進階高階web前端工程師!Web前端工程師
- 計算機視覺—CNN識別手寫數字(11)計算機視覺CNN
- 計算機視覺—kNN識別手寫數字(10)計算機視覺KNN
- 【TensorFlow2.0】LeNet進行手寫體數字識別
- 在PaddlePaddle上實現MNIST手寫體數字識別
- 小熊飛槳練習冊-01手寫數字識別
- Pytorch 手寫數字識別 深度學習基礎分享PyTorch深度學習
- Python全棧指什麼?全棧工程師的意義是什麼?Python全棧工程師
- 全棧低程式碼專案,你手寫一個企業級的低程式碼全棧專案全棧
- 迷茫的全棧工程師(我叫沈?兒)全棧工程師
- 全棧工程師的定義和價值全棧工程師
- 成為Java全棧工程師的步驟Java全棧工程師