利用YOLO進行圖像目標識別
爲什麼需要目標檢測?
在上一課將激光雷達3D點雲映射到相機圖像中我們已經實現了將激光雷達3D點雲映射到相機圖像中(效果如下圖所示),但是要想讓我們得到的激光和相機融合後的結果更好地服務於自動駕駛車輛,例如,應用到常見的碰轉時間(TTC)估計中,我們還需要一種技術來檢測圖像中的車輛目標,這樣我們就能分離出圖像中匹配的關鍵點(matched keypoints)以及映射在圖像中的激光雷達點雲,並能進一步將它們與特定的對象進行關聯。
我們知道,利用圖像處理技術,我們可以使用各種檢測器(detectors )和描述子(descriptors)來檢測和匹配圖像中的關鍵點(keypoints )。然而,在TTC估計的應用場景中,爲了計算車輛的碰撞時間,我們需要分離出車輛目標上的關鍵點,這樣TTC估計就不會因爲包含了前方場景中路面、靜止物體或其他車輛上的匹配信息而失真。實現這一目的的一種方法是使用目標檢測來自動識別場景中的車輛。這種算法的輸出(理想情況下)是場景中所有對象周圍的一組2D邊界框(bounding boxes)。基於這些邊界框,我們可以輕鬆地將匹配關鍵點與目標對象關聯起來,從而獲得一個穩定的TTC估計值。
對於激光雷達的測量結果,可以應用相同的原理。我們知道,激光雷達的3D點雲可以通過聚類形成單獨的對象。例如,我們可以通過使用點雲庫(PCL)中的算法,進行激光雷達點雲的聚類處理,而PCL則可以被近似看作是3D應用程序的OpenCV庫。在本節中,讓我們來看看另一種將激光雷達點分聚類成目標的方法。根據上一節的課程,我們現在已經掌握瞭如何將激光雷達點投射到圖像平面上,那麼給定一組圖像中來自目標檢測的邊界框,我們這隻需檢查它在映射到相機圖像後是否在某個邊界框中,就可以很容易地將一個三維激光雷達點與場景中的特定目標關聯起來。
我們將在本節後面詳細介紹這兩種方法,但是現在,讓我們先來了解一種檢測相機圖像中目標的方法——這是分離出匹配關鍵點和激光雷達點雲的先決條件。下面就讓我們學習一種“檢測和分類對象”的方法。
YOLO簡介
本節的目的是使你能夠快速地利用一個強大和先進的工具進行目標檢測。本文的目的不是在理論上深入研究這些算法的內部工作原理,而是讓您能夠快速將目標檢測集成到本課程的代碼框架中。下面的圖像顯示了我們將在本節中開發的代碼的示例輸出。
注:上面的圖片改編自開源項目源代碼
不同於HOG/SVM這樣基於分類器的系統,YOLO針對整個圖像,因此其預測值依賴於整個圖像的全部內容。它的預測只用了單層網絡模型,而不像R-CNN系統那樣依賴上千層網絡模型。這使得YOLO在輸出類似預測結果的條件下,具有極快的預測速度。
YOLO的創造者也提供了一系列基於COCO數據集進行預訓練得到的權重,使當前版本YOLOv3能夠識別圖像和圖像中的80種不同目標。這意味着,我們可以利用YOLOv3作爲一個“開箱即用的”分類器,來相對準確地檢測圖像中的汽車及其他目標。
YOLOv3目標識別工作流程
本節,我們將看一下針對本課程的圖像數據集,執行YOLO所需的步驟。下面提到的參數是課程作者推薦的。下面是主要算法流程:
- 首先,圖像被分成1313像素的柵格單元。基於輸入圖像的尺寸,這些柵格單元的尺寸也應該相應變化。在下面的代碼中,一個416416像素的圖像,對應的柵格單元尺寸爲32*32像素。
- 如下方原理圖所示,每個柵格單元用於預測一系列的bounding box。對於每個bounding box,網絡也會預測該box包含某個特定目標的置信度,以及該目標屬於哪種種類的概率(依據COCO數據集)。
- 最後,進行非極大值抑制 (non-maximum suppression),用於排除具有低置信度的bounding box以及包含了同一目標的多餘box。
下面較爲詳細地展示了程序工作流程對應的代碼。
步驟1:參數初始化
YOLOv3預測的每個bounding box都有一個相應的置信度。參數 confThreshold
用於移除低置信度的bounding box。
然後,非極大值抑制(NMS)用那個與保留bounding box,NMS過程受參數nmsThreshold
控制。
輸入圖像的而大小受參數inpWidth
和inpWidth
控制,YOLO作者將其均設置爲416。這裏我們也可以將其改爲320(更快)或608(更慢)。
步驟2:準備模型
文件yolov3.weights
包含YOLO作者提供的預訓練網絡的權重,可以通過此鏈接下載。
下面的代碼展示瞭如何加載模型權重以及相關的模型配置。
// load image from file
cv::Mat img = cv::imread("./images/img1.png");
// load class names from file
string yoloBasePath = "./dat/yolo/";
string yoloClassesFile = yoloBasePath + "coco.names";
string yoloModelConfiguration = yoloBasePath + "yolov3.cfg";
string yoloModelWeights = yoloBasePath + "yolov3.weights";
vector<string> classes;
ifstream ifs(yoloClassesFile.c_str());
string line;
while (getline(ifs, line)) classes.push_back(line);
// load neural network
cv::dnn::Net net = cv::dnn::readNetFromDarknet(yoloModelConfiguration, yoloModelWeights);
net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
加載了網絡之後,DNN backend被設置爲DNN_BACKEND_OPENCV
。如果OpenCV已經經過Intel的Inference引擎編譯,則應該將其設置爲DNN_BACKEND_INFERENCE_ENGINE
。 在代碼中,目標被設置爲了CPU,如果要使用(Intel)GPU,則應該將DNN_TARGET_CPU
設置爲DNN_TARGET_OPENCL
。
**步驟3:從輸入圖像中生成4D Blob **
當數據流進入網絡模型,YOLO以“Blobs”作爲保存、通信、操作信息的基本單元:blob是用於許多框架(如Caffe)的標準序列(array)和統一存儲接口。一個Blob是一個提供實際數據處理、交互,以及CPU和GPU之間數據同步的包裝器 (wrapper)。數學上,一個blob是以C-contiguous方式存儲的一個N維序列,N是數據的batch size。常見的用於圖像數據batches的blob維度是 數量 N * 通道C * 高度H * 寬度W。在這種情況下,N是數據的batch size。利用Batch進行處理可以使數據通信和設備處理獲得更好的數據吞吐量。對於一個256個圖像的訓練batch,N應爲256. C表示特徵維度,例如對於RGB圖像,C=3。在OpenCV中,blob被存儲爲4維cv::Mat 序列(array),維度順序爲NCHW。更多關於blob的信息可以參考:http://caffe.berkeleyvision.org/tutorial/net_layer_blob.html
下面的例子展示了N=2,C=16通道,H=5, W=4時的blob數據情況。
注:圖片來源
在一個blob數據結構中,位置值(n,c,h,w)可以通過下面的公式進行訪問:
b (n, c, h, w) = ((n * C + c) * H + h) * W + w
下方的代碼展示了從文件中加載的圖像數據如何傳遞給blobFromImage
函數從而轉換爲用於神經網絡的輸入block。像素值可以通過乘以一個縮放因子1/255,來將像素值縮放到[0,1]範圍。而且代碼中還調整了圖像尺寸爲(416,416,416),從而使圖像不會被裁剪。
// generate 4D blob from input image
cv::Mat blob;
double scalefactor = 1/255.0;
cv::Size size = cv::Size(416, 416);
cv::Scalar mean = cv::Scalar(0,0,0);
bool swapRB = false;
bool crop = false;
cv::dnn::blobFromImage(img, blob, scalefactor, size, mean, swapRB, crop);
接下來的代碼中,輸出的blob將會作爲神經網絡的輸入。然後,會執行前向傳遞從而獲得一個預測的bounding box作爲網絡的輸出。這些box經過後處理步驟,濾掉了其中具有低置信度的部分。詳細內容請見下一步驟。
步驟4:執行網絡的前向傳遞
接下來,我們將上一步中輸出的blob作爲神經網絡的輸入。然後,我們運行forward-function函數執行網絡的前向傳播過程。爲了實現這一步,我們需要確定網絡的最後一層,並提供相關的函數內部名稱。這一步通過getUnconnectedOutLayers
函數完成,它會給未連接的網絡輸出層提供名字,下面的代碼展示了具體實現:
// Get names of output layers
vector<cv::String> names;
vector<int> outLayers = net.getUnconnectedOutLayers(); // get indices of output layers, i.e. layers with unconnected outputs
vector<cv::String> layersNames = net.getLayerNames(); // get names of all layers in the network
names.resize(outLayers.size());
for (size_t i = 0; i < outLayers.size(); ++i) // Get the names of the output layers in names
{
names[i] = layersNames[outLayers[i] - 1];
}
// invoke forward propagation through network
vector<cv::Mat> netOutput;
net.setInput(blob);
net.forward(netOutput, names);
因此,前向傳播的結果,也就是神經網絡的輸出是一個長度爲C(blob類別個數)的數組,每個類別的前四個值分別爲bounding box的x中心、y中心、寬度、高度;第五個值表示該bounding box包含一個目標的置信度;剩餘的矩陣元素是對應coco.cfg文件中每個目標類別的置信度。隨後,在代碼中,我們將每個box都關聯到了置信度最高的目標類別上。
下面的代碼展示瞭如何遍歷網絡結果並且將具有足夠高置信度的bounding box聚合爲一個向量。函數cv::minMaxLoc
勇於找到元素中的最小值和最大值,以及它們在向量中對應的位置。
// Scan through all bounding boxes and keep only the ones with high confidence
float confThreshold = 0.20;
vector<int> classIds;
vector<float> confidences;
vector<cv::Rect> boxes;
for (size_t i = 0; i < netOutput.size(); ++i)
{
float* data = (float*)netOutput[i].data;
for (int j = 0; j < netOutput[i].rows; ++j, data += netOutput[i].cols)
{
cv::Mat scores = netOutput[i].row(j).colRange(5, netOutput[i].cols);
cv::Point classId;
double confidence;
// Get the value and location of the maximum score
cv::minMaxLoc(scores, 0, &confidence, 0, &classId);
if (confidence > confThreshold)
{
cv::Rect box; int cx, cy;
cx = (int)(data[0] * img.cols);
cy = (int)(data[1] * img.rows);
box.width = (int)(data[2] * img.cols);
box.height = (int)(data[3] * img.rows);
box.x = cx - box.width/2; // left
box.y = cy - box.height/2; // top
boxes.push_back(box);
classIds.push_back(classId.x);
confidences.push_back((float)confidence);
}
}
}
步驟5:網絡輸出的後處理
通常,爲了避免多個box實際包含了同一目標,我們會利用非極大值抑制算法(NMS)來去除掉多餘的bounding box,並只保留具有最高置信度的box。OpenCV庫 提供了一個現成的函數用於實現重疊bounding box的NMS。該函數爲NMSBoxes
,其調用方式如下:
// perform non-maxima suppression
float nmsThreshold = 0.4; // Non-maximum suppression threshold
vector<int> indices;
cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
應用了NMS操作之後,多餘的bounding box將被成功移除。下圖展示了最後利用YOLOv3進行圖像目標識別的最終效果: