超详细!使用OpenCV深度学习模块在图像分类下的应用实践
專注計算機視覺前沿資訊和技術干貨
微信公眾號:極市平臺
官網:https://www.cvmart.net/
極市導讀:本文來自6月份出版的新書《OpenCV深度學習應用與性能優化實踐》,由Intel與阿里巴巴高級圖形圖像專家聯合撰寫,系統地介紹了OpenCV DNN 推理模塊原理和實踐。
深度學習理論的廣泛研究促進了其在不同場景的應用。在計算機視覺領域,圖像分類、目標檢測、語義分割和視覺風格變換等基礎任務的性能也因為采用了深度學習的方法而有了飛躍性的提升。本章將為讀者梳理深度學習方法在這些基本應用場景的應用情況,并結合OpenCV深度學習模塊的示例程序,從源代碼和實際運行兩個層面進行講解。下文對書中圖像分類部分內容進行摘錄:
圖像分類是計算機視覺領域的基礎任務之一,在各種基于視覺的人工智能應用中,圖像分類都扮演著重要的角色。例如,在智能機器人應用中,我們需要對所采集的視頻中的每一幀進行主要物體的檢測和分類,并以此作為進一步決策的基礎。
近些年,圖像分類與深度學習的飛速發展有著密不可分的關系。在2012年的ILSVRC (ImageNet Large Scale Visual Recognition Competition,ImageNet大規模視覺識別挑戰賽)大賽上,AlexNet橫空出世,以壓倒性優勢戰勝了傳統圖像分類算法而奪得冠軍,開啟了計算機視覺領域的深度學習革命。2015年,ResNet首次在圖像分類準確度上戰勝人類。2017年,隨著SENet的奪冠,最后一屆ILSVRC大賽落下帷幕。下面為大家梳理一下歷屆ILSVRC大賽中出現的經典網絡結構。
圖像分類經典網絡結構
自2012年ILSVRC大賽AlexNet奪冠以來,直至2017年最后一屆SENet奪冠,所有冠軍都被各種深度神經網絡所摘得。歷屆ILSVRC大賽的經典網絡結構及其特點如表9-1所示。
這些網絡結構不僅可應用在圖像分類中,而且可作為其他計算機視覺任務(如目標檢測、語義分割和視覺風格變換)的骨干(backbone)網絡,用來提取圖像特征。因此,它們對整個計算機視覺技術的發展有著深遠的影響。
下面我們摘錄OpenCV官方Wiki上的DNN模塊運行效率統計表,看一下AlexNet、GoogLeNet和ResNet-50在OpenCV DNN模塊中的運行效率。
測試系統軟硬件配置如下:
各軟件組件的版本信息如表9-2所示。
CPU實現的運行時間如表9-3所示,該時間取的是50次運行的中位數時間,中位數時間可以排除多次推理運算中某些過于異常的值對平均值的干擾。另外,所有網絡模型都采用32位浮點數據格式進行計算。在神經網絡的推理計算中,可以采用量化方法把32位浮點精度的模型參數降低到16位浮點精度以節省數據讀取帶寬提高運算效率,但是并不是所有算法都支持針對16位浮點精度的實現,為了便于比較,測試都采用32位浮點精度。
GPU實現的運行時間如表9-4所示。
從上面的數據可以看到,DNN模塊的OpenCL實現跟原生C++實現性能相近,而Intel-Caffe MKLDNN的加速性能最好,原因是多方面的。首先MKLDNN是針對Intel CPU進行高度優化的神經網絡計算庫,能夠充分發揮Intel CPU的性能。其次,該測試使用的CPU硬件性能比較強勁(8核心,4.0GHz運行頻率),而集成的GPU是中低配置。最后,測試的3種網絡模型的運算量不算太大,未能充分發揮GPU的并發特性。
接下來,我們以GoogLeNet(Inception-v1)為例,詳細講解其網絡結構和設計原理,然后結合OpenCV中的圖像分類示例程序講解GoogLeNet模型的實際使用。
GoogLeNet
GoogLeNet自2014年提出以來,總共演進了4個版本,由于第1版是后續幾個版本的基礎,本節主要介紹2014年的第1版,即GoogLeNet v1。
GoogLeNet v1是2014年ILSVRC大賽的冠軍模型,它延續了自LeNet以來的典型卷積網絡結構,即多個卷積層前后堆疊,然后通過全連接層輸出最終的特征值。GoogLeNet的結構如圖9-1所示。
下面對圖9-1中各列進行解釋。type列表示層或者模塊的類型,其中inception代表一個Inception模塊,GoogLeNet中總共堆疊了9個Inception模塊,convolution表示卷積層,max pool表示最大池化層,avg pool表示平均池化層,dropout代表隨機裁剪操作,linear是全連接層,softmax表示最后對輸出特征值進行sotfmax操作。patch size列表示卷積核大小,stride表示卷積運算的步進值。output size列表示輸出特征圖的長、寬和通道數。Depth列表示該層或者模塊重復連接的次數。#1×1,#3×3,#5×5列分別表示Inception模塊中的1×1,3×3,5×5卷積核大小的卷積分支的輸出通道數。pool proj列表示池化投影的輸出通道數。#3×3 reduce和#5×5 reduce列表示Inception結構中3×3和5×5卷積核卷積之前的1×1卷積的輸出通道數。params列表示參數數目。ops列表示運算量。Inception模塊是GoogLeNet的最大創新點,它的初衷是增加卷積核尺寸種類的同時降低訓練參數數量,下面對Inception v1模塊進行講解,它的結構如圖9-1所示。
Inception模塊使用1×1卷積對前層數據進行降維處理并分成多路,然后用3×3,5×5卷積對降維后的分支進行卷積運算,同時將各個卷積結果和3×3最大池化的結果按通道進行連接。這種創新的結構使得網絡參數大大降低的同時保留了很好的特征表達能力,達到了深度和參數數量的雙贏。
為什么使用多種尺寸的卷積核有助于提高特征表達能力呢?我們以圖9-3為例,最左邊的狗占據了圖的大部分,中間的狗占了圖的一部分,而最右邊的狗占了圖的很小一部分。采用多種尺寸的卷積核可以學習到不同尺度的特征,使網絡具有更好的特征適應性。
接下來,我們結合DNN模塊圖像分類示例程序看一下圖像分類應用的具體實現。
圖像分類程序源碼分析
我們借助OpenCV的示例程序來介紹圖像分類應用的主要步驟。OpenCV DNN模塊示例程序囊括了各種不同應用場景,它們有著相似的代碼結構和流程,如圖9-4所示。各種示例應用源代碼的區別主要體現在最后一步:推理結果的解析和可視化。本節將詳細講解代碼的每個步驟,之后各節的源碼分析將重點聚焦于應用特定的參數及推理結果的解析和可視化。
下面分析圖像分類示例程序源碼。
首先引入必要的頭文件,參見代碼清單9-1。其中,fstream和sstream是C++標準庫頭文件,用于文件讀取和文本處理。dnn.hpp、imgproc.hpp、highgui.hpp提供OpenCV API聲明,common.hpp提供了一些DNN示例程序通用的函數,例如,查找輸入文件位置,從模型配置文件中讀取默認的運行時參數等。
9-1 引入必要的頭文件
#include <fstream> #include <sstream> #include <opencv2/dnn.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include "common.hpp"代碼清單9-2定義了命令行參數,下面逐一講解。
9-2 命令行參數定義
std::string keys="{ help h | | Print help message. }""{ @alias | | An alias name of model to extract preprocessing parameters from models.yml file. }""{ zoo | models.yml | An optional path to file with preprocessing parameters }""{ input i | | Path to input image or video file. Skip this argument to capture frames from a camera.}""{ framework f | | Optional name of an origin framework of the model. Detect it automatically if it does not set. }""{ classes | Optional path to a text file with names of classes. }""{ backend | 0 | Choose one of computation backends: ""0: automatically (by default), ""1: Halide language (http://halide-lang.org/), ""2: Intel's Deep Learning Inference Engine (https://software.intel.com/openvino-toolkit), ""3: OpenCV implementation }""{ target | 0 | Choose one of target computation devices: ""0: CPU target (by default), ""1: OpenCL, ""2: OpenCL fp16 (half-float precision), ""3: VPU }";接下來引用命名空間,參見代碼清單9-3。我們的代碼用到了cv和dnn命名空間中的API,通過顯式聲明命名空間,方便后續的API調用。
9-3 聲明命名空間及定義全局變量
using namespace cv; using namespace dnn;接下來定義用于存放類別名稱的變量classes:
std::vector<std::string> classes;下面進入主函數。
首先,解析命令行參數,參見代碼清單9-4。
9-4 主函數(解析命令行參數)
int main(int argc, char** argv) {CommandLineParser parser(argc, argv, keys);const std::string modelName=parser.get<String>("@alias");const std::string zooFile=parser.get<String>("zoo");keys +=genPreprocArguments(modelName, zooFile);parser=CommandLineParser(argc, argv, keys);parser.about("Use this script to run classification deep learning networks using OpenCV.");if (argc==1 || parser.has("help")){parser.printMessage();return 0;}float scale=parser.get<float>("scale");Scalar mean=parser.get<Scalar>("mean");bool swapRB=parser.get<bool>("rgb");int inpWidth=parser.get<int>("width");int inpHeight=parser.get<int>("height");String model=findFile(parser.get<String>("model"));String config=findFile(parser.get<String>("config"));String framework=parser.get<String>("framework");int backendId=parser.get<int>("backend");int targetId=parser.get<int>("target");如果命令行參數提供了類別文件路徑,則解析類別文件并將類別名稱存儲到全局變量classes,參見代碼清單9-5。
9-5 主函數(類別文件解析)
if (parser.has("classes")) {std::string file=parser.get<String>("classes");std::ifstream ifs(file.c_str());if (!ifs.is_open())CV_Error(Error::StsError, "File " + file + " not found");std::string line;while (std::getline(ifs, line)){classes.push_back(line);} }接下來進行異常情況檢查,包括命令行參數異常,以及缺失模型文件異常,參加代碼清單9-6。
9-6 主函數(異常情況檢查)
if (!parser.check()){parser.printErrors();return 1;}CV_Assert(!model.empty());加載網絡模型,創建DNN模塊網絡對象,并設置加速后端和目標運算設備,參加代碼清單9-7。
9-7 主函數(初始化網絡并創建顯示窗口)
Net net=readNet(model, config, framework); net.setPreferableBackend(backendId); net.setPreferableTarget(targetId);接下來,創建用于顯示結果的窗口對象。代碼如下:
static const std::string kWinName="Deep learning image classification in OpenCV"; namedWindow(kWinName, WINDOW_NORMAL);然后,創建圖像輸入對象cap,用于讀取指定的圖片、視頻文件,參見代碼清單9-8。如果沒有指定圖片或視頻文件,則從攝像頭讀取視頻幀。
9-8 主函數(創建圖像輸入對象)
VideoCapture cap; if (parser.has("input")) cap.open(parser.get<String>("input")); elsecap.open(0);接下來進入圖像處理循環,循環起始部分通過cap對象讀取一幀圖像,參見代碼清單9-9。
9-9 圖像處理循環(讀取一幀圖像)
Mat frame, blob; while (waitKey(1) < 0) {cap >> frame;if (frame.empty()){waitKey();break;}然后調用blobFromImage()函數將讀入的圖像轉換成網絡模型的輸入(blob),并設置網絡對象,參見代碼清單9-10。blobFromImage()函數會對圖像進行一系列的預處理,包括調整大小、減均值、交換紅藍顏色通道等,最終返回一個一維數組(N、C、H、W)。其中,N代表批大小,實時應用中通常為1,即一次處理一幀圖像數據;C代表圖像通道數,一般為3,即R、G、B三種顏色;H、W分別代表圖像的高度和寬度。
9-10 圖像處理循環(設置網絡輸入)
blobFromImage(frame, blob, scale, Size(inpWidth, inpHeight), mean, swapRB, false); net.setInput(blob);接下來運行網絡模型推理,代碼如下:
Mat prob=net.forward();網絡推理的輸出數據對象prob包含1000個概率值,分別對應1000個圖像類別。
至此,網絡推理運算部分結束,接下來進行推理結果的解析和可視化,參見代碼清單9-11和代碼清單9-12。
9-11 圖像處理循環(解析網絡推理輸出)
Point classIdPoint; double confidence; // 找到概率值最大的類別id,該類別為圖像所屬分類 minMaxLoc(prob.reshape(1, 1), 0, &confidence, 0, &classIdPoint); int classId=classIdPoint.x;9-12 圖像處理循環(可視化推理結果)
// 獲取網絡推理運算耗時,并疊加到原始圖像上 std::vector<double> layersTimes; double freq=getTickFrequency() / 1000; double t=net.getPerfProfile(layersTimes) / freq; std::string label=format("Inference time: %.2f ms", t); putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX,0.5, Scalar(0, 255, 0)); // 將圖像類別標簽和概率值疊加到原始圖像上 label=format("%s: %.4f", (classes.empty() ?format("Class #%d", classId).c_str() :classes[classId].c_str()),confidence); putText(frame, label, Point(0, 40), FONT_HERSHEY_SIMPLEX,0.5, Scalar(0, 255, 0)); // 顯示圖像 imshow(kWinName, frame);}// 循環結束,退出主函數return 0; }以上內容摘自**《OpenCV深度學習應用與性能優化實踐》**一書,經出版方授權發布。
關注極市平臺公眾號(ID:extrememart),獲取計算機視覺前沿資訊/技術干貨/招聘面經等
總結
以上是生活随笔為你收集整理的超详细!使用OpenCV深度学习模块在图像分类下的应用实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Label Assign综述:提升目标检
- 下一篇: 『PPYOLO tiny尝鲜』基于Pad