目录

1 背景

2 实现

3 结果和代码

4 参考


手部关键点检测是在手指上找到关节以及在给定图像中找到指尖的过程。它类似于在脸部(面部关键点检测)或身体(人体姿势估计)上找到关键点。但是手部检测不同的地方在于,我们将整个手部视为一个对象。

美国卡耐基梅隆大学智能感知实验室(CMU Perceptual Computing Lab)发布了手的关键点检测模型。详情见:

https://arxiv.org/pdf/1704.07809.pdf

我们将在本文介绍如何调用该模型。

1 背景

上图出自上面说的论文

他们从一小组标记的手部图像开始,并使用神经网络(卷积姿势分析机 https://arxiv.org/pdf/1602.00134.pdf 来粗略估计手部关键点。他们设置了一个多视图系统可以从31个高清摄像头获取来自不同视点或角度的图像。

他们将这些图像传递通过检测器,以获得许多粗略的关键点预测。一旦从不同视图获得同一手的检测到的关键点,就会执行关键点三角测量以获得关键点的3D位置。关键点的3D位置用于通过从3D到2D的重投影来稳健地预测关键点。这对于难以预测关键点的图像尤其重要。通过这种方式,他们可以在几次迭代中获得更好的检测器。

总之,他们使用关键点检测器和多视图图像来提出改进的检测器。改进的主要来源是标记的图像集的多视图图像。

该模型产生22个关键点。手有21个关键点(0到20号关键点),而第22个关键点代表背景。关键点位置如下图所示:

2 实现

从此链接下载该模型:

http://posefs1.perception.cs.cmu.edu/OpenPose/models/hand/pose_iter_102000.caffemodel

这是一个caffe模型。

模型读取预测代码和其他caffe模型一样,如下所示:

	//模型文件位置
string protoFile = "./model/pose_deploy.prototxt";
string weightsFile = "./model/pose_iter_102000.caffemodel"; // read image 读取图像
string imageFile = "./image/hand.jpg";
Mat frame = imread(imageFile);
if (frame.empty())
{
cout << "check image" << endl;
return 0;
}
//复制图像
Mat frameCopy = frame.clone();
//读取图像长宽
int frameWidth = frame.cols;
int frameHeight = frame.rows; float thresh = 0.01; //原图宽高比
float aspect_ratio = frameWidth / (float)frameHeight;
int inHeight = 368;
//缩放图像
int inWidth = (int(aspect_ratio*inHeight) * 8) / 8; cout << "inWidth = " << inWidth << " ; inHeight = " << inHeight << endl; double t = (double)cv::getTickCount();
//调用caffe模型
Net net = readNetFromCaffe(protoFile, weightsFile);
Mat inpBlob = blobFromImage(frame, 1.0 / 255, Size(inWidth, inHeight), Scalar(0, 0, 0), false, false);
net.setInput(inpBlob);
Mat output = net.forward(); int H = output.size[2];
int W = output.size[3];

输出有22个矩阵,每个矩阵是关键点的概率图。为了找到确切的关键点,首先,我们将概率图缩放到原始图像的大小。然后通过查找概率图的最大值来找到关键点的位置。这是使用OpenCV中的minmaxLoc函数完成的。我们绘制检测到的点以及图像上的编号。我们将使用检测到的点来获取关键点形成的骨架并将其绘制在图像上。画骨架代码如下:

	// find the position of the body parts 找到各点的位置
vector<Point> points(nPoints);
for (int n = 0; n < nPoints; n++)
{
// Probability map of corresponding body's part. 第一个特征点的预测矩阵
Mat probMap(H, W, CV_32F, output.ptr(0, n));
//放大预测矩阵
resize(probMap, probMap, Size(frameWidth, frameHeight)); Point maxLoc;
double prob;
//寻找预测矩阵,最大值概率以及最大值的坐标位置
minMaxLoc(probMap, 0, &prob, 0, &maxLoc);
if (prob > thresh)
{
//画图
circle(frameCopy, cv::Point((int)maxLoc.x, (int)maxLoc.y), 8, Scalar(0, 255, 255), -1);
cv::putText(frameCopy, cv::format("%d", n), cv::Point((int)maxLoc.x, (int)maxLoc.y), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(0, 0, 255), 2);
}
//保存特征点的坐标
points[n] = maxLoc;
} //获取要画的骨架线个数
int nPairs = sizeof(POSE_PAIRS) / sizeof(POSE_PAIRS[0]); //连接点,画骨架
for (int n = 0; n < nPairs; n++)
{
// lookup 2 connected body/hand parts
Point2f partA = points[POSE_PAIRS[n][0]];
Point2f partB = points[POSE_PAIRS[n][1]]; if (partA.x <= 0 || partA.y <= 0 || partB.x <= 0 || partB.y <= 0)
continue; //画骨条线
line(frame, partA, partB, Scalar(0, 255, 255), 8);
circle(frame, partA, 8, Scalar(0, 0, 255), -1);
circle(frame, partB, 8, Scalar(0, 0, 255), -1);
}

结果如下:

3 结果和代码

需要注意的一点是,检测器需要手周围的边界框来预测关键点。因此,为了获得更好的效果,手应靠近相机,反正总而言之手的位置要清楚,在屏幕中央。现在的深度学习只能这样。精度不怎么高,只能在特定场合下使用,就是先确定关键点,然后训练模型,基于统计进行检测。

代码见:

https://github.com/luohenyueji/OpenCV-Practical-Exercise

C++代码:

// HandPoints_detection.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
// #include "pch.h"
#include <iostream>
#include <opencv2/opencv.hpp> using namespace std;
using namespace cv;
using namespace cv::dnn; //各个部位连接线坐标,比如(0,1)表示第0特征点和第1特征点连接线为拇指
const int POSE_PAIRS[20][2] =
{
{0,1}, {1,2}, {2,3}, {3,4}, // thumb
{0,5}, {5,6}, {6,7}, {7,8}, // index
{0,9}, {9,10}, {10,11}, {11,12}, // middle
{0,13}, {13,14}, {14,15}, {15,16}, // ring
{0,17}, {17,18}, {18,19}, {19,20} // small
}; int nPoints = 22; int main()
{
//模型文件位置
string protoFile = "./model/pose_deploy.prototxt";
string weightsFile = "./model/pose_iter_102000.caffemodel"; // read image 读取图像
string imageFile = "./image/hand.jpg";
Mat frame = imread(imageFile);
if (frame.empty())
{
cout << "check image" << endl;
return 0;
}
//复制图像
Mat frameCopy = frame.clone();
//读取图像长宽
int frameWidth = frame.cols;
int frameHeight = frame.rows; float thresh = 0.01; //原图宽高比
float aspect_ratio = frameWidth / (float)frameHeight;
int inHeight = 368;
//缩放图像
int inWidth = (int(aspect_ratio*inHeight) * 8) / 8; cout << "inWidth = " << inWidth << " ; inHeight = " << inHeight << endl; double t = (double)cv::getTickCount();
//调用caffe模型
Net net = readNetFromCaffe(protoFile, weightsFile);
Mat inpBlob = blobFromImage(frame, 1.0 / 255, Size(inWidth, inHeight), Scalar(0, 0, 0), false, false);
net.setInput(inpBlob);
Mat output = net.forward(); int H = output.size[2];
int W = output.size[3]; // find the position of the body parts 找到各点的位置
vector<Point> points(nPoints);
for (int n = 0; n < nPoints; n++)
{
// Probability map of corresponding body's part. 第一个特征点的预测矩阵
Mat probMap(H, W, CV_32F, output.ptr(0, n));
//放大预测矩阵
resize(probMap, probMap, Size(frameWidth, frameHeight)); Point maxLoc;
double prob;
//寻找预测矩阵,最大值概率以及最大值的坐标位置
minMaxLoc(probMap, 0, &prob, 0, &maxLoc);
if (prob > thresh)
{
//画图
circle(frameCopy, cv::Point((int)maxLoc.x, (int)maxLoc.y), 8, Scalar(0, 255, 255), -1);
cv::putText(frameCopy, cv::format("%d", n), cv::Point((int)maxLoc.x, (int)maxLoc.y), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(0, 0, 255), 2);
}
//保存特征点的坐标
points[n] = maxLoc;
} //获取要画的骨架线个数
int nPairs = sizeof(POSE_PAIRS) / sizeof(POSE_PAIRS[0]); //连接点,画骨架
for (int n = 0; n < nPairs; n++)
{
// lookup 2 connected body/hand parts
Point2f partA = points[POSE_PAIRS[n][0]];
Point2f partB = points[POSE_PAIRS[n][1]]; if (partA.x <= 0 || partA.y <= 0 || partB.x <= 0 || partB.y <= 0)
continue; //画骨条线
line(frame, partA, partB, Scalar(0, 255, 255), 8);
circle(frame, partA, 8, Scalar(0, 0, 255), -1);
circle(frame, partB, 8, Scalar(0, 0, 255), -1);
} //计算运行时间
t = ((double)cv::getTickCount() - t) / cv::getTickFrequency();
cout << "Time Taken = " << t << endl;
imshow("Output-Keypoints", frameCopy);
imshow("Output-Skeleton", frame);
imwrite("Output-Skeleton.jpg", frame); waitKey(); return 0;
}

python代码:

from __future__ import division
import cv2
import time
import numpy as np protoFile = "./model/pose_deploy.prototxt"
weightsFile = "./model/pose_iter_102000.caffemodel"
nPoints = 22
POSE_PAIRS = [ [0,1],[1,2],[2,3],[3,4],[0,5],[5,6],[6,7],[7,8],[0,9],[9,10],[10,11],[11,12],[0,13],[13,14],[14,15],[15,16],[0,17],[17,18],[18,19],[19,20] ]
net = cv2.dnn.readNetFromCaffe(protoFile, weightsFile) frame = cv2.imread("./image/hand.jpg")
frameCopy = np.copy(frame)
frameWidth = frame.shape[1]
frameHeight = frame.shape[0]
aspect_ratio = frameWidth/frameHeight threshold = 0.1 t = time.time()
# input image dimensions for the network
inHeight = 368
inWidth = int(((aspect_ratio*inHeight)*8)//8)
inpBlob = cv2.dnn.blobFromImage(frame, 1.0 / 255, (inWidth, inHeight), (0, 0, 0), swapRB=False, crop=False) net.setInput(inpBlob) output = net.forward()
print("time taken by network : {:.3f}".format(time.time() - t)) # Empty list to store the detected keypoints
points = [] for i in range(nPoints):
# confidence map of corresponding body's part.
probMap = output[0, i, :, :]
probMap = cv2.resize(probMap, (frameWidth, frameHeight)) # Find global maxima of the probMap.
minVal, prob, minLoc, point = cv2.minMaxLoc(probMap) if prob > threshold :
cv2.circle(frameCopy, (int(point[0]), int(point[1])), 8, (0, 255, 255), thickness=-1, lineType=cv2.FILLED)
cv2.putText(frameCopy, "{}".format(i), (int(point[0]), int(point[1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, lineType=cv2.LINE_AA) # Add the point to the list if the probability is greater than the threshold
points.append((int(point[0]), int(point[1])))
else :
points.append(None) # Draw Skeleton
for pair in POSE_PAIRS:
partA = pair[0]
partB = pair[1] if points[partA] and points[partB]:
cv2.line(frame, points[partA], points[partB], (0, 255, 255), 2)
cv2.circle(frame, points[partA], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED)
cv2.circle(frame, points[partB], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED) cv2.imshow('Output-Keypoints', frameCopy)
cv2.imshow('Output-Skeleton', frame) cv2.imwrite('Output-Keypoints.jpg', frameCopy)
cv2.imwrite('Output-Skeleton.jpg', frame) print("Total time taken : {:.3f}".format(time.time() - t)) cv2.waitKey(0)

4 参考

手部特征点识别

https://www.learnopencv.com/hand-keypoint-detection-using-deep-learning-and-opencv/

其他身体特征点识别,一样的套路

https://www.learnopencv.com/deep-learning-based-human-pose-estimation-using-opencv-cpp-python/

[OpenCV实战]12 使用深度学习和OpenCV进行手部关键点检测的更多相关文章

  1. [OpenCV实战]15 基于深度学习的目标跟踪算法GOTURN

    目录 1 什么是对象跟踪和GOTURN 2 在OpenCV中使用GOTURN 3 GOTURN优缺点 4 参考 在这篇文章中,我们将学习一种基于深度学习的目标跟踪算法GOTURN.GOTURN在Caf ...

  2. [OpenCV实战]5 基于深度学习的文本检测

    目录 1 网络加载 2 读取图像 3 前向传播 4 处理输出 3结果和代码 3.1结果 3.2 代码 参考 在这篇文章中,我们将逐字逐句地尝试找到图片中的单词!基于最近的一篇论文进行文字检测. EAS ...

  3. [OpenCV实战]1 基于深度学习识别人脸性别和年龄

    目录 1基于CNN的性别分类建模原理 1.1 人脸识别 1.2 性别预测 1.3 年龄预测 1.4 结果 2 代码 参考 本教程中,我们将讨论应用于面部的深层学习的有趣应用.我们将估计年龄,并从单个图 ...

  4. 深度学习与CV教程(13) | 目标检测 (SSD,YOLO系列)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  5. [PyImageSearch] Ubuntu16.04 使用深度学习和OpenCV实现物体检测

    上一篇博文中讲到如何用OpenCV实现物体分类,但是接下来这篇博文将会告诉你图片中物体的位置具体在哪里. 我们将会知道如何使用OpenCV‘s的dnn模块去加载一个预训练的物体检测网络,它能使得我们将 ...

  6. [OpenCV实战]17 基于卷积神经网络的OpenCV图像着色

    目录 1 彩色图像着色 1.1 定义着色问题 1.2 CNN彩色化结构 1.3 从 中恢复彩色图像 1.4 具有颜色再平衡的多项式损失函数 1.5 着色结果 2 OpenCV中实现着色 2.1 模型下 ...

  7. 斯坦福新深度学习系统 NoScope:视频对象检测快1000倍

    以作备份,来源http://jiasuhui.com/archives/178954 本文由“新智元”(微信ID:AI_era)编译,来源:dawn.cs.stanford.edu,编译:刘小芹 斯坦 ...

  8. 深度学习 + OpenCV,Python实现实时视频目标检测

    使用 OpenCV 和 Python 对实时视频流进行深度学习目标检测是非常简单的,我们只需要组合一些合适的代码,接入实时视频,随后加入原有的目标检测功能. 在本文中我们将学习如何扩展原有的目标检测项 ...

  9. 语义分割:基于openCV和深度学习(一)

    语义分割:基于openCV和深度学习(一) Semantic segmentation with OpenCV and deep learning 介绍如何使用OpenCV.深度学习和ENet架构执行 ...

随机推荐

  1. Java中的多线程的创建方式

    首先理清几个基本概念: 程序:为完成特定任务,用某种语言编写的一组指令的集合.即一段静态的代码(还没运行起来) 进程:是程序的一次执行过程,也就是说程序运行起来了,加载到了内存中,并占用了cpu的资源 ...

  2. 🔥支持 Java 19 的轻量级应用开发框架,Solon v1.10.4 发布

    Java 轻量级应用开发框架.可用来快速开发 Java 应用项目,主框架仅 0.1 MB. 相对于 Spring Boot 和 Spring Cloud 的项目: 启动快 5 - 10 倍. (更快) ...

  3. lombok下载和安装

    lombok是什么 第三方的组件:使用注解来简化类的编写,注解替换set/get/构造 注解: @setter @getter @NoArgsConstructor @AllArgsConstruct ...

  4. java实现单链表的创建、增、删、改、查

    文章目录 单链表的创建.增.删.改.查 1.增加一个节点 2.删除一个节点 3.修改某一个节点 5.遍历单链表 单链表的创建.增.删.改.查 双向链表的增删改查:https://blog.csdn.n ...

  5. python更改文件后缀名

    path = '1024.png' extension = 'jpg' for i in range(1,len(path)): if (path[-i] == '.'):#找到后缀初始点 new_p ...

  6. vue-axios 输入参数获取数据的写法

    <template> <div class="nav"> <input v-model="name" type="tex ...

  7. spalsh安装及简单使用

    selenium是浏览器测试自动化工具,很容易完成鼠标点击,翻页等动作,确定是一次只能加载一个页面,无法异步渲染页面,也就限制了selenium爬虫的抓取效率. splash可以实现异步渲染页面,可以 ...

  8. "xxx cannot be cast to jakarta.servlet.Servlet "报错解决方式

    在做jsp的上机时候同学出现了一个500错误:com.kailong.servlet.ComputeBill cannot be cast to jaka.servlet.Servlet 然后因为我用 ...

  9. C. 连锁商店(状压dp)

    C. 连锁商店 time limit per test 1 second memory limit per test 512 megabytes input standard input output ...

  10. Linux网络通信(线程池和线程池版本的服务器代码)

    线程池 介绍 线程池: 一种线程使用模式.线程过多会带来调度开销,进而影响缓存局部性和整体性能.而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务.这避免了在处理短时间任务时创建与销毁线程的 ...