windows 基于 MediaPipe 实现 Holistic

Posted J ..

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了windows 基于 MediaPipe 实现 Holistic相关的知识,希望对你有一定的参考价值。


主页: https://google.github.io/mediapipe/solutions/holistic.html

MediaPipe Holistic pipelines 集成了姿势、面部和手部组件的独立模型,每个组件都针对其特定领域进行了优化,每个组件的推断输入图不同。

MediaPipe Holistic 首先通过 BlazePose 的姿势检测器和后续的关键点模型来估计人的姿势。然后,利用推断出的姿势关键点,为每只手和脸部推导出三个感兴趣区域(ROI)裁剪,并采用 re-crop 模型来改进 ROI

然后,pipelines 将全分辨率输入帧上裁剪这些 ROI,并应用特定任务的模型来估计它们对应的关键点。最后,将所有关键点与姿势模型的关键点合并,得出全部 540 多个关键点。


实现过程 🚀​

编译demo

  1. bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 --action_env PYTHON_BIN_PATH="C:xxx//python.exe" mediapipe/examples/desktop/holistic_tracking:holistic_tracking_cpu --verbose_failures
    
    bazel-bin\\mediapipe\\examples\\desktop\\holistic_tracking\\holistic_tracking_cpu --calculator_graph_config_file=mediapipe/graphs/holistic_tracking/holistic_tracking_cpu.pbtxt
    
  2. 如果运行成功,则默认会调用摄像头

编写测试demo

  1. 环境配置参考:

  2. 修改 mediapipe\\examples\\desktop\\holistic_trackingBUILD

  3. 增加个测试程序节点

    cc_binary(
        name = "holistic_tracking_sample",
        srcs = ["main.cpp","holistic_detect.h","holistic_detect.cpp"],
        # linkshared=True,
        deps = [
            "//mediapipe/graphs/holistic_tracking:holistic_tracking_cpu_graph_deps",
        ],
    )
    
  4. holistic_detect.h

    #ifndef _HOLISTIC_DETECT_H_
    #define _HOLISTIC_DETECT_H_
    
    #include <cstdlib>
    #include "absl/flags/flag.h"
    #include "absl/flags/parse.h"
    #include "mediapipe/framework/calculator_framework.h"
    #include "mediapipe/framework/formats/image_frame.h"
    #include "mediapipe/framework/formats/image_frame_opencv.h"
    #include "mediapipe/framework/port/file_helpers.h"
    #include "mediapipe/framework/port/opencv_highgui_inc.h"
    #include "mediapipe/framework/port/opencv_imgproc_inc.h"
    #include "mediapipe/framework/port/opencv_video_inc.h"
    #include "mediapipe/framework/port/parse_text_proto.h"
    #include "mediapipe/framework/port/status.h"
    #include "mediapipe/framework/formats/detection.pb.h"
    #include "mediapipe/framework/formats/landmark.pb.h"
    #include "mediapipe/framework/formats/rect.pb.h"
    #include "mediapipe/framework/formats/classification.pb.h"
    #include "mediapipe/framework/port/ret_check.h"
    
    namespace mp 
    
    	using holistic_callback_t = std::function<void()>;
    
    	class HolisticDetect
    	
    	public:
    		int initModel(const char* model_path) noexcept;
    		int detectVideoCamera(holistic_callback_t call);
    		int detectVideo(const char* video_path, int show_image, holistic_callback_t call);
    		int detectVideo();
    		int release();
    	private:
    		absl::Status initGraph(const char* model_path);
    		absl::Status runMPPGraphVideo(const char* video_path, int show_image, holistic_callback_t call);
    		absl::Status releaseGraph();
    
    		void showFaceLandmarks(cv::Mat& image);
    		void showPoseLandmarks(cv::Mat& image);
    		void showLHandLandmarks(cv::Mat& image);
    		void showRHandLandmarks(cv::Mat& image);
    
    		const char* kInputStream = "input_video";
    		const char* kOutputStream = "output_video";
    		const char* kWindowName = "MediaPipe";
    		const char* kOutputLandmarks = "face_landmarks";
    		const char* kOutputPoseLandmarks = "pose_landmarks";
    		const char* kOutputLHandLandmarks = "left_hand_landmarks";
    		const char* kOutputRHandLandmarks = "right_hand_landmarks";
    
    		bool init_ = false;
    		mediapipe::CalculatorGraph graph_;
    
    		const float kMinFloat = std::numeric_limits<float>::lowest();
    		const float kMaxFloat = std::numeric_limits<float>::max();
    
    		std::unique_ptr<mediapipe::OutputStreamPoller> pPoller_;
    		std::unique_ptr<mediapipe::OutputStreamPoller> pPollerFaceLandmarks_;
    		std::unique_ptr<mediapipe::OutputStreamPoller> pPollerPoseLandmarks_;
    		std::unique_ptr<mediapipe::OutputStreamPoller> pPollerLHandLandmarks_;
    		std::unique_ptr<mediapipe::OutputStreamPoller> pPollerRHandLandmarks_;
    	;
    
    
    #endif
    
    
  5. holistic_detect.cpp

    #include "holistic_detect.h"
    using namespace mp;
    
    int HolisticDetect::initModel(const char* model_path) noexcept 
    	absl::Status run_status = initGraph(model_path);
    	if (!run_status.ok())
    		return -1;
    	init_ = true;
    	return  1;
    
    
    int HolisticDetect::detectVideoCamera(holistic_callback_t call) 
    	if (!init_)
    		return -1;
    
    	absl::Status run_status = runMPPGraphVideo("", true, call);
    	return run_status.ok() ? 1 : -1;
    
    
    int HolisticDetect::detectVideo(const char* video_path, int show_image, holistic_callback_t call) 
    	if (!init_)
    		return -1;
    
    	absl::Status run_status = runMPPGraphVideo(video_path, show_image, call);
    	return run_status.ok() ? 1 : -1;
    
    
    int HolisticDetect::detectVideo() 
    	if (!init_)
    		return -1;
    	absl::Status run_status = runMPPGraphVideo("", 1, nullptr);
    	return run_status.ok() ? 1 : -1;
    
    
    int HolisticDetect::release() 
    	absl::Status run_status = releaseGraph();
    	return run_status.ok() ? 1 : -1;
    
    
    absl::Status HolisticDetect::initGraph(const char* model_path) 
    
    	std::string calculator_graph_config_contents;
    	MP_RETURN_IF_ERROR(mediapipe::file::GetContents(model_path, &calculator_graph_config_contents));
    	mediapipe::CalculatorGraphConfig config =
    		mediapipe::ParseTextProtoOrDie<mediapipe::CalculatorGraphConfig>(
    			calculator_graph_config_contents);
    	MP_RETURN_IF_ERROR(graph_.Initialize(config));
    	auto sop = graph_.AddOutputStreamPoller(kOutputStream);
    	assert(sop.ok());
    	pPoller_ = std::make_unique<mediapipe::OutputStreamPoller>(std::move(sop.value()));
    
    	// 脸部特征
    	mediapipe::StatusOrPoller faceLandmark = graph_.AddOutputStreamPoller(kOutputLandmarks);
    	assert(faceLandmark.ok());
    	pPollerFaceLandmarks_ = std::make_unique<mediapipe::OutputStreamPoller>(std::move(faceLandmark.value()));
    
    	// 姿态特征
    	mediapipe::StatusOrPoller poseLandmark = graph_.AddOutputStreamPoller(kOutputPoseLandmarks);
    	assert(poseLandmark.ok());
    	pPollerPoseLandmarks_ = std::make_unique<mediapipe::OutputStreamPoller>(std::move(poseLandmark.value()));
    
    	// 左手特征点
    	mediapipe::StatusOrPoller leftHandLandmark = graph_.AddOutputStreamPoller(kOutputLHandLandmarks);
    	assert(leftHandLandmark.ok());
    	pPollerLHandLandmarks_ = std::make_unique<mediapipe::OutputStreamPoller>(std::move(leftHandLandmark.value()));
    
    	// 右手特征点
    	mediapipe::StatusOrPoller rightHandLandmark = graph_.AddOutputStreamPoller(kOutputRHandLandmarks);
    	assert(rightHandLandmark.ok());
    	pPollerRHandLandmarks_ = std::make_unique<mediapipe::OutputStreamPoller>(std::move(rightHandLandmark.value()));
    
    	MP_RETURN_IF_ERROR(graph_.StartRun());
    	std::cout << "======= graph_ StartRun success ============" << std::endl;
    	return absl::OkStatus();
    
    
    void HolisticDetect::showFaceLandmarks(cv::Mat& image) 
    	mediapipe::Packet packet_landmarks;
    	if (pPollerFaceLandmarks_->QueueSize() == 0 || !pPollerFaceLandmarks_->Next(&packet_landmarks))
    		return;
    
    	//  468 face landmark
    	auto& output_landmarks = packet_landmarks.Get<mediapipe::NormalizedLandmarkList>();
    	for (int i = 0; i < output_landmarks.landmark_size(); ++i)
    	
    		const mediapipe::NormalizedLandmark landmark = output_landmarks.landmark(i);
    		float x = landmark.x() * image.cols;
    		float y = landmark.y() * image.rows;
    		//cv::circle(image, cv::Point(x, y), 2, cv::Scalar(0, 255, 0));
    		// todo
    		// ...
    	
    
    
    void HolisticDetect::showPoseLandmarks(cv::Mat& image) 
    	mediapipe::Packet packet_landmarks;
    	if (pPollerPoseLandmarks_->QueueSize() == 0 || !pPollerPoseLandmarks_->Next(&packet_landmarks))
    		return;
    
    	//  33 pose landmark
    	auto& output_landmarks = packet_landmarks.Get<mediapipe::NormalizedLandmarkList>();
    	for (int i = 0; i < output_landmarks.landmark_size(); ++i)
    	
    		const mediapipe::NormalizedLandmark landmark = output_landmarks.landmark(i);
    		float x = landmark.x() * image.cols;
    		float y = landmark.y() * image.rows;
    		//cv::circle(image, cv::Point(x, y), 2, cv::Scalar(0, 255, 0));
    		// todo
    		// ...
    	
    
    
    void HolisticDetect::showLHandLandmarks(cv::Mat& image) 
    	mediapipe::Packet packet_landmarks;
    	if (pPollerLHandLandmarks_->QueueSize() == 0 || !pPollerLHandLandmarks_->Next(&packet_landmarks))
    		return;
    	//  21 left hand landmark
    	auto& output_landmarks = packet_landmarks.Get<mediapipe::NormalizedLandmarkList>();
    	for (int i = 0; i < output_landmarks.landmark_size(); ++i)
    	
    		const mediapipe::NormalizedLandmark landmark = output_landmarks.landmark(i);
    		float x = landmark.x() * image.cols;
    		float y = landmark.y() * image.rows;
    		//cv::circle(image, cv::Point(x, y), 2, cv::Scalar(0, 255, 0));
    		// todo
    		// ...
    	
    
    
    void HolisticDetect::showRHandLandmarks(cv::Mat& image) 
    	mediapipe::Packet packet_landmarks;
    	if (pPollerRHandLandmarks_->QueueSize() == 0 || !pPollerRHandLandmarks_->Next(&packet_landmarks))
    		return;
    	//  21 right hand landmark
    	auto& output_landmarks = packet_landmarks.Get<mediapipe::NormalizedLandmarkList>();
    	for (int i = 0; i < output_landmarks.landmark_size(); ++i)
    	
    		const mediapipe::NormalizedLandmark landmark = output_landmarks.landmark(i);
    		float x = landmark.x() * image.cols;
    		float y = landmark.y() * image.rows;
    		//cv::circle(image, cv::Point(x, y), 2, cv::Scalar(0, 255, 0));
    		// todo
    		// ...
    	
    
    
    absl::Status HolisticDetect::runMPPGraphVideo(const char* video_path, int show_image, holistic_callback_t call) 
    	cv::VideoCapture capture(video_path);
    	RET_CHECK(capture.isOpened());
    #if (CV_MAJOR_VERSION >= 3) && (CV_MINOR_VERSION >= 2)
    	capture.set(cv::CAP_PROP_FRAME_WIDTH, 640);
    	capture.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
    	capture.set(cv::CAP_PROP_FPS, 30);
    #endif
    	int tmp = 0;
    	bool grab_frames = true;
    	while (grab_frames) 
    		// Capture opencv camera or video frame.
    		cv::Mat camera_frame_raw;
    		capture >> camera_frame_raw;
    		if (camera_frame_raw.empty())
    			break;
    
    		cv::Mat camera_frame;
    		cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB);
    		cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1);
    
    		// Wrap Mat into an ImageFrame.
    		auto input_frame = absl::make_unique<mediapipe::ImageFrame>(
    			mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows,
    			mediapipe::ImageFrame::kDefaultAlignmentBoundary);
    		cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get());
    		camera_frame.copyTo(input_frame_mat);
    
    		// Send image packet into the graph.
    		size_t frame_timestamp_us =
    			(double)cv::getTickCount() / (double)cv::getTickFrequency() * 1e6;
    
    		MP_RETURN_IF_ERROR(graph_.AddPacketToInputStream(
    			kInputStream, mediapipe::Adopt(input_frame.release())
    			.At(mediapipe::Timestamp(frame_timestamp_us))));
    
    		// Get the graph result packet, or stop if that fails.
    		mediapipe::Packet packet;
    		if (!pPoller_->Next(&packet)) break;
    
    		showFaceLandmarks(camera_frame);
    		showPoseLandmarks(camera_frame);
    		showLHandLandmarks(camera_frame);
    		showRHandLandmarks(camera_frame);
    
    		if (show_image) 
    			auto& output_frame = packet.Get<mediapipe::ImageFrame>();
    			// Convert back to opencv for display or saving.
    			cv::Mat output_frame_mat &

    SignAll SDK:基于 MediaPipe 的手语接口现已开放


    客座博文 / 由 SignAll 与 MediaPipe 团队联合完成。请注意,以下内容中的信息、用途及应用场景完全来自 SignAll 客座作者的观点。

    SignAll SDK

    当 Google 发布第一个基于 MediaPipe 的设备端手部追踪技术时,它便成为了开发者构建手语识别解决方案应用的基础。Google 之后对这个手部跟踪解决方案的进一步更新,将其准确率提升至其他技术所无法达到的水平(图 1)。

    图 1 随着时间推移,MediaPipe 的改进:较早版本(2020 年 2 月 10 日)和最新版本(2020 年 12 月 16 日)的手部骨架追踪输出。这种手形经常在手语中使用,但由于缺少表示法的训练数据集而经常被遗漏

    SignAll 是一家研发手语翻译技术的初创公司。该公司的使命是为失聪人士普及手语翻译,让他们能够与听力正常的人群以及计算机进行交流。SignAll 的产品采用了复杂的多摄像头设置和带有彩色标记的手套,广泛用于美国的通信和教育领域。虽然手语的复杂性不仅限于手形(还包括面部特征、肢体、语法等),但准确追踪手部确实已经给预处理程序(即计算机视觉)造成了巨大阻碍。MediaPipe 为 SignAll 的解决方案提供了更多可能性,不仅能够免除手套,还可以使用单摄像头设置。SignAll 已经宣布针对此类型开放首版 SDK,所以开发者现在能够在自己的应用中启用手语输入。

    • SignAll
      https://www.signall.us/

    • 首版 SDK
      https://signall.us/sdk

    近期,该公司在 App Store 上发布了一个互动式教育应用,该应用可以让用户通过即时反馈来练习手语,还能够展现出 SDK 的潜力。

    SignAll 与 MediaPipe Hands

    我们的系统在手语识别方面采用多个数据层,各层数据的抽象性越来越高。低级数据层从 2D 和 3D 摄像头中提取关键的手部、躯体和面部数据。在我们的第一个实现中,此数据层会检测手套的颜色,并创建 3D 手部数据。将其替换为 MediaPipe HandsMediaPipe Pose 和 MediaPipe Face Mesh 作为补充)具有颠覆性的重要意义,因为你不再需要手套或特殊光线来使用我们的系统。

    图 2 使用 MediaPipe 开发的 SignAll SDK 的演示

    如上文所述,我们使用了多个带有深度传感器的摄像头,并在实际中对这些传感器进行了校准。相较于本地摄像头或张量空间,这种方法能够实现更加准确的 3D 世界空间探测,但每个摄像头都需要进行手部特征点检测。摄像头的位置和屏幕方向各不相同,因此可以实现更高的手部可视频率,因为从一个摄像头的角度来看,手部可能会被另外一只手遮挡,但从另一个摄像头的角度来看,可能并不存在遮挡。

    图 3 在三个摄像头的基础上纠正 3D 手形。由于其镜头方向的特殊性,正前方的摄像头检测有误,但侧面的摄像头能够纠正结果

    接下来的步骤是过滤数据,并进行数据平滑处理,以复制彩色手套标记提供的精确测量值。虽然 SignAll 的标记与 MediaPipe 提供的界标不同,但我们使用了手部模型并根据界标生成彩色标记。因此,新的动作捕捉数据与之前的数据完全兼容。

    虽然我们主要关注手部,但我们同时整合了 MediaPipe PoseMediaPipe Face Mesh。即便在彼此接触,或距离很近的情况下,姿态界标都能提供准确的手部姿态信息。

    虽然这两个版本的动作捕捉是兼容的,但工件的性质不同:一种是直接测量各个标记,另一种是根据全局检测的手部模拟标记。因为存在差异,所以我们必须在更高层级对参数进行优化。另一方面,我们仍可以利用我们的大型手语数据库来进行无手套配置。我们可以通过替换低级数据,优化高级数据,以无手套形式测试我们的系统。实现无手套化,对于手语识别技术的全球推广具有重要意义。

    图 4 不同低级追踪的兼容动作捕捉的演示:右图是无手套的情况,左图是有手套的情况。因为具有这种兼容性,所以我们能够利用 SignAll 精心标记的 300000 多个手语视频数据集,在不同低级数据的基础上训练识别模型

    使用 MediaPipe 框架的 SignAll 系统

    MediaPipe Hands 整合至系统后,我们还希望能够利用 MediaPipe 框架在多个平台上提供的自定义和扩容机会。这样我们不仅可以用 Python 原型化我们的状态研究方法,而且还可以为 Windows、iOS、Android 甚至 Web 提供最终用户解决方案。由于我们的模块图系统和 MediaPipe 的计算图之间具有相似性,现有的处理单元只需稍作修改就可以在这个新框架中重用。尽管如此,扩展平台组还面临着其他挑战,例如在大多数情况下我们只能使用单个 2D 摄像头而不是经过校准的多摄像头系统。

    我们开发并使用的模型、算法和技术,主要是为了在 3D 全局世界中处理动作捕捉数据。毫无疑问,从单摄像头设置中提取的数据达不到同样的详细程度。所以我们必须对实现进行一些调整,微调算法并添加一些额外逻辑(例如,动态适应手持摄像头用例导致的空间变化)。幸运的是,MediaPipe 框架让我们能够用 C ++ 实现核心处理单元,因此我们仍然可以从先前开发的运行时优化核心解决方案中受益。

    为了更好地处理来自单个 2D 源的数据,一些基于 3D 数据训练的高级模型需要重新训练。MediaPipe 界标由 3D 坐标定义,因此可以重复使用现有的训练方法和概念。另一方面,2D 信息的提取比三维坐标更为直接也更为稳定,在修改设计训练时需要考虑到这一点。

    幸运的是,我们无需为实现此目标而进行全新的数据记录。我们仍然可以使用注释详细的大型视频数据库。预处理的动作捕捉数据可以从我们的记录中提取,并在 3D 世界中解释,从而用来模拟任何虚拟摄像头视图中的手部、骨架或面部界标检测。

    在虚拟摄像头视图的数据中,我们同时使用传统的 2D 记录,以足够的比例覆盖界标检测的独特噪点特征。由于大多数此类数据已经提前收集了,所以我们可以专注于尝试最新技术并训练新模型。

    总结

    在 MediaPipe 助力的改进,让 SignAll 可以更改其模型。除了提供用于手语教学和翻译的多合一产品之外,SignAll 现在也开始提供面向开发者的 SDK。此 SDK 的功能取决于摄像头的类型和可用的算力。SDK 可以启用的功能包括:

    • 通过用手语表示联系人的姓名来发起视频通话

    • 通过手语(与语音输入相对应)在导航中添加地址,或在快餐店的信息亭或直通车道中进行点餐。

    • 面向开发者的 SDK
      https://signall.us/sdk

    SignAll 的使命是让手语能够全方位替代语音,而我们非常高兴看到越来越多的应用实现了此功能。

    我们十分期待 MediaPipe 未来的更新,这些更新能够帮助我们进一步实现终极目标:让所有人在任何设备上都能使用我们的解决方案。最值得期待的更新是能够建立自定义的 MediaPipe 图,并添加我们自己的计算器,从而在 WebAssembly 技术的辅助下实现基于网络的解决方案,这样网站就能够为失聪访问者提供全新水平的无障碍功能。

    更多 AI 相关阅读:

    以上是关于windows 基于 MediaPipe 实现 Holistic的主要内容,如果未能解决你的问题,请参考以下文章

    基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统

    机器学习实验五基于多分类线性SVM实现简易人机猜拳游戏

    基于Mediapipe与Unity的人体姿态捕捉系统

    基于mediapipe的姿态识别和简单行为识别

    基于mediapipe的动作捕捉和Unity的球棍模型同步

    Mediapipe姿态估计——用坐标计算手指关节弯曲角度并实时标注