草庐IT

[OpenCV实战]12 使用深度学习和OpenCV进行手部关键点检测

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

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

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

1 背景

上图出自上面说的论文

他们从一小组标记的手部图像开始,并使用神经网络(卷积姿势分析机

)来粗略估计手部关键点。他们设置了一个多视图系统可以从31个高清摄像头获取来自不同视点或角度的图像。

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

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

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

2 实现

从此链接下载该模型:

这是一个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. 结果和代码

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

代码见:

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 参考

手部特征点识别

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

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

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  9. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  10. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

随机推荐