當Node.js遇上OpenCV深度神經網路
編者按:今天我們將看看Node.js的OpenCV深度神經網路模塊。如果你希望釋放神經網路的魔力,來辨識和分類圖像中的物體,卻對深度學習是如何工作的毫無頭緒(像我一樣),更不知道如何創建和訓練神經網路,那麼本文正適合你!
原文:Node.js meets OpenCV』s Deep Neural Networks?—?Fun with Tensorflow and Caffe
註:本文由opencv4nodejs及face-recognition.js維護者Vincent Mühler授權論智編譯,請勿隨意轉載。詳情見轉載須知。
所以我們今天將創建什麼?
在這一篇教程中,我們將了解如何通過OpenCV的DNN模塊,從Tensorflow和Caffe載入預訓練的模型,然後我們將深入兩個基於Node.js和OpenCV進行物體識別的例子。
首先我們將使用Tensorflow的Inception模型來辨識圖像中的物體,之後我們將使用COCO SSD模型檢測和辨識同一圖像中的多個不同物體。
你可以在我的github倉庫上找到樣例代碼:justadudewhohacks/opencv4nodejs
Tensorflow Inception
訓練過的Tensorflow Inception模型可以辨別約1000個分類的物體。如果你將圖像傳入網路,它將給出圖像中的物體的每個分類的似然。
要在OpenCV下使用Inception模型,我們需要載入二進位文件tensorflow_inception_graph.pb
以及分類名稱列表imagenet_comp_graph_label_strings.txt
。你可以下載inception5h.zip
並解壓以獲得這些文件(下面的代碼內有下載鏈接):
// 替換路徑為你解壓縮inception模型的路徑const inceptionModelPath = ../data/dnn/tf-inceptionconst modelFile = path.resolve(inceptionModelPath, tensorflow_inception_graph.pb);const classNamesFile = path.resolve(inceptionModelPath, imagenet_comp_graph_label_strings.txt);if (!fs.existsSync(modelFile) || !fs.existsSync(classNamesFile)) { console.log(退出: 找不到inception模型); console.log(從以下網址下載模型: https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip); return;}// 讀取classNames(分類名稱),然後在數組中儲存它們const classNames = fs.readFileSync(classNamesFile).toString().split("
");// 從modelFile初始化tensorflow inception模塊const net = cv.readNetFromTensorflow(modelFile);
分類圖像中的物品
為了分類圖像中的物品,我們將編寫以下幫助函數:
const classifyImg = (img) => { // inception模型使用224 x 224 圖像, // 因此我們調整輸入圖像的大小, // 並使用白像素補齊圖像 const maxImgDim = 224; const white = new cv.Vec(255, 255, 255); const imgResized = img.resizeToMax(maxImgDim).padToSquare(white); // 網路接受blob作為輸入 const inputBlob = cv.blobFromImage(imgResized); net.setInput(inputBlob); // 前向傳播輸入至整個網路, // 將返回包含每個分類的置信度的1xN矩陣分類結果 const outputBlob = net.forward(); // 找到大於最小置信度的所有標籤 const minConfidence = 0.05; const locations = outputBlob .threshold(minConfidence, 1, cv.THRESH_BINARY) .convertTo(cv.CV_8U) .findNonZero(); const result = locations.map(pt => ({ confidence: parseInt(outputBlob.at(0, pt.x) * 100) / 100, className: classNames[pt.x] })) // 根據置信度排序 .sort((r0, r1) => r1.confidence - r0.confidence) .map(res => `${res.className} (${res.confidence})`); return result;}
這一函數做了這些事:
準備輸入圖像
Tensorflow Inception網路接受224x224大小的輸入圖像。因此我們調整圖像大小,使其最大邊的大小為224,然後用白像素補齊。
讓圖像穿過網路
我們可以直接從圖像創建blob,然後調用net.forward()
前向傳播輸入,然後獲取輸出blob.
從輸出blob提取結果
為了通用性,輸出blob的表達形式直接是矩陣(cv.Mat
),而它的維度取決於模型。在Inception下這很簡單。blob不過是一個1xN矩陣(其中N等於分類數),描述了所有分類的概率分布。每個條目為一個浮點數,表示相應分類的置信度。所有條目相加,總和為1.0(100%)。
我們想仔細看看圖像可能性最大的分類,因此我們查看所有置信度大於minConfidence
(這個例子中是5%)。最後,我們根據置信度排序結果,並返回className
、confidence
對。
測試
現在我們將讀取一些我們希望網路辨識的樣本數據:
const testData = [ { image: ../data/banana.jpg, label: banana }, { image: ../data/husky.jpg, label: husky }, { image: ../data/car.jpeg, label: car }, { image: ../data/lenna.png, label: lenna }];testData.forEach((data) => { const img = cv.imread(data.image); console.log(%s: , data.label); const predictions = classifyImg(img); predictions.forEach(p => console.log(p)); console.log(); cv.imshowWait(img, img);});
輸出為:(你可以參考本文開頭的圖片)
banana:banana (0.95)husky:Siberian husky (0.78)Eskimo dog (0.21)car:sports car (0.57)racer (0.12)lenna:sombrero (0.34)cowboy hat (0.3)
很有趣。我們得到了愛基斯摩犬和香蕉圖像非常準確的描述。對於汽車圖像而言,汽車的具體類別不太准,但模型確實辨識出了圖像中的汽車。當然,網路不可能在無限的分類上進行訓練,因此它沒有為最後一張圖像返回「婦女」描述。然而,它確實辨識出了帽子。
COCO SSD
好,模型表現不錯。但是我們如何處理包含多個物體的圖像呢?為了辨識單一圖像中的多個物體,我們將利用單圖多盒檢測器(Single Shot Multibox Detector, SSD)。在我們的第二個例子中,我們將查看一個在COCO(Common Object in Context)數據集上訓練的SSD模型。我們使用的這一模型在84個不同分類上訓練過。
這一模型來自Caffe,因此我們將載入二進位文件VGG_coco_SSD_300x300_iter_400000.caffemodel
,以及protoxt文件deploy.prototxt
:
// 替換路徑為你解壓縮coco-SSD模型的路徑const ssdcocoModelPath = ../data/dnn/coco-SSD_300x300const prototxt = path.resolve(ssdcocoModelPath, deploy.prototxt);const modelFile = path.resolve(ssdcocoModelPath, VGG_coco_SSD_300x300_iter_400000.caffemodel);if (!fs.existsSync(prototxt) || !fs.existsSync(modelFile)) { console.log(退出: 找不到ssdcoco模型); console.log(從以下網址下載模型 https://drive.google.com/file/d/0BzKzrI_SkD1_dUY1Ml9GRTFpUWc/view); return;}// 從prototxt和modelFile初始化ssdcoco模型const net = cv.readNetFromCaffe(prototxt, modelFile);
基於COCO分類
我們的分類函數和基於Inception的分類函數幾乎一樣,不過這次輸入將是300x300的圖像,而輸出將是1x1xNx7矩陣。
const classifyImg = (img) => { const white = new cv.Vec(255, 255, 255); // ssdcoco模型接受300 x 300圖像 const imgResized = img.resize(300, 300); // 網路接受blob作為輸入 const inputBlob = cv.blobFromImage(imgResized); net.setInput(inputBlob); // 前向傳播輸入至整個網路, // 將返回1x1xNxM矩陣作為分類結果 let outputBlob = net.forward(); // 提取NxM矩陣 outputBlob = outputBlob.flattenFloat(outputBlob.sizes[2], outputBlob.sizes[3]); const results = Array(outputBlob.rows).fill(0) .map((res, i) => { const className = classNames[outputBlob.at(i, 1)]; const confidence = outputBlob.at(i, 2); const topLeft = new cv.Point( outputBlob.at(i, 3) * img.cols, outputBlob.at(i, 6) * img.rows ); const bottomRight = new cv.Point( outputBlob.at(i, 5) * img.cols, outputBlob.at(i, 4) * img.rows ); return ({ className, confidence, topLeft, bottomRight }) }); return results;};
我不是很清楚為何輸出是1x1xNx7矩陣,不過我們實際上只關心Nx7部分。我們可以使用flattenFloat工具函數映射第三、第四維至2D矩陣。與Inception輸出矩陣相比,這次N不對應每個分類,而是檢測到的每個物體。另外,每個物體對應7個條目。
為什麼是7個條目?
記住,這裡我們遇到的問題和之前有點不一樣。我們想要檢測單張圖像中的多個物體,因此我們不可能僅僅給出每個分類的置信度。我們實際上想要得到的是一個指示每個物體在圖中的位置的矩形。7個條目分別為:
- 我其實毫無頭緒
- 物體的分類標籤
- 分類的置信度
- 矩形左端的x
- 矩形底部的y
- 矩形右端的x
- 矩形頂部的y
輸出矩陣給了我們不少關於結果的信息,這看起來相當整潔。我們同樣可以根據置信度再次過濾結果,並為每個辨識出的物體在圖像中繪製邊框。
看看它的效果!
出於行文的簡潔,我將跳過繪製矩形的代碼,以及其他可視化的代碼。如果你想知道具體是怎麼做的,可以訪問前面提到的github倉庫。
讓我們傳入一張汽車圖像到網路,然後過濾結果,看看是否檢測到了car
分類:
很棒!下面提高一下難度。讓我們試下……一張早餐桌?
很不錯!
最後的話
如果你想進一步嘗試,我建議你查看Caffe Model Zoo,其中提供了用於不同用例的已訓練模型,你可以直接下載。
如果你基於OpenCV和DNN做了很棒的應用,我會很感興趣!歡迎留言分享。
推薦閱讀:
※如何看待 Azer Ko?ulu 刪除了自己的所有 npm 庫?
※前端構建工具 Gulp / browserify/ webpack / npm ?
※請問一下,跨平台解決方案中,Qt 和 Electron 孰優孰劣?
※如何解釋Node.js下與瀏覽器環境代碼執行結果不一致的問題?
※node相比傳統服務端技術棧差在哪裡?