用MediaPipe实现一个DLL满足各种姿态捕捉
这个文章并没有什么技术含量。只是我自己在实现这个DLL的时候,发现文档很少,官网文档也很不够,踩了很多坑。写这个只是希望能帮到一些后来人,少踩坑。当然了,也许文档已经很多了,我找不到是我太菜?有可能。
另,我基本不做移动端开发,这里实现的DLL只针对win10,c++。其他的完全不涉及。如果找其他的相关的API,只能借鉴,未必有用。
首先,编译C++的example。这方面的文档已经足够多了,随便找找就有,官网也有,所以这里不详细写,只说一些坑。一个坑是版本。做这类编译的时候,千万不要自己没事找事。例如人家官网编译文档,openCV用的是3.4.10,你就不要用其他版本。例如我之前已经有了更高的版本,但是为了编译这个,我重新下了这个低版本的,就是避免出乱七八糟的问题。Python的版本同样如此。一句话,不要没事自找不痛快,换版本。
好了,这里假设你已经有了一定的c++基础,已经编译出来了hello,world,而且成功跑了起来。可以开始了。
我们先说一个手势识别的dll。这里,文档其实也已经很多了。例如这里就有一个,已经完成了的:https://github.com/HW140701/Google_Mediapipe_Hand_Tracking_dll。我刚开始的时候也是看了一下这个。不过我并没有用这个。一来这个dll的代码风格跟我差别巨大。另外这个只有手势识别,我需要各种识别,不可能只用这一个。所以,我是看了一下这里的核心代码,然后全部重写。渲染sample我自己用的d3d11。
假设你已经完全看懂了上面的sample,手势识别已经做出来了。那么问题来了,很多代码没什么注释,也不知道为什么这样写。例如这里有一个“pose_landmarks”,到底是个什么东西?我自己详细看了一下各种pbtxt文件,大概是这些:
里面,有比较详细的描述。我以手势识别那个为例。
看这里,这里有个landmarks,所以我猜测应该是在这里定义的stream。后来我测试了一下,果然是这样的。所以,当你需要某个信息的时候,步骤大概是这样的:
1、先看看pbtxt里面,包含了什么Stream,例如输入的,输出的。这里,landmark,其实就是一个一个的点。
2、代码里面,你需要加入差不多这样的:
ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller poller, mCalculatorGraph.AddOutputStreamPoller(kOutputStream));
ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller poller_output, mCalculatorGraph.AddOutputStreamPoller(kOutputLandmarks[mTrackingType]));
先用AddOutputStreamPoller,增加一个poller,这里的代码差不多都属于依着葫芦画瓢,至于为什么,我也不知道,照抄的sample。
3、再要增加一个packet。我猜测,这里的架构大概是这样的,你增加一个输出stream,然后用Next函数就能够得到这里的packet,然后packet里面,再调用Get,获取到你希望的信息。我是按照这个流程走的,然后发现结果都对。
这里有一个坑,你如何知道,你packet调用Get的时候,该用什么类型?我开始也是不知道的,然后各种查,什么文档都没查到,也许是我太菜。后来我就想,example都是要画出来的,example怎么知道自己该画什么,不该画什么呢?然后我就去查example的渲染,真被我找到了。例如可以看这个文件:mediapipe\graphs\hand_tracking\subgraphs\hand_renderer_cpu.pbtxt,里面就有这样的描述:
这就像黑夜中看到了曙光,我立马按照这个套路操作,成功识别到了左手、右手。这个需要先定义一个字符串:“handedness”,然后把这个加入到outputstream,返回类型是:ClassificationList。
识别出了左右手,我就打算识别人脸。识别人脸的过程中,很容易看到,这个时候需要找一个叫做Detection的,但是我找啊找,怎么都找不到哪里可以获取到Detection的什么坐标之类的。后来就怀疑是不是这个套路出了问题。再后来,我终于找到了一个地方,叫做“"mediapipe/framework/formats/location_data.pb.h"”
反正是之前眼睛不大好,一直没看到Detection里面,有一个叫做LocationData的东东。需要先获得Detecttion,然后再获取LocationData,然后LocationData里面,再细分。包括什么相对坐标,KeyPoint之类的描述。KeyPoint里面包含了鼻子眼睛之类的点。
人脸识别搞了之后,就搞那个FaceMesh。这里,文档也很少。这个很容易能得到一堆LandMark,也就是很多点。但是,人家的sampele里面,是有连线的。例如把眉毛连起来。你现在得到478个点,怎么连线呢?这个,我又找了半天文档,没找到。后来我又想到,看看example是怎么连线的。然后我又找example渲染的地方,又给我找到了。是这里:
“mediapipe\graphs\face_mesh\calculators\face_landmarks_to_render_data_calculator.cc”
这里面,有详细的连线代码,有点像D3D里面的Index那种连线做法,按照这个,很容易就把脸给连起来了。我想了一下,用这个来做一个识别,例如在UE4里面做一个人脸,然后识别某个人的表情,然后把这个表情渲染到游戏里,还是挺可以的事。下面是我渲染的效果:
到了这里,大部分的坑都踩了,我以为已经是一片坦途。后来搞那个Selfie_Segmentation的时候,又坑了。原因在于,这里文档说的是一张mask图,我猜测是返回一张图片的mask。例如假设图片是640*480,每个像素一个float,0-1之间。这里,被坑的主要原因,是先入为主。
为什么呢,因为之前发现了一个规律,在framework\formats文件夹里面,有很多.proto文件,以我之前的理解,这些是定义了一个消息机制。例如之前的什么LandMark,什么location_data等等,全部都有这个proto。也就是说,我先入为主的认为,这个mask图,必然也有有一个proto文件,专门描述怎么记录消息。我以为代码的框架就是这样设定的。没想到这里不按常理出牌。我找遍了所有的proto文件,一个一个全部看完了,都找不到。
后来只能死马按照活马医的方法,不管不顾了,直接用返回ImageFrame,然后试了一下,居然成了。
至此,几乎所有的坑都跑了,然后DLL基本完成。支持各种人脸,姿势,抠图等等骚操作。效果嘛,还是可以的。比较坑爹的有两点:
1、Detection这个检测,没法识别到一个ID。例如你识别到几个人脸,你怎么知道哪个脸是谁的?我尝试了好多骚操作,都找不到一个唯一ID。例如我尝试了什么feature_tag,什么trackID,还有什么Detection_id(),一个个返回结果都不对。后来放弃了,毕竟实在不行,可以自己根据实时范围实现一个识别。有知道的大佬也可以说一下,谢谢。
2、姿态识别,只能识别一个人。我找遍了各个地方也没找到好的解决方案,也许是我太菜。然后我发现网上一个方案,居然是先识别矩形,然后把视频分割,再用多个实例识别多个POSE。例如假设视频里有两个人,你先把这两个人识别出来,然后把单个视频分割成两个,再一个一个视频去单独识别。这个操作也是醉了。如果姿势之间有叠加,这个操作立马就废了。我实在接受不了。然后看到别人都是提议多个姿态识别,用OpenPose或者PoseNet之类的。OpenPose看着是CMU出品,应该品质有保证,不过我没研究过。我想有需要的时候,我再研究吧,说不定MediaPipe后面升级就增加了多姿态支持呢?Google的东西按道理也是有保障的。
大概就这样,希望能帮到有需要的同学,少踩坑。