OpenPose C++实现多人姿态估计 - 代码实现
OpenPose C++实现多人姿态估计代码实现flyfish#include<opencv2/dnn.hpp>#include<opencv2/imgproc.hpp>#include<opencv2/highgui.hpp>#include<iostream>#include<chrono>#include<r...
·
OpenPose C++实现多人姿态估计
代码实现
flyfish
#include<opencv2/dnn.hpp>
#include<opencv2/imgproc.hpp>
#include<opencv2/highgui.hpp>
#include<iostream>
#include<chrono>
#include<random>
#include<set>
#include<cmath>
cv::Mat g_test_output_frame;
struct KeyPoint{
KeyPoint(cv::Point point,float probability){
this->id = -1;
this->point = point;
this->probability = probability;
}
int id;
cv::Point point;
float probability;
};
//重载 << 输出 keypoint
std::ostream& operator << (std::ostream& os, const KeyPoint& kp)
{
os << "Id:" << kp.id << ", Point:" << kp.point << ", Prob:" << kp.probability << std::endl;
return os;
}
////////////////////////////////
struct ValidPair{
ValidPair(int aId,int bId,float score){
this->aId = aId;
this->bId = bId;
this->score = score;
}
int aId;
int bId;
float score;
};
//重载 << 输出 pair
std::ostream& operator << (std::ostream& os, const ValidPair& vp)
{
os << "A:" << vp.aId << ", B:" << vp.bId << ", score:" << vp.score << std::endl;
return os;
}
////////////////////////////////
//重载 << 输出 vector
template < class T > std::ostream& operator << (std::ostream& os, const std::vector<T>& v)
{
os << "[";
bool first = true;
for (typename std::vector<T>::const_iterator ii = v.begin(); ii != v.end(); ++ii, first = false)
{
if(!first) os << ",";
os << " " << *ii;
}
os << "]";//<<std::endl;;
return os;
}
//重载 << 输出 set
template < class T > std::ostream& operator << (std::ostream& os, const std::set<T>& v)
{
os << "[";
bool first = true;
for (typename std::set<T>::const_iterator ii = v.begin(); ii != v.end(); ++ii, first = false)
{
if(!first) os << ",";
os << " " << *ii;
}
os << "]";
return os;
}
////////////////////////////////
//COCO的模型
const int kPoints = 18;
//为每个关键点命名,一共18个,不包含背景
//鼻子-0, 脖子-1,右肩-2,右肘-3,右手腕-4,左肩-5,左肘-6,左手腕-7,右臀-8,右膝盖-9,
//右脚踝-10,左臀-11,左膝盖-12,左脚踝-13,右眼-14,左眼-15,有耳朵-16,左耳朵-17
const std::string keypointsMapping[] = {
"Nose", "Neck",
"R-Sho", "R-Elb", "R-Wr",
"L-Sho", "L-Elb", "L-Wr",
"R-Hip", "R-Knee", "R-Ank",
"L-Hip", "L-Knee", "L-Ank",
"R-Eye", "L-Eye", "R-Ear", "L-Ear"
};
//posePairs看下一个结构,posePairs的输出索引 例如1,2 对应,31,32;1,5对应39,40
//一共19对
const std::vector<std::pair<int,int>> mapIdx = {
{31,32}, {39,40}, {33,34}, {35,36}, {41,42}, {43,44},
{19,20}, {21,22}, {23,24}, {25,26}, {27,28}, {29,30},
{47,48}, {49,50}, {53,54}, {51,52}, {55,56}, {37,38},
{45,46}
};
//0分别与1,14,15连接
//Nose分别与Neck,Right Eye,Left Eye连接
//一共19对
const std::vector<std::pair<int,int>> posePairs = {
{1,2}, {1,5}, {2,3}, {3,4}, {5,6}, {6,7},
{1,8}, {8,9}, {9,10}, {1,11}, {11,12}, {12,13},
{1,0}, {0,14}, {14,16}, {0,15}, {15,17}, {2,17},
{5,16}
};
void output_draw_contours(cv::OutputArrayOfArrays& in)
{
cv::drawContours(g_test_output_frame, in, -1, cv::Scalar::all(255));
cv::imshow("Contours", g_test_output_frame);
}
//对 Confidence Map 采用 NMS(Non Maximum Suppression) 来检测关键点.
//probMap smoothProbMap maskedProbMap
//probMap->smoothProbMap->maskedProbMap
//findContours函数
// contour
// 美[ 'kɑːntʊr ]
// 英[ 'kɒntʊə ]
// n. 等高线 / 周线 / 电路 / 概要
// v. 使与某轮廓吻合 / 循地形轮廓而行 / 画轮廓 / 画等高线
//void cv::findContours(
// cv::InputOutputArray image, // 输入的8位单通道“二值”图像
// cv::OutputArrayOfArrays contours, // 包含points的vectors的vector
// cv::OutputArray hierarchy, // (可选) 拓扑信息
// int mode, // 轮廓检索模式
// int method, // 近似方法
// cv::Point offset = cv::Point() // (可选) 所有点的偏移
//);
//下面的findContours是当前使用的
//void cv::findContours(
// cv::InputOutputArray image, // 输入的8位单通道“二值”图像
// cv::OutputArrayOfArrays contours, // 包含points的vectors的vector 等同于 std::vector<std::vector<cv::Point> >
// int mode, // 轮廓检索模式
// int method, // 近似方法
// cv::Point offset = cv::Point() // (可选) 所有点的偏移
//);
//当前使用的轮廓检索模式是cv::RETR_TREE
//cv::RETR_EXTERNAL:表示只提取最外面的轮廓;
//cv::RETR_LIST:表示提取所有轮廓并将其放入列表;
//cv::RETR_CCOMP:表示提取所有轮廓并将组织成一个两层结构,其中顶层轮廓是外部轮廓,第二层轮廓是“洞”的轮廓;
//cv::RETR_TREE:表示提取所有轮廓并组织成轮廓嵌套的完整层级结构。
//---------------------------------------------------------------------------------------------
//void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT ) ;
//功能:对输入的图像src进行高斯滤波后用dst输出。
//参数:src和dst当然分别是输入图像和输出图像。Ksize为高斯滤波器模板大小,sigmaX和sigmaY分别为高斯滤波在横线和竖向的滤波系数。borderType为边缘扩展点插值类型。
void getKeyPoints(cv::Mat& probMap,double threshold,std::vector<KeyPoint>& keyPoints){
//对于每个关键点,我们将阈值应用于置信度图(在本例中为0.1)
cv::Mat smoothProbMap;
cv::GaussianBlur( probMap, smoothProbMap, cv::Size( 3, 3 ), 0, 0 );
cv::Mat maskedProbMap;
//去噪声 https://docs.opencv.org/4.2.0/d7/d1b/group__imgproc__misc.html#gae8a4a146d1ca78c626a53577199e9c57
cv::threshold(smoothProbMap,maskedProbMap,threshold,255,cv::THRESH_BINARY);
maskedProbMap.convertTo(maskedProbMap,CV_8U,1);
//-显示3个不同类型的mat
cv::imshow("probMap", probMap);
cv::imshow("smoothProbMap", smoothProbMap);
cv::imshow("maskedProbMap", maskedProbMap);
//关键点区域 keypoint region
//找出对应于关键点的所有区域的轮廓(contour)
std::vector<std::vector<cv::Point> > contours;
cv::findContours(maskedProbMap,contours,cv::RETR_TREE,cv::CHAIN_APPROX_SIMPLE);
std::cout<<contours;
output_draw_contours(contours);
//对于每个关键点轮廓区域,找到最大值.
for(size_t i = 0; i < contours.size();++i){
cv::Mat blobMask = cv::Mat::zeros(smoothProbMap.rows,smoothProbMap.cols,smoothProbMap.type());
//填充凸多边形,只需要提供凸多边形的顶点
cv::fillConvexPoly(blobMask,contours[i],cv::Scalar(1));
double maxVal;
cv::Point maxLoc;
//提取关键点区域的局部最大值,与之前目标检测相似
// MatExpr cv::Mat::mul ( InputArray m,double scale = 1) const
// Performs an element-wise multiplication or division of the two matrices.
// The method returns a temporary object encoding per-element array multiplication, with optional scale.
// Note that this is not a matrix multiplication that corresponds to a simpler "\*" operator.
//https://docs.opencv.org/4.2.0/d3/d63/classcv_1_1Mat.html#a385c09827713dc3e6d713bfad8460706
// 执行两个矩阵的逐元素乘法或除法。
// 该方法返回编码每个元素数组乘法的临时对象,具有可选的小数位数位数。请注意,这不是与更简单的“\*”运算符相对应的矩阵乘法。
cv::minMaxLoc(smoothProbMap.mul(blobMask),0,&maxVal,0,&maxLoc);
//我们为每一个关键点存储坐标(x,y),置信度分数
//maxLoc 是坐标
keyPoints.push_back(KeyPoint(maxLoc, probMap.at<float>(maxLoc.y,maxLoc.x)));
}
}
void populateColorPalette(std::vector<cv::Scalar>& colors,int nColors){
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis1(64, 200);
std::uniform_int_distribution<> dis2(100, 255);
std::uniform_int_distribution<> dis3(100, 255);
for(int i = 0; i < nColors;++i){
colors.push_back(cv::Scalar(dis1(gen),dis2(gen),dis3(gen)));
}
}
void splitNetOutputBlobToParts(cv::Mat& netOutputBlob,const cv::Size& targetSize,std::vector<cv::Mat>& netOutputParts){
//size是指多维矩阵中每一维的大小,nParts是57
int nParts = netOutputBlob.size[1];
int h = netOutputBlob.size[2];
int w = netOutputBlob.size[3];
for(int i = 0; i< nParts;++i){
cv::Mat part(h, w, CV_32F, netOutputBlob.ptr(0,i));
cv::Mat resizedPart;
//将输出结果的图像大小还原到原来的大小
cv::resize(part,resizedPart,targetSize);
netOutputParts.push_back(resizedPart);
}
}
//a,b两点之间采样,a和b之间连接一条线段,在线上均匀的取numPoints个点
void populateInterpPoints(const cv::Point& a,const cv::Point& b,int numPoints,std::vector<cv::Point>& interpCoords){
float xStep = ((float)(b.x - a.x))/(float)(numPoints-1);
float yStep = ((float)(b.y - a.y))/(float)(numPoints-1);
interpCoords.push_back(a);
for(int i = 1; i< numPoints-1;++i){
interpCoords.push_back(cv::Point(a.x + xStep*i,a.y + yStep*i));
}
interpCoords.push_back(b);
}
//不同的关键点连接起来就是pair,获取有效的pair
void getValidPairs(const std::vector<cv::Mat>& netOutputParts,
const std::vector<std::vector<KeyPoint>>& detectedKeypoints,
std::vector<std::vector<ValidPair>>& validPairs,
std::set<int>& invalidPairs) {
int nInterpSamples = 10;//采样个数
float pafScoreTh = 0.1;
float confTh = 0.7;
//mapIdx 大小19,4层循环 能否减少 计算次数
//mapIdx与posePairs是一一对应的
for(size_t k = 0; k < mapIdx.size();++k ){
//A->B constitute a limb
//Part Affinity Fields
//从netOutputParts中按照mapIdx的键值对 取出,按key取出的值算作PAF_A,按value取出的值算作一个PAF_B
//这里first指的key,second指的value
cv::Mat pafA = netOutputParts[mapIdx[k].first];
cv::Mat pafB = netOutputParts[mapIdx[k].second];
//Find the keypoints for the first and second limb
//查找第一个 limb 和第二个 limb 的关键点位置
//从detectedKeypoints中按照 posePairs的键值对 取出,按key取出的值算作CAND_A,按value取出的值算作一个CAND_B
//CAND_A和CAND_B连起来就是 candidate limb
const std::vector<KeyPoint>& candA = detectedKeypoints[posePairs[k].first];
const std::vector<KeyPoint>& candB = detectedKeypoints[posePairs[k].second];
//candA和candB 因为检测结果,有相等的情况,也有不相等的情况
int nA = candA.size();
int nB = candB.size();
/*
# If keypoints for the joint-pair is detected
# check every joint in candA with every joint in candB
# Calculate the distance vector between the two joints
# Find the PAF values at a set of interpolated points between the joints
# Use the above formula to compute a score to mark the connection valid
*/
/*
如果检测到 joint-pair 的关键点位置,则,
检查 candA 和 candB 中每个 joint.
计算两个 joints 之间的距离向量(distance vector).
计算两个 joints 之间插值点集合的 PAF 值.
使用论文里的公式,计算 score 值,判断连接的有效性.
*/
if(nA != 0 && nB != 0){
std::vector<ValidPair> localValidPairs;
for(int i = 0; i< nA;++i){
int maxJ = -1;
float maxScore = -1;
bool found = false;
for(int j = 0; j < nB;++j){
std::pair<float,float> distance(candB[j].point.x - candA[i].point.x,candB[j].point.y - candA[i].point.y);
float norm = std::sqrt(distance.first*distance.first + distance.second*distance.second);
if(!norm){
continue;
}
distance.first /= norm;
distance.second /= norm;
//Find p(u)
std::vector<cv::Point> interpCoords; //A,B两点之间采样,A和B之间连接一条线段,在线上均匀的取nInterpSamples=10个点
populateInterpPoints(candA[i].point,candB[j].point,nInterpSamples,interpCoords);
//Find L(p(u)) pafA,pafB 存储了模型输出的值
std::vector<std::pair<float,float>> pafInterp;
for(size_t l = 0; l < interpCoords.size();++l){
pafInterp.push_back(
std::pair<float,float>(
pafA.at<float>(interpCoords[l].y,interpCoords[l].x),
pafB.at<float>(interpCoords[l].y,interpCoords[l].x)
));
}
//计算点积得到相似度
std::vector<float> pafScores;
float sumOfPafScores = 0;
int numOverTh = 0;
for(size_t l = 0; l< pafInterp.size();++l){
float score = pafInterp[l].first*distance.first + pafInterp[l].second*distance.second;
sumOfPafScores += score;
if(score > pafScoreTh){
++numOverTh;
}
pafScores.push_back(score);
}
//计算相似度的平均值
float avgPafScore = sumOfPafScores/((float)pafInterp.size());
//选最大的
if(((float)numOverTh)/((float)nInterpSamples) > confTh){
if(avgPafScore > maxScore){
maxJ = j;
maxScore = avgPafScore;
found = true;
}
}
}/* j */
if(found){
localValidPairs.push_back(ValidPair(candA[i].id,candB[maxJ].id,maxScore));
}
}/* i */
validPairs.push_back(localValidPairs);
} else {
invalidPairs.insert(k);
validPairs.push_back(std::vector<ValidPair>());
}
}/* k */
}
//获取属于每个人的关键点集合
//先把关键点两个两个的连接起来,成为一对对的,再把一对对的关键点组成了人体的姿态
void getPersonwiseKeypoints(const std::vector<std::vector<ValidPair>>& validPairs,
const std::set<int>& invalidPairs,
std::vector<std::vector<int>>& personwiseKeypoints) {
for(size_t k = 0; k < mapIdx.size();++k){
if(invalidPairs.find(k) != invalidPairs.end()){
continue;
}
const std::vector<ValidPair>& localValidPairs(validPairs[k]);
int indexA(posePairs[k].first);
int indexB(posePairs[k].second);
for(size_t i = 0; i< localValidPairs.size();++i){
bool found = false;
int personIdx = -1;
for(size_t j = 0; !found && j < personwiseKeypoints.size();++j){
if(indexA < static_cast<int>( personwiseKeypoints[j].size()) &&
personwiseKeypoints[j][indexA] == localValidPairs[i].aId){
personIdx = j;
found = true;
}
}/* j */
if(found){
personwiseKeypoints[personIdx].at(indexB) = localValidPairs[i].bId;
} else if(k < 17){
std::vector<int> lpkp(std::vector<int>(18,-1));
lpkp.at(indexA) = localValidPairs[i].aId;
lpkp.at(indexB) = localValidPairs[i].bId;
personwiseKeypoints.push_back(lpkp);
}
}/* i */
}/* k */
}
int main(int argc,char** argv) {
std::string inputFile = "2.jpg";
if(argc > 1){
inputFile = std::string(argv[1]);
}
cv::Mat input = cv::imread(inputFile, cv::IMREAD_COLOR);
//-----------------------------------------
std::chrono::time_point<std::chrono::system_clock> startTP = std::chrono::system_clock::now();
//加载模型
cv::dnn::Net inputNet = cv::dnn::readNetFromCaffe("openpose_pose_coco_multi_person.prototxt","pose_iter_440000.caffemodel");
//输入的height是固定的368,根据长宽比计算输入的width.
cv::Mat inputBlob = cv::dnn::blobFromImage(input,1.0/255.0,cv::Size((int)((368*input.cols)/input.rows),368),cv::Scalar(0,0,0),false,false);
inputNet.setInput(inputBlob);
cv::Mat netOutputBlob = inputNet.forward();
//netOutputBlob为4维矩阵:
// 第一个维度 忽略
// 第二个维度 对于COCO模型,它由57个部分组成,size[1]
// 18个关键点置信度图(confidence Map)+1个背景+19*2个Part Affinity Map。
// 18+1+38=57
// 第三个维度是输出图像的高度。row size[2]
// 第四个维度是输出图像的宽度。col size[3]
std::vector<cv::Mat> netOutputParts;//一维,大小是57
//参数是原始图片的大小,不是resize后的大小 ,要把57个部分 分开
splitNetOutputBlobToParts(netOutputBlob,cv::Size(input.cols,input.rows),netOutputParts);
std::chrono::time_point<std::chrono::system_clock> finishTP = std::chrono::system_clock::now();
std::cout << "Time Taken in forward pass = " << std::chrono::duration_cast<std::chrono::milliseconds>(finishTP - startTP).count() << " ms" << std::endl;
int keyPointId = 0;
//如果设计接口的输出 可以设计下面的数据结构 1(接口要输出part 和 pair)
//detectedKeypoints,keyPointsList存储相同的内容,一个按二维存,一个是按一维存
std::vector<std::vector<KeyPoint>> detectedKeypoints;//二维,存储了所有人的所有关键点
std::vector<KeyPoint> keyPointsList;//一维,为了在两个关键点之间画线,容易将keypoint取出来,所以又定义了一个keyPointsList
g_test_output_frame = input.clone();
for(int i = 0; i < kPoints;++i){
std::vector<KeyPoint> keyPoints;
// 0.1是阈值
//-----------------------
// cv::Mat output_part = netOutputParts[i].clone();//用于展示每个part 分析代码用
// cv::imshow("netOutputParts", output_part);
// cv::waitKey(0);
//------------------------------------------
getKeyPoints(netOutputParts[i],0.1,keyPoints);
//std::cout << "Keypoints - " << keypointsMapping[i] << " : " << keyPoints << std::endl;
//以输出左耳为例
// 只有一个人的时候
//Keypoints - L-Ear : [ Id:0, Point:[281, 89], Prob:0.784812]
// 多个人的时候
// Keypoints - L-Ear :
// [ Id:88, Point:[335, 175], Prob:0.62902
// , Id:89, Point:[189, 175], Prob:0.590987
// , Id:90, Point:[260, 175], Prob:0.714218
// , Id:91, Point:[76, 173], Prob:0.573334
// , Id:92, Point:[125, 167], Prob:0.353028
// , Id:93, Point:[455, 159], Prob:0.258485
// ]
// 这样依次输出18个关键点
for(size_t i = 0; i< keyPoints.size();++i,++keyPointId){
keyPoints[i].id = keyPointId;
}
std::cout << "Keypoints - " << keypointsMapping[i] << " : " << keyPoints << std::endl;
//id是这样,从0开始,依次向后累加,假设一共6个人,检测出了3个鼻子,就是0,1,2,那么脖子的id就是从3开始。
//而不是从6开始,不需要给6个人的鼻子都保留位置。
detectedKeypoints.push_back(keyPoints);
keyPointsList.insert(keyPointsList.end(),keyPoints.begin(),keyPoints.end());
}
//填充调色板
std::vector<cv::Scalar> colors;
populateColorPalette(colors,kPoints);
cv::Mat outputFrame = input.clone();
//将图片上所有关键点keypoint都画出来,
for(int i = 0; i < kPoints;++i){
for(size_t j = 0; j < detectedKeypoints[i].size();++j){
cv::circle(outputFrame,detectedKeypoints[i][j].point,10,colors[i],-1,cv::LINE_AA);
}
}
// 此时只是画出 keypoint,pair还没计算
std::vector<std::vector<ValidPair>> validPairs;
std::set<int> invalidPairs;
getValidPairs(netOutputParts,detectedKeypoints,validPairs,invalidPairs);
//一个point,有xy;两个point就可以连接成pair。
//Nose 1分别与Neck 2 ,LAnkle 5连接 {1,2}, {1,5}
//这里只能按照确定的一个方向去连接,例如nose 连接到 neck,而不是反方向的neck连接到nose
std::vector<std::vector<int>> personwiseKeypoints;//二维
//所有的pair,分配到不同的人
getPersonwiseKeypoints(validPairs,invalidPairs,personwiseKeypoints);
//如果设计接口的输出 可以设计下面的数据结构 2
//假设是一个人,18关键点没有遮挡全部检测出来 personwiseKeypoints结果就是(这里是关键点的id)
//[ [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]]
//假设是两个人,下面的 -1表示关键点没检测出来或者关键点没有连接到其他的关键点
//一行表示一个人的的关键点的id,该关键点是可以连接到其他关键点的
// [ [ 0, 2, 4, -1, -1, 8, -1, -1, -1, -1, -1, -1, -1, -1, 10, -1, 12, -1],
// [ 1, 3, 5, -1, -1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, 11, -1, 13]]
//人多一点就可以看的清楚一些,personwiseKeypoints的结果
// [
// [ -1, 3, 9, 17, 20, 26, 31, 36, 41, 48, 54, 59, 66, 73, -1, -1, -1, -1],
// [ -1, 4, 11, 16, 22, 27, 33, 38, 43, 50, 56, 61, 68, 74, -1, -1, -1, -1],
// [ -1, 5, 10, 15, 21, 28, 32, 37, 42, 49, 55, 62, 67, 72, -1, -1, -1, -1],
// [ 1, 6, 13, 19, 25, -1, -1, -1, 45, 51, 58, 63, 69, 76, 78, 80, -1, 91],
// [ 2, 7, 12, 17, -1, 29, 34, 39, 44, 47, 53, 60, 65, 71, 79, 81, 87, 93],
// [ -1, 8, 14, 18, 23, 30, 35, 40, 46, 52, 57, 64, 70, 75, -1, -1, -1, -1],
// [ 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 77, -1, 86, -1]]
//下面是将每两个点之间画上线,按照posePairs的定义去画,是确定的哪个关键点可以连接哪个关键点
for(int i = 0; i< kPoints-1;++i){
for(size_t n = 0; n < personwiseKeypoints.size();++n){
const std::pair<int,int>& posePair = posePairs[i];
int indexA = personwiseKeypoints[n][posePair.first];
int indexB = personwiseKeypoints[n][posePair.second];
//看personwiseKeypoints的值,就知道要判断-1的情况
if(indexA == -1 || indexB == -1){
continue;
}
const KeyPoint& kpA = keyPointsList[indexA];
const KeyPoint& kpB = keyPointsList[indexB];
cv::line(outputFrame,kpA.point,kpB.point,colors[i],3,cv::LINE_AA);
}
}
cv::imshow("Detected Pose",outputFrame);
cv::waitKey(0);
return 0;
}
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)