基於 OpenVINO 的“人體姿態識別“案例精講
Intel OpenVINO(下面簡稱 OpenVINO)可以將 Pre-trained Deep Learning Models 部署在多個平臺,並充分利用平臺特性進行推理加速。無論是官方提供的模型,還是我們使用工具進行轉換後的模型,都在 OpenVINO 推理引擎上有很好的效能。
另一方面,OpenVINO 複雜的結構、龐大的內容提高了學習難度;另外,如何將深度訓練模型解決實際問題,相信很多情況下這也是難題。本場 Chat,我們就從 OpenVINO 的“人體姿態識別”(human_pose_estimation_demo)例子出發,共同探討相關問題。
一、基於深度學習的“人體姿態識別”簡介
作為本場 Chat 的核心問題,我們首先要了解什麼是“人體姿態識別”,特別什麼是“基於深度學習的人體姿態識別”。
什麼是“人體姿態識別”?
“人體姿態識別”就是通過將圖片中已檢測到的人體關鍵點正確的聯絡起來,從而估計人體姿態、人體位置。
這裡的“關鍵點”通常對應人體上有一定自由度的關節,比如頸、肩、肘、腕、腰、膝、踝等,如下圖:
這項技術簡單來說,就是通過對人體關鍵點在三維空間相對位置的計算,達到估計人體當前的姿態、位置的目的。
“人體姿態識別”能夠做什麼?
工業方面,可以用於“越界”探測,得出比“人體識別”更為精準的定量結果,這也是我主要關注方向。
消費類方面,博物館、科技館、商場中的“3D 換衣機”已經推廣使用。
娛樂方面,類似“抖音尬舞機”等也有很多實際應用。
深度學習方法和專用硬體採集的比較
很長一段時間以來,在電影和遊戲製作,“人體姿態識別和採集”都基於專用的硬體進行比較著名的包括“魔戒”和“底特律變人”等。
即使在即今天,在實時遊戲領域,Kinect Realsense 一類的體感攝像頭仍然是實時控制的最佳選擇(時效性、精確性)。
另一方面,隨著 AI 軟硬體就似乎不斷髮展,基於深度學習的人體姿態識別技術不斷髮展,我認為它的應用場景目前看來主要是在歷史資料處理和大規模應用。
其中比較著名的是 OpenPose,其基於卷積神經網路和監督學習並以 Caffe 為框架寫成的開源庫,可以實現人的面部表情、軀幹和四肢甚至手指的跟蹤,不僅適用於單人也適用於多人,同時具有較好的魯棒性。可以稱是世界上第一個基於深度學習的實時多人二維姿態估計,是人機互動上的一個里程碑,為機器理解人提供了一個高質量的資訊維度。本場 Chat 中的 OpenVINO 例程就是基於 OpenPose 的。
二、正確配置 human_pose_estimation_demo 範例
OpenVINO 下載和配置
OpenVINO 提供高度自動化安裝方式,並且有詳細說明,我們按照預設方法安裝 OpenVINO 軟體包:
https://docs.openvinotoolkit.org/latest/_docs_install_guides_installing_openvino_windows.html
OpenVINO 例子簡介
在 OpenVINO 安裝程式安裝成功後,我們需要編譯它的例子(它的文件裡面寫得很詳細)。
1. make 生成 sln 檔案
C:\Program Files (x86)\IntelSWTools\openvino_2019.2.275\inference_engine\demos\build_demos_msvc.bat
2. 設定環境變數
C:\Program Files (x86)\IntelSWTools\openvino\bin\setupvars.bat
3. 執行 build_demos_msvc.bat,它會自動探測你機器安裝的 vs 並且生成對於的工程檔案。
4. vs2017 開啟編譯
C:\Users\Administrator\Documents\Intel\OpenVINO\omz_demos_build
5. 批生成 all_build,成功。
6. 實際執行的時候還需要進一步處理 path 目錄和 dll 版本等問題,解決方法是設定環境變數(需要重啟)。
OpenVINO 提供了豐富的例子,這裡做簡單說明,具體需要自己實驗。
一、系統自用,僅作為完整編譯而儲存(4)
- ALL_BUILD,全部編譯;
- common,全部專案都要用到的程式碼(有一些東西可以複用的);
- ie_cpu_extension,解決 extension 而存在的;
- ZERO_CHECK 可能是和構建相關的。
二、工具類(7)
- benchmark_app,預測推斷時間(to estimate deep learning inference performance on supported devices);
- calibration_tool,(標定工具,賈是不是有一篇這樣的文章?)
- format_reader,格式讀取工具;
- gflags_nothreads_static,用於把玩 gflags 之用;
- lenet_network_graph_builder,構圖很好,但是 lenet 是專用的意思嗎?
- perfcheck,引數檢查;
- validation_app,程式檢測。
三、入門級(5)
Hello 開頭的,都是比較簡單的程式程式碼。
- hello_autoresize_classification;
- hello_classification;
- hello_request_classification;
- hello_shape_infer_ssd;
- segmentation_demo 模型簡單。
四、普通級(5)
- human_pose_estimation_demo 探測人臉的方向;
- mask_rcnn_demo,RCNN 一直以來都是非常有用的東西,但是這裡提供的資料不是很多;
- object_detection_sample_ssd 和 object_detection_demo,都是有文件的(Object Detection Faster R-CNN C++ Demo );
- pedestrian_tracker_demo,行人檢測;
- style_transfer_sample 和 super_resolution_demo,遷移和超分。
五、高階級(7)
- crossroad_camera_demo,多個模型探測路上行人和車輛情況;
- interactive_face_detection_demo,級聯模型用於人臉探測;
- Multi-Channel Face Detection C++ Demo 和 multi-channel-human-pose-estimation-demo,使用多個模型,肯定是比較複雜的;
- object_detection_demo_yolov3_async 和 object_detection_demo_ssd_async,加上了非同步操作;
- security_barrier_camera_demo,級聯車牌探測;
- smart_classroom_demo 一個貼近實際的例子;
- text_detection_demo OCR 識別,也是多個模型。
六、備用級(3)
- classification_sample 和 classification_sample_async,我估計這是用來處理更為普通情況下的分類的;
- end2end_video_analytics_ie 和 end2end_video_analytics_opencv,用於過渡 OpenCV 程式碼的,對於 OpenVINO 來說價值有限;
- speech_sample 讀取 ark 資料,語音相關。
human_pose_estimation_demo 引用模型
相關文件:
https://docs.openvinotoolkit.org/latest/_models_intel_index.html
模型名稱:human-pose-estimation-0001,包含一個 XML 和一個 bin 檔案。
它的輸入是 256X256 的 3 通道圖片,輸出是一個 38X32X57 維度向量。
正確配置執行 human_pose_estimation_demo
OpenVINO 提供了範例(human_pose_estimation_demo),能夠在 CPU 上以較快速度識別出多人。
引數配置
-i E:/OpenVINO_modelZoo/head-pose-face-detection-female-and-male.mp4 -m E:/OpenVINO_modelZoo/human-pose-estimation-0001.xml -d CPU
基於這篇論文:
參考文件:
https://docs.openvinotoolkit.org/latest/_demos_human_pose_estimation_demo_README.html
Demo 程式碼講解
1. OpenVINO 基本結構
OpenVINO 在其程式主體中,主要實現現有模型的推斷(infer)工作,其它的所有工作都圍繞此展開。
一般來說,基本結構共包含 8 個部分,其框圖如:
但是在不同的例子中是有不一樣的地方的。同時 Intel 在 sample 程式碼中,也有前後不一致的地方,需要注意批判學習。
這一部分官方的參考文件為:
https://docs.openvinotoolkit.org/latest/_docs_IE_DG_inference_engine_intro.html
2. humanposeestimation_demo 的基本結構
在本例中,我們更加關注 human_pose_estimation_demo 是如何“圍繞此(infer)展開”,實際上 OpenVINO 已經將這快的內容進行了封裝。結構上來看,本例一共 5 個 cpp 檔案(另有 5 個 hpp 檔案),其中主線程式碼為 main.cpp,其它程式碼都被 main.cpp 所引用,具體來看:
bool ParseAndCheckCommandLine(int argc, char* argv[]) {
// ---------------------------Parsing and validation of input args--------------------------------------
gflags::ParseCommandLineNonHelpFlags(&argc, &argv, true);
if (FLAGS_h) {
showUsage();
showAvailableDevices();
return false;
}
std::cout << "Parsing input parameters" << std::endl;
if (FLAGS_i.empty()) {
throw std::logic_error("Parameter -i is not set");
}
if (FLAGS_m.empty()) {
throw std::logic_error("Parameter -m is not set");
}
return true;
}
ParseAndCheckCommandLine 主要是對命令列進行的操作,後面再程式碼除錯中,我們可以直接將引數寫到邏輯中。
HumanPoseEstimator estimator(FLAGS_m, FLAGS_d, FLAGS_pc); //這裡採用類的方式建立 estimator
建立 HumanPoseEstimator ,用來實現人體骨骼的識別,其中 HumanPoseEstimator 在 human_pose_estimator.cpp 檔案中具體實現
do {
double t1 = static_cast<double>(cv::getTickCount());
SumOfFrames = SumOfFrames + 1;
if (SumOfFrames == FRAMEBLOCK)
{
SumOfFrames = 0;//歸零並且處理
std::vector<HumanPose> poses = estimator.estimate(image); //已經獲得humanpose
if (poses.size() != 0)
{……
在 main.cpp 中,特別注意這裡的 do 函式塊,這個整個程式的核心部分(因為我們處理的是視訊,那麼肯定需要一個迴圈來不斷都資料、處理資料),在這塊,主要是通過 std::vector poses = estimator.estimate(image);
來將獲得處理的結果。
…… renderHumanPose(poses, image); //這個函式是什麼意思,應該是獲得某個結果的意思
}
cv::Mat fpsPane(35, 155, CV_8UC3);
fpsPane.setTo(cv::Scalar(153, 119, 76));
cv::Mat srcRegion = image(cv::Rect(8, 8, fpsPane.cols, fpsPane.rows));
cv::addWeighted(srcRegion, 0.4, fpsPane, 0.6, 0, srcRegion);
std::stringstream fpsSs;
fpsSs << "FPS: " << int(1000.0f / inferenceTime * 100) / 100.0f;
cv::putText(image, fpsSs.str(), cv::Point(16, 32),
cv::FONT_HERSHEY_COMPLEX, 0.8, cv::Scalar(0, 0, 255));
最後處理得到的結果需要顯示,這裡採用的是 renderHumanPose 函式來畫 18 個點,同時還要將 FPS 等資訊繪製上去。
3. 函式檔案如何具體實現
在瞭解完 main.cpp 後,我們來看其它幾個檔案是如何具體實現的。
Human_pose.cpp 中最重要的功能就是定義了表示骨骼的這個資料結構 humanpose,其實質是一組放在 vector 中的 Point2f。
#include <vector>
#include "human_pose.hpp"
namespace human_pose_estimation {
struct HumanPose {
HumanPose(const std::vector<cv::Point2f>& keypoints = std::vector<cv::Point2f>(),
const float& score = 0);
std::vector<cv::Point2f> keypoints;
float score;
};
}
human_pose_estimator.cpp 實現了整個推斷過程,重點來看其 estimate 函式。
std::vector<HumanPose> HumanPoseEstimator::estimate(const cv::Mat& image) {
CV_Assert(image.type() == CV_8UC3);
cv::Size imageSize = image.size();
if (inputWidthIsChanged(imageSize)) {
auto input_shapes = network.getInputShapes();
std::string input_name;
InferenceEngine::SizeVector input_shape;
std::tie(input_name, input_shape) = *input_shapes.begin();
input_shape[2] = inputLayerSize.height;
input_shape[3] = inputLayerSize.width;
input_shapes[input_name] = input_shape;
network.reshape(input_shapes);
executableNetwork = ie.LoadNetwork(network, targetDeviceName);
request = executableNetwork.CreateInferRequest();
}
InferenceEngine::Blob::Ptr input = request.GetBlob(network.getInputsInfo().begin()->first);//這裡是塞資料的,最後賽道buffer中
matU8ToBlob<float_t>(image, input); //這裡的相關內容是可以被替換掉的
//以下是原來的方法
//InferenceEngine::Blob::Ptr input = request.GetBlob(network.getInputsInfo().begin()->first);
//auto buffer = input->buffer().as<InferenceEngine::PrecisionTrait<InferenceEngine::Precision::FP32>::value_type *>();
//preprocess(image, static_cast<float*>(buffer));
request.Infer();//使用的是同步方法
InferenceEngine::Blob::Ptr pafsBlob = request.GetBlob(pafsBlobName);
InferenceEngine::Blob::Ptr heatMapsBlob = request.GetBlob(heatmapsBlobName);
CV_Assert(heatMapsBlob->getTensorDesc().getDims()[1] == keypointsNumber + 1);
InferenceEngine::SizeVector heatMapDims =
heatMapsBlob->getTensorDesc().getDims();
std::vector<HumanPose> poses = postprocess(
heatMapsBlob->buffer(),
heatMapDims[2] * heatMapDims[3],
keypointsNumber,
pafsBlob->buffer(),
heatMapDims[2] * heatMapDims[3],
pafsBlob->getTensorDesc().getDims()[1],
heatMapDims[3], heatMapDims[2], imageSize);
return poses;
}
我們特別需要注意,主體主要分為 3 個部分,一部分是 preprocess,一部分是 postprescess,其中 preprocess 的功能是將圖片塞入模型中。
void HumanPoseEstimator::preprocess(const cv::Mat& image, float* buffer) const {
cv::Mat resizedImage;
double scale = inputLayerSize.height / static_cast<double>(image.rows);
cv::resize(image, resizedImage, cv::Size(), scale, scale, cv::INTER_CUBIC);
cv::Mat paddedImage;
cv::copyMakeBorder(resizedImage, paddedImage, pad(0), pad(2), pad(1), pad(3),
cv::BORDER_CONSTANT, meanPixel);
std::vector<cv::Mat> planes(3);
cv::split(paddedImage, planes);
for (size_t pId = 0; pId < planes.size(); pId++) {
cv::Mat dst(inputLayerSize.height, inputLayerSize.width, CV_32FC1,
reinterpret_cast<void*>(
buffer + pId * inputLayerSize.area()));
planes[pId].convertTo(dst, CV_32FC1);
}
}
這個部分是可以替換的,這裡註釋的部分是原始碼,我們採用 matU8ToBlob 的方法可以進行替換。
matU8ToBlob<float_t>(image, input); //這裡的相關內容是可以被替換掉的
//以下是原來的方法
//InferenceEngine::Blob::Ptr input = request.GetBlob(network.getInputsInfo().begin()->first);
//auto buffer = input->buffer().as<InferenceEngine::PrecisionTrait<InferenceEngine::Precision::FP32>::value_type *>();
//preprocess(image, static_cast<float*>(buffer));
request.Infer();//使用的是同步方法
postprescess 是將資料處理。
std::vector<HumanPose> HumanPoseEstimator::postprocess(
const float* heatMapsData, const int heatMapOffset, const int nHeatMaps,
const float* pafsData, const int pafOffset, const int nPafs,
const int featureMapWidth, const int featureMapHeight,
const cv::Size& imageSize) const {
std::vector<cv::Mat> heatMaps(nHeatMaps);
for (size_t i = 0; i < heatMaps.size(); i++) {
heatMaps[i] = cv::Mat(featureMapHeight, featureMapWidth, CV_32FC1,
reinterpret_cast<void*>(
const_cast<float*>(
heatMapsData + i * heatMapOffset)));
}
resizeFeatureMaps(heatMaps);
std::vector<cv::Mat> pafs(nPafs);
for (size_t i = 0; i < pafs.size(); i++) {
pafs[i] = cv::Mat(featureMapHeight, featureMapWidth, CV_32FC1,
reinterpret_cast<void*>(
const_cast<float*>(
pafsData + i * pafOffset)));
}
resizeFeatureMaps(pafs);
std::vector<HumanPose> poses = extractPoses(heatMaps, pafs);
correctCoordinates(poses, heatMaps[0].size(), imageSize);
return poses;
}
為什麼要 postprocess 而不是直接輸出?是因為這個模型輸出的結果比較複雜。
最後通過 extractPoses,將 19+38 的結果轉成 18 個點。
std::vector<HumanPose> HumanPoseEstimator::extractPoses(
const std::vector<cv::Mat>& heatMaps,
const std::vector<cv::Mat>& pafs) const {
std::vector<std::vector<Peak> > peaksFromHeatMap(heatMaps.size());
FindPeaksBody findPeaksBody(heatMaps, minPeaksDistance, peaksFromHeatMap);//尋找軀幹主體
cv::parallel_for_(cv::Range(0, static_cast<int>(heatMaps.size())),
findPeaksBody);
int peaksBefore = 0;
for (size_t heatmapId = 1; heatmapId < heatMaps.size(); heatmapId++) {
peaksBefore += static_cast<int>(peaksFromHeatMap[heatmapId - 1].size());
for (auto& peak : peaksFromHeatMap[heatmapId]) {
peak.id += peaksBefore;
}
}
std::vector<HumanPose> poses = groupPeaksToPoses(
peaksFromHeatMap, pafs, keypointsNumber, midPointsScoreThreshold,
foundMidPointsRatioThreshold, minJointsNumber, minSubsetScore);
return poses;
}
render_human_pose.cpp 和 peak.cpp 是用於顯示最終結果的,這裡不展開講解。
三、現實場景下“人體姿態識別”識別研究
抽取 18 個點,做簡單的越界分析
既然以經能夠從視訊中抽取人體骨骼,並且對應 18 個數據點:
那麼就能夠做定量分析。
對於這個視訊,使用 MarkMan 能夠測量出關鍵領域的位置,那麼最簡單的想法就是首先獲得“人的中心”這個點,當這個點位於敏感區域的時候進行報警。
但是這種方法很粗糙,我們希望得到的是這個敏感區域裡面,沒有人體的任何一個位置,因此首先對所有的點進行排序,而後判斷
bool SortbyXaxis(const cv::Point2f & a, const cv::Point2f &b)
{
return a.x > b.x;
}
//而後對所有的點進行這樣處理
HumanPose firstHumanPose = poses[0];
std::vector<cv::Point2f> firstKeypoints = firstHumanPose.keypoints;
sort( firstKeypoints .begin(), firstKeypoints .end(), SortbyYaxis );
if (! (firstKeypoints[0].x < 369 || firstKeypoints[firstKeypoints.size() - 1].x > 544))
{
std::stringstream inRanges;
inRanges << "inRanges! ";
cv::putText(image, inRanges.str(), cv::Point(16, 64),
cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(0, 0, 255));
}
這樣就能夠好許多。
更接近實際的情況
前面的情況還是過於簡單,這個視訊更接近實際情況:
比如地上有這條安全線,傾斜的,就是不能越過,應該如何來處理?
首先還是量出這條線(固定物鏡關係),並且我們能夠繪製出這條線。下面,首先要做一個簡單的數學複習。
K = (y1-y2)/(x1-x2),當 K1>K2 的時候點在左邊,而在左邊灰色區域的時候,絕對在左邊,在右邊藍色區域的時候,絕對在右邊。 據此編寫函式。
bool PointIsLeftLine(cv::Point2f point, cv::Point2f PointLineLeft, cv::Point2f PointLineRight)
{
//邊界外直接返回
if (point.x < 0)
return false;
if (point.x <= PointLineLeft.x)
return true;
if (point.x > PointLineRight.x)
return false;
//在邊界內的情況,通過計算斜率
if (PointLineRight.x == PointLineLeft.x)
assert("error PointLineRight.x == PointLineLeft.x");
float kLine = (PointLineRight.y - PointLineLeft.y) / (PointLineRight.x - PointLineLeft.x);
float k = (point.y - PointLineLeft.y) / (point.x - PointLineLeft.x);
return (k >= kLine);
}
並且分別對兩個腳進行處理:
bRight = PointIsLeftLine(pointRightFoot, cv::Point2f(1017, 513), cv::Point2f(433, image.rows - 1));
bLeft = PointIsLeftLine(pointLeftFoot, cv::Point2f(1017, 513), cv::Point2f(433, image.rows - 1));
加上一些影象繪製:
if (bRight|| bLeft)
{
line(image, cv::Point(1017, 513), cv::Point(433, image.rows - 1), cv::Scalar(0, 0, 255), 8);
}
else
{
line(image, cv::Point(1017, 513), cv::Point(433, image.rows - 1), cv::Scalar(0, 255, 0), 8);
}
得到這樣的結果:
更加符合實際情況。
四、進一步解決識別效能問題
OpenVINO 的例子雖然速度不錯,但是隻有 7~8 的幀率(1980*1024),對於實時應用來說還是慢了,為了解決效能問題,做以下研究。
原始的抽幀
對於這樣一個問題,想提高速度,能夠想到的最簡單、最直接的方法就是“抽幀”。比如新增一個計數器。
這裡,只有當 SumofFrames 達到 FRAMEBLOCK 的時候,才進行下面的影象處理,否則只是顯示影象本身而不處理。
但是這樣做,得到的結果很詭異:就是這個人走走停停的。這樣想來,這個影象處理的過程,還是不能放到主執行緒中去,還是要獨立出來。
就是起碼要 2 個執行緒。這個時候 Console 程式的能力就不夠了,所以開始修改 GOMFCTemplate。
對 human_pose_estimation_demo 結構的進一步理解
當我開始移植 human_pose_estimation_demo 到 GOMfcTemplate 中的時候,才發現它的結構化方法提供了非常多的便利:
引入它的檔案:
執行它的過程:
當然看上去簡單,細節還是很多的,這個放到第 4 個部分來講。
Dshow 提供的加成
得益於其良好的結構,可以很方便地移植到 GoMFCTemplate2 平臺,測試發現速度已經有了明顯的提升:
注意,這裡已經將視訊設定成了 1920*1080 的原始大小。可以看到,最上面的 VCam 是虛擬攝像頭,它比較流暢,而 GOMfctemplate 裡面的演算法處理也是比較快的,並且 DShow 自動進行了抽幀處理!原理我還沒有考證,但是結果看上去是這樣的,這也是採用專業基礎庫的紅利吧。
關於 GOMFCtemplate,需要說的東西就很多了,有興趣可以延伸關注。
注意事項
- 因為 OpenVINO 的原因,所有的專案不要放在有中文和空格的地方;
- 正確設定解析度進行測試,否則小解析度測試不出來什麼效果;
- matU8ToBlob 等函式在引用過來的時候會批量報錯,需要改寫。
五、參考文章和資源
完整例子程式碼:
https://pan.baidu.com/s/1JIAL6r8OjYQq3rFXhU78zQ
提取碼:ppwl
來自為知筆記(Wiz)