深度學習的JavaScript基礎:從瀏覽器中提取數據
- 2019 年 12 月 16 日
- 筆記
最近在讀一本《基於瀏覽器的深度學習》,書比較薄,但是涉及的內容很多,因此在讀的過程中不得不再查閱一些資料,以加深理解。我目前從事的本職工作就是瀏覽器研發,對於前端技術並不陌生。但是從前段時間開發微信小程式識狗君的過程來看,對JavaScript還是掌握得太少,特別是對一些前端框架以及一些比較新的JavaScript語法和編程模型,了解的不夠。在修改tfjs-core源碼時,就體會到這種痛苦。好吧,既然無法避開,那就正面剛吧。
在python語言中,通過文件、攝影機獲取數據,並不是什麼難事。但對於瀏覽器來說,出於安全的考慮,並不能直接訪問本地文件,至於訪問攝影機、麥克風這樣的硬體設備,只是從HTML5才開始得到支援。本文就如果獲取數據展開討論,看看在瀏覽器中提取數據有哪些方法。
載入影像數據
影像分類、對象目標檢測等是機器學習方面的重要應用,這離不開影像數據。為了將影像作為機器學習演算法的輸入,必須事先提取影像的像素值。
從影像中提取像素值
熟悉HTML的朋友肯定知道,要在瀏覽器中顯示一幅影像,通常通過HTML img標籤:
<img src="images/cat.jpg" id="img_cat"></img>
現在我們可以使用全局DOM API document.getElementById(『img_cat』)訪問影像元素。問題是這樣獲得的HTMLImageElement類型,並沒有相關的API來提取像素值。此外還需要注意的是,這裡用到的DOM API只在瀏覽器中可用,在Node.js這樣沒有DOM的JavaScript運行時中不可用。
慶幸的是,從HTML 5開始,現代瀏覽器提供了Canvas API,可以用編程的方式將像素繪製到螢幕上,也有相應的API提取像素值。
為了從Canvas元素中提取數據,我們首先需要創建畫布上下文,在此上下文中,我們可以將影像內容繪製到畫布上,然後訪問並返回畫布像素數據。
function loadRgbDataFromImage(img) { // 創建canvas元素 const canvas = document.createElement('canvas'); // 將canvas尺寸設置為影像大小 canvas.width = img.width; canvas.height = img.height; // 創建2D渲染上下文 const ctx = canvas.getContext('2d'); // 將影像渲染到canvas上下文的坐上角坐標(0, 0) ctx.drawImage(img, 0, 0, img.width, img.height); // 提取RGB數據 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 將數據轉換為int32數組 return new Int32Array(imgData.data); }
在上面的程式碼中,ctx.getImagedata函數返回ImageData類型的數據,這是一個包含width, height和data屬性的對象。data屬性值的存儲格式為類型化數組Uint8ClampedArray。
需要注意的是,影像是非同步載入的,因此我們只有在瀏覽器完全載入了影像才能提取像素值,這可以在onload事件中完成。
const img = document.getElementById('img_cat'); img.onload = () => { const data = loadRgbdataFromImage(img); ... }
載入遠程資源
影像數據不僅可以是本伺服器上的圖片,還可以是其它遠程伺服器上的資源,以URL的形式提供。
<img src="https://<other_server>/cat.jpg" crossOrigin="anonymous" id="img_cat"></img>
在載入其它遠程伺服器上的資源時,需要了解跨域資源共享(Cross-Origin Resource Sharing, CORS)的概念。出於安全的考慮,瀏覽器會自動阻止對當前連接之外的不同域、協議或埠的cross-site請求。而CORS策略允許瀏覽器通過設置附加的HTTP頭來執行對資源的跨域HTTP請求。比如上面程式碼中,使用crossOrigin屬性,並將其設置為anonymouse,顯式地允許該元素載入cross-site資源。
我們也可以通過JavaScript,以編程方式完成上述程式碼的功能。需要注意載入影像資源是非同步行為,我們返回Promise,而不是已經載入的資源。
function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; img.src = url; img.onload = () => resolv(img); img.onerror = reject; }); }
載入二進位塊
經過訓練的模型,模型權重、參數等數據,通常以二進位塊的形式保存,所以在瀏覽器中使用機器學習模型,一定會面臨二進位塊的載入問題。好在JavaScript是一種非常通用的語言,內置了對類型化數組和數組緩衝區的支援,這使得在瀏覽器中使用二進位數據非常方便。
相比文本表示格式(如csv或JSON),二進位數據文件更小,載入速度更快(不需要解析),這使得在JavaScript中載入較大規模的模型權重成為可能。
假如我們有一個二進位塊rand.bin,可以創建一個函數來獲取二進位塊作為數組緩衝區。
async function loadBinaryDataFromUrl(url) { const req = new Request(url); const res = await fetch(req); if (!res.ok) { throw Error(res.statusText); } return res.arrayBuffer(); }
訪問外設數據
隨著移動終端的普及,以前很多需要電腦上完成的工作,都可以在移動終端上完成,而移動終端豐富且使用方便的外設(相機、麥克風、重力感應器等)提供了多種玩法。早期的瀏覽器訪問設備的能力幾乎沒有,但從HTML5開始,增加了硬體訪問能力,提供了Device API,藉助於Device API,通過JS和HTML頁面訪問終端的應該成為可能。
從網路攝影機獲取影像
瀏覽器的MediaDevices API允許用戶訪問影片和音頻設備,例如相機、麥克風和揚聲器。它是更通用的WebRTC API的一部分。
我們可以使用MediaDevices::getUserMedia()函數啟動影片流,該函數將返回包含MediaStream對象的promise。
navigator.mediaDevices.getUserMedia({ video: true, audio: false}) .then((stream) => { ... });
為了從MediaStream中提取數據,需要將流附加到HTML video元素。我們可以通過程式碼創建一個這樣的元素,並將流提供給播放器。
const player = document.createElement('video'); navigator.mediaDevices.getUserMedia({ video: true, audio: false}) .then((stream) => { player.srcObject = stream; });
最後,我們可以從video元素中提取內容,將影像渲染到畫布,然後提取畫布中的像素。
const data = loadRgbDataFromImage(player, width, height);
是的,這個地方沒有看錯,player也可以傳遞給loadRgbDataFromImage。查看drawImage函數的原型,對於img參數的說明為:
img:Specifies the image, canvas, or video element to use
也就是說這裡傳遞image、canvas或video都是可以的。
還有一種更高端用法,就是從WebGL中的video元素訪問,而無須使用畫布,有興趣的可以查閱相關資料。
用麥克風錄音
訪問麥克風同樣通過MediaDevices API,處理數據則通過WebAudio API,這是一個非常靈活的基於圖的音頻處理API。
首先使用MediaDevices::getUserMedia()函數檢索音頻流。
navigator.mediaDevices.getUserMedia( { audio: true, video: false }) .then(onStream);
接下來,設置一個非常簡單的音頻圖,包括輸入、簡單處理器和默認輸出。我們還需定義處理器的屬性,包括輸入和輸出通道的數量以及音頻塊的緩衝區大小。
const audioContext = new AudioContext(); const bufferSize = 4096; const numInputChannels = 1; const numOutputChannels = 3; function onStream(stream) { const source = audioContex.createMediaStreamSource(stream); const processor = audioContext.createScriptProcessor( bufferSize, numInputChannels, numOutputChannels); source.connect(processor); processor.connect(audioContext.destination); processor.onaudioprocess = onProcess; }
然後,在onProcess中執行處理。
function onProcess(e) { const data = e.inputBuffer.getChannelData(0); ... }
AudioBuffer.getChannelData()函數返回Float32Array(bufferSize)的數據,還可以通過duration、sampleRate和numberOfChannels屬性獲得其它音頻資訊。
現在我們可以使用這些數據進一步處理或直接送給模型。
小結
本文探討如何在瀏覽器中獲取數據的幾種方法,包括影像數據、音頻數據,現代瀏覽器具備原來越豐富的設備訪問能力,配合移動終端方便易用的外設,必將產生越來越多的有趣的機器學習應用。