開(kāi)始之前,請(qǐng)先在項(xiàng)目的根目錄位置創(chuàng)建一個(gè)static目錄。我們將從這里提供HTML和JavaScript。
現(xiàn)在,我們首先使用WebRTC的getUserMedia抓取一個(gè)本地?cái)z像頭Feed。從這里,我們需要將該Feed的快照發(fā)送到剛剛創(chuàng)建的對(duì)象檢測(cè)Web API,獲取結(jié)果,然后使用canvas實(shí)時(shí)地在視頻上顯示這些結(jié)果。
HTML
我們先創(chuàng)建local.html文件:

此網(wǎng)頁(yè)的作用如下:
- 使用WebRTC adapter.js代碼填充(polyfill)
- 設(shè)置一些樣式,以便
- 將各個(gè)元素一個(gè)個(gè)疊加起來(lái)
- 將視頻放在底部,這樣我們就能使用canvas在它上面繪圖
- 為我們的getUserMedia流創(chuàng)建一個(gè)視頻元素
- 鏈接到一個(gè)調(diào)用getUserMedia的JavaScript文件
- 鏈接到一個(gè)將與我們的對(duì)象檢測(cè)API交互并在我們的視頻上繪制方框的JavaScript文件
獲取攝像頭流
現(xiàn)在,在靜態(tài)目錄中創(chuàng)建一個(gè)local.js文件,并將下面的代碼添加到該文件中:

在這里你會(huì)看到我們首先設(shè)置了一些約束條件。對(duì)于我自己的情況,我需要一段1280×720視頻,但要求范圍在640×480與1920×1080之間。然后,我們使用這些約束條件執(zhí)行g(shù)etUserMedia,并將所生成的流分配給我們?cè)贖TML中創(chuàng)建的視頻對(duì)象。
對(duì)象檢測(cè)API的客戶(hù)端版本
TensorFlow對(duì)象檢測(cè)API教程包含了可執(zhí)行以下操作的代碼:獲取現(xiàn)有圖像,將其發(fā)送給實(shí)際API進(jìn)行“推斷”(對(duì)象檢測(cè)),然后為它所看到的對(duì)象顯示方框和類(lèi)名。要想在瀏覽器中模擬這一功能,我們需要:
- 抓取圖像——我們會(huì)創(chuàng)建一個(gè)canvas來(lái)完成這一步
- 將這些圖像發(fā)送給API——為此,我們會(huì)將文件作為XMLHttpRequest中form-body的一部分進(jìn)行傳遞
- 再使用一個(gè)canvas將結(jié)果繪制在我們的實(shí)時(shí)流上
- 要完成所有這些步驟,需要在靜態(tài)文件夾中創(chuàng)建一個(gè)objDetect.js文件。
初始化和設(shè)置
我們需要先定義一些參數(shù):

你會(huì)注意到,我將其中一些參數(shù)作為data-元素添加到了自己的HTML代碼中。我最終是要在多個(gè)不同的項(xiàng)目中使用這段代碼,并且希望重用相同的代碼庫(kù),而如此添加參數(shù)就可以輕松做到這一點(diǎn)。待具體使用這些參數(shù)時(shí),我會(huì)一一解釋。
設(shè)置視頻和canvas元素
我們需要一個(gè)變量來(lái)表示我們的視頻元素,需要一些起始事件,還需要?jiǎng)?chuàng)建上面提到的2個(gè)canvas。

drawCanvas用于顯示我們的方框和標(biāo)簽。imageCanvas用于向我們的對(duì)象檢測(cè)API上傳數(shù)據(jù)。我們需要向可見(jiàn)HTML添加drawCanvas,這樣我們就能在繪制對(duì)象框時(shí)看到它。接下來(lái),需要跳轉(zhuǎn)到ObjDetect.js底部,逐函數(shù)向上編寫(xiě)。
啟動(dòng)該程序
1.觸發(fā)視頻事件
我們來(lái)啟動(dòng)該程序。首先要觸發(fā)一些視頻事件:

先查找視頻的onplay事件和loadedmetadata事件——如果沒(méi)有視頻,圖像處理也就無(wú)從談起。我們需要用到元數(shù)據(jù)來(lái)設(shè)置我們的繪圖canvas尺寸,使其與下一部分中的視頻尺寸相符。
2.啟動(dòng)主對(duì)象檢測(cè)子例程

雖然drawCanvas必須與視頻元素大小相同,但imageCanvas絕不會(huì)顯示出來(lái),只會(huì)發(fā)送到我們的API。可以使用文件開(kāi)頭的uploadWidth參數(shù)減小此大小,以幫助降低所需的帶寬量和服務(wù)器上的處理需求。需要注意的是,減小圖片可能會(huì)影響識(shí)別準(zhǔn)確度,特別是圖片縮減得過(guò)小的時(shí)候。
至此我們還需要為drawCanvas設(shè)置一些樣式。我選擇的是cyan,但你可以任選顏色。只是要確保所選的顏色與視頻Feed對(duì)比明顯,從而提供很好的可見(jiàn)度。
3.toBlob conversion

設(shè)置好canvas大小后,我們需要確定如何發(fā)送圖像。一開(kāi)始我采取的是較為復(fù)雜的方法,結(jié)果看到Fippo的grab()函數(shù)在最后一個(gè)KrankyGeek WebRTC事件處,所以我又改用了簡(jiǎn)單的toBlob方法。待圖片轉(zhuǎn)換為blob(二進(jìn)制大對(duì)象)后,我們就會(huì)將它發(fā)送到我們要?jiǎng)?chuàng)建的下一個(gè)函數(shù),即postFile。

有一點(diǎn)需要注意——Edge似乎不支持HTMLCanvasElement.toBlob方法。好像可以改用此處推薦的polyfill或改用msToBlob,但這兩個(gè)我都還沒(méi)有機(jī)會(huì)試過(guò)。
將圖像發(fā)送至對(duì)象檢測(cè)API

我們的postFile接受圖像blob作為實(shí)參。要發(fā)送此數(shù)據(jù),我們需要使用XHR將其作為表單數(shù)據(jù)通過(guò)POST方法發(fā)布。不要忘了,我們的對(duì)象檢測(cè)API還接受一個(gè)可選的閾值,所以在這里我們也可以加入此閾值。為便于調(diào)整,同時(shí)避免操作此庫(kù),你可以在我們?cè)陂_(kāi)頭設(shè)置的data-標(biāo)記中加入此參數(shù)及其他一些參數(shù)。
我們?cè)O(shè)置好表單后,需要使用XHR來(lái)發(fā)送它并等待響應(yīng)。獲取到返回的對(duì)象后,我們就可以繪制它們(見(jiàn)下一個(gè)函數(shù))。這樣就大功告成了。由于我們想要持續(xù)不斷地執(zhí)行上述操作,因此我們需要在獲取到上一API調(diào)用返回的響應(yīng)后,立即繼續(xù)抓取新圖像并再次發(fā)送。
繪制方框和類(lèi)標(biāo)簽
接下來(lái)我們需要使用一個(gè)函數(shù)來(lái)繪制對(duì)象API輸出,以便我們可以實(shí)際查看一下檢測(cè)到的是什么:

由于我們希望每次都使用一個(gè)干凈的繪圖板來(lái)繪制矩形,我們首先要使用clearRect來(lái)清空canvas。然后,直接使用class_name對(duì)項(xiàng)目進(jìn)行過(guò)濾,然后對(duì)剩余的每個(gè)項(xiàng)目執(zhí)行繪圖操作。
在objects對(duì)象中傳遞的坐標(biāo)是以百分比為單位表示的圖像大小。要在canvas上使用它們,我們需要將它們轉(zhuǎn)換成以像素?cái)?shù)表示的尺寸。我們還要檢查是否啟用了鏡像參數(shù)。如果已啟用,我們需要翻轉(zhuǎn)x軸,以便與視頻流翻轉(zhuǎn)后的鏡像視圖相匹配。最后,我們需要編寫(xiě)對(duì)象class_name并繪制矩形。
讓我們?cè)囈幌掳桑?/strong>
現(xiàn)在,打開(kāi)你最喜歡的WebRTC瀏覽器,在地址欄中輸入網(wǎng)址。如果你是在同一臺(tái)計(jì)算機(jī)上運(yùn)行,網(wǎng)址將為http://localhost:5000/local(如果設(shè)置了證書(shū),則為https://localhost:5000/local)。
關(guān)于優(yōu)化
上述設(shè)置將通過(guò)服務(wù)器運(yùn)行盡可能多的幀。除非為T(mén)ensorflow設(shè)置了GPU優(yōu)化,否則這會(huì)消耗大量的CPU資源(例如,我自己的情況是消耗了一整個(gè)核心),即便不作任何改動(dòng)也是如此。更高效的做法是,限制調(diào)用該API的頻率,僅在視頻流中有新活動(dòng)時(shí)才調(diào)用該API。為此,我在一個(gè)新的objDetectOnMotion.js文件中對(duì)objDetect.js做了一些修改。
修改前后內(nèi)容大致相同,我只不過(guò)添加了2個(gè)新函數(shù)。首先,不再是每次都抓取圖像,而是使用一個(gè)新函數(shù)sendImageFromCanvas(),僅當(dāng)圖片在指定的幀率內(nèi)發(fā)生了變化時(shí),該函數(shù)才發(fā)送圖片。幀率用一個(gè)新的updateInterval參數(shù)表示,限定了可以調(diào)用該API的最大間隔。為此,我們需要使用新的canvas和內(nèi)容。
這段代碼很簡(jiǎn)單:

imageChangeThreshold是一個(gè)百分比,表示有改動(dòng)的像素所占的百分比。我們獲得此百分比后將其傳遞給imageChange函數(shù),此函數(shù)返回True或False,表示是否超出了閾值。下面顯示的就是這個(gè)函數(shù):

上面的這個(gè)函數(shù)其實(shí)是經(jīng)過(guò)大幅改進(jìn)后的版本,之前的版本是我很久以前編寫(xiě)的,用于在動(dòng)作檢測(cè)嬰兒監(jiān)視器程序中檢測(cè)嬰兒動(dòng)作。它首先測(cè)量每個(gè)像素的RGB顏色值。如果這些值與該像素的總體顏色值相比絕對(duì)差值超過(guò)10,則將該像素視為已改動(dòng)。10只是隨意定的一個(gè)值,但在我的測(cè)試中似乎是很合適的值。如果有改動(dòng)的像素?cái)?shù)超出threshold,該函數(shù)就會(huì)返回True。
對(duì)此稍微深入研究后,我發(fā)現(xiàn)其他一些算法通常轉(zhuǎn)換成灰度值,因?yàn)轭伾⒉荒芎芎玫胤从硠?dòng)作。應(yīng)用高斯模糊也可以消除編碼差異。Fippo提出了一個(gè)很好的建議,即借鑒test.webrtc.org在檢測(cè)視頻活動(dòng)時(shí)使用的結(jié)構(gòu)相似性算法(見(jiàn)此處此處)。后續(xù)還會(huì)分享更多技巧。
適合任何視頻元素
這段代碼實(shí)際上對(duì)任何

不妨使用你自己的視頻試一下。只是提醒一下,如果你使用的是托管在另一臺(tái)服務(wù)器上的視頻,要注意CORS問(wèn)題。
一不小心寫(xiě)成了長(zhǎng)篇
完成上面這一切耗費(fèi)了很多時(shí)間,不過(guò)現(xiàn)在我希望開(kāi)始著手有趣的部分了:嘗試不同的型號(hào)和訓(xùn)練我自己的分類(lèi)器。已發(fā)布的對(duì)象檢測(cè)API是針對(duì)靜態(tài)圖像設(shè)計(jì)的。那么針對(duì)視頻和對(duì)象跟蹤進(jìn)行了調(diào)整的模型又是什么樣的呢?很值得一試。
此外,這里還有太多的優(yōu)化工作需要做。我在運(yùn)行此服務(wù)時(shí)并沒(méi)有配備GPU,如果配備的話(huà),性能會(huì)大為不同。如果幀的數(shù)量不多,支持1個(gè)客戶(hù)端需要大約1個(gè)核心,而我使用的是最快但準(zhǔn)確性最低的模型。在這方面有很大的性能提升空間。如果觀察此服務(wù)在GPU云端網(wǎng)絡(luò)中性能如何,想必也很有趣。