概括

通过Dlib获得当前人脸的特征点,然后通过旋转平移标准模型的特征点进行拟合,计算标准模型求得的特征点与Dlib获得的特征点之间的差,使用Ceres不断迭代优化,最终得到最佳的旋转和平移参数。

Android版本在原理上同C++版本:头部姿态估计 - OpenCV/Dlib/Ceres

主要介绍在移植过程中遇到的问题。

使用环境

系统环境:Ubuntu 18.04

Java环境:JRE 1.8.0

使用语言:C++(clang), Java

编译工具:Android Studio 3.4.1

  • CMake 3.10.2
  • LLDB
  • NDK 20.0

上述工具在Android Studio中SDK的管理工具里下载即可。

第三方工具

Dlib:用于获得人脸特征点

Ceres:用于进行非线性优化

源代码

https://github.com/Great-Keith/head-pose-estimation/tree/master/android/landmark-fitting

准备工作

第三方库的Android接口

Dlib

使用的GitHub上提供的现成接口:https://github.com/tzutalin/dlib-android

该项目还提供了具体的app样例:https://github.com/tzutalin/dlib-android-app/

我们所做的app就是建立在该app样例之上。

Ceres

具体使用可以参见前一篇随笔:Android平台使用Ceres Solver

总之最后我们整合Dlib和Ceres得到了我们app的基本框架:https://github.com/Great-Keith/dlib-android-app

增加前置摄像头转换

增设转换按钮

最初的样例dlib-android-app仅提供了后置摄像头,这对于单人测试很不方便,因此我们修改代码来实现一个切换前后摄像头的按钮。

首先找到相机视图res/layout/camera_connection_fragment.xml,在其右上角增加 Switch 按钮。

最后我们找到该app的实现细节,是通过自己新建一个CameraConnectionFragment类来替换原本的Fragment,从而实现的一系列操作。该类中setUpCameraOutputs方法实现了对相机的选择,其会便利移动设备上可用的所有相机,优先选择后置摄像头。

给该方法增加一个boolean b参数,用于选择摄像头:

  1. if(b) {
  2. // 只使用后置摄像头
  3. // If facing back camera or facing external camera exist, we won't use facing front camera
  4. if (num_facing_back_camera != null && num_facing_back_camera > 0) {
  5. // 前置摄像头跳过(如果有后置摄像头)
  6. // We don't use a front facing camera in this sample if there are other camera device facing types
  7. if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
  8. continue;
  9. }
  10. }
  11. } else {
  12. // 只使用前置摄像头
  13. if (num_facing_front_camera != null && num_facing_front_camera > 0) {
  14. // 前置摄像头跳过(如果有后置摄像头)
  15. // We don't use a front facing camera in this sample if there are other camera device facing types
  16. if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
  17. continue;
  18. }
  19. }
  20. }

然后在初始化的过程中关联上我们的Switch按钮:

  1. switchBtn = view.findViewById(R.id.cameraSwitch);
  2. switchBtn.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
  3. @Override
  4. public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
  5. closeCamera();
  6. openCamera(textureView.getWidth(), textureView.getHeight(), b);
  7. }
  8. });

[NOTE] 通过openCamera将参数b传输给setUpCameraOutputs

修复前置摄像头倒转

修改完后我们运行程序,会发现出现预显示窗口倒转的情况,因此我们需要对预显示窗口的显示进行翻转。

找到相机类处理捕捉到的画面的监听器OnGetImageListener,其中对捕捉到的画面进行处理的函数即为drawResizedBitmap,在最终绘制之前,增加矩阵翻转。

  1. /* If using front camera, matrix should rotate 180 */
  2. if(!switchBtn.isChecked()) {
  3. matrix.postTranslate(-dst.getWidth() / 2.0f, -dst.getHeight() / 2.0f);
  4. matrix.postRotate(180);
  5. matrix.postTranslate(dst.getWidth() / 2.0f, dst.getHeight() / 2.0f);
  6. }
  7. final Canvas canvas = new Canvas(dst);
  8. canvas.drawBitmap(src, matrix, null);

[NOTE] 在该类中没有办法直接获取CameraId来判断当前使用的相机是前置还是后置,因此我们通过之前的Switch按钮来进行判断。查阅可能可以使用的Camera类在API 21以后淘汰使用了。

主要过程

还是在相机的监听器当中,我们可以看到dlib获得的特征点数据,并进行绘制。

  1. mInferenceHandler.post(
  2. new Runnable() {
  3. @Override
  4. public void run() {
  5. // ...
  6. long startTime = System.currentTimeMillis();
  7. List<VisionDetRet> results;
  8. synchronized (OnGetImageListener.this) {
  9. results = mFaceDet.detect(mCroppedBitmap);
  10. }
  11. long endTime = System.currentTimeMillis();
  12. mTransparentTitleView.setText("Time cost: " + String.valueOf((endTime - startTime) / 1000f) + " sec");
  13. // Draw on bitmap
  14. if (results != null) {
  15. for (final VisionDetRet ret : results) {
  16. // 绘制人脸框和特征点
  17. // ...
  18. }
  19. }
  20. }
  21. mWindow.setRGBBitmap(mCroppedBitmap);
  22. mIsComputing = false;
  23. }
  24. });

我们选择在绘制人脸框和特征点的for循环中增加优化。

首先将特征点复制一份Point数组,用于作为传入参数。

  1. /* Transform landmarks to array, which is needed by JNI */
  2. Point[] tmp = landmarks.toArray(new Point[0]);

初始化好double x[]随后我们可以调用我们的CeresSolver类来进行处理,得到的最优解通过指针x返回。

  1. CeresSolver.solve(x, tmp);

最后我们再调用两个方法来进行将三维特征点转化为二维的映射。

  1. Point3f[] points3f = CeresSolver.transform(x);
  2. Point[] points2d = CeresSolver.transformTo2d(points3f);

[NOTE] 项目中的二维点使用android.graphics.Point(对应C++中使用的dlib::point),而三维点使用我们自己建的一个类Point3f(对应C++中使用的dlib::vector<double, 3>)。

综上,我们实际上要实现的是一个提供Ceres支持的工具类CeresSolver,下面具体描述。

CeresSolver类与其JNI接口

初始化

我们需要读取标准模型特征点的三维坐标,该坐标存储于landmarks.txt文件中。对于Android工程,我们将该文件放在assets目录下。在CameraActivity初始化onCreate的时候顺带进行初始化:

  1. CeresSolver.init(getResources().getAssets().open("landmarks.txt"));

该初始化具体过程如下:

  1. public static void init(InputStream in) {
  2. try {
  3. InputStreamReader inputReader = new InputStreamReader(in);
  4. BufferedReader bufReader = new BufferedReader(inputReader);
  5. String line;
  6. int i = 0;
  7. while((line = bufReader.readLine()) != null) {
  8. String[] nums = line.split(" ");
  9. modelLandmarks[i] = new Point3f(Double.valueOf(nums[0]),
  10. Double.valueOf(nums[1]),
  11. Double.valueOf(nums[2]));
  12. i++;
  13. }
  14. } catch (Exception e) {
  15. Log.e(TAG, "Loading model landmarks from file failed.");
  16. e.printStackTrace();
  17. }
  18. Log.i(TAG, "Loading model landmarks from file succeed.");
  19. init_();
  20. }

init_是一个JNI的函数,用于将CeresSolver类中读取的modelLandmark数据读取到本地变量``model_landmark,并提前读取一些jmethodIDjfieldID

[NOTE] 其实也可以通过调用jmethodID或者jfieldID来获得Java类中的modelLandmark,但我目前不是很清楚两种方法之间在效率上的差异。

[NOTE] 将这些数据提前在cpp文件中读取并保存成静态变量,这个过程有一些问题,由于Java的垃圾回收机制,JNI中的静态类型,有些会失去关联(可能是指针?)。比如jfieldID的调用往往没有问题,但是jclass就会失效,因此jclass类型无法提前先初始化好。

解决最小二乘

同C++一样,提前定义好CostFunctor:

  1. struct CostFunctor {
  2. public:
  3. explicit CostFunctor(JNIEnv *_env, jobjectArray _shape){
  4. env = _env;
  5. shape = _shape; }
  6. bool operator()(const double* const x, double* residual) const {
  7. /* Init landmarks to be transformed */
  8. fitting_landmarks.clear();
  9. for (auto &model_landmark : model_landmarks)
  10. fitting_landmarks.push_back(model_landmark);
  11. transform(fitting_landmarks, x);
  12. std::vector<Point2d> model_landmarks_2d;
  13. landmarks_3d_to_2d(fitting_landmarks, model_landmarks_2d);
  14. /* Calculate the energe (Euclid distance from two points) */
  15. for(unsigned long i=0; i<LANDMARK_NUM; i++) {
  16. jobject point = env->GetObjectArrayElement(shape, static_cast<jsize>(i));
  17. long tmp1 = env->GetIntField(point, getX2d) - model_landmarks_2d.at(i).x;
  18. long tmp2 = env->GetIntField(point, getY2d) - model_landmarks_2d.at(i).y;
  19. residual[i] = sqrt(tmp1 * tmp1 + tmp2 * tmp2);
  20. }
  21. return true;
  22. }
  23. private:
  24. JNIEnv *env;
  25. jobjectArray shape; /* 3d landmarks coordinates got from dlib */
  26. };

基本与C++相同,唯一不同的地方是shape的类型直接使用的JNI中的类型jobjectArray,并且需要使用到调用,因此需要在初始化的时候导入JNIEnv环境。

其余在调用部分就和C++部分基本相同,所有的JNI函数都需要注意在参数传入和传出的时候进行类型的转变。

坐标转化

涉及三维点旋转和平移的转化以及三维点转二维点的转化,同C++中的涉及。

需要另外提供JNI接口给Java中的类使用,主要涉及jobject的方法调用、成员访问等等。当然,也可以在Java中实现这些方法,感觉效率会更高一些。这一部分具体可以看源代码,其中有详细的注释。

信息打印(Debug)

在Android项目中,输出的消息很多,debug的难度是比较大的,因此需要灵活使用打印信息来获得所需要的信息。其中Java程序中可以使用android.util.Log来进行输出,可以在logcat或者run中进行查看。具体比如:

  1. Log.i(TAG, String.format("After Solve x: %f %f %f %f %f %f",
  2. x[0], x[1], x[2], x[3], x[4], x[5]));

JNI的cpp文件中,定义如下宏定义来进行输出:

  1. #define TAG "CERES-CPP"
  2. #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG,__VA_ARGS__)
  3. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG,__VA_ARGS__)
  4. #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG,__VA_ARGS__)
  5. #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG,__VA_ARGS__)
  6. #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, TAG,__VA_ARGS__)

使用该Log需要在CMakeLists.txt中需要链接log库。

结果测试

进入相机界面,并进行摄像头的切换。

这边可以看到,刚打开的时候,这个求解得到的点是非常混乱的,这是由于初始值没有设置好,在经过一段时间后就会进入正常状态。

*[NOTE] 后来我在读入的模型特征点的时候加入一个缩放系数(1/100),效果得到很好的改善。

实时效果

总结

因为整体逻辑在C++已经实现了,所以复制这个逻辑的过程并不困难。难点主要是在JNI的使用上,没有接触过NDK的我在将Ceres移植到安卓平台上花费了大量的时间,最后写了Android平台使用Ceres Solver总结了这个过程。当这一部分完成之后,后面的过程就快了起来,但关于JNI的很多特性,跟Java息息相关,还需要更多的摸索。

进一步可以优化

  • 初始值选择问题;
  • 去除app中的识别行人模块;
  • 优化使用Ceres求解最小二乘的过程;
  • 前后摄像头显示区别;
  • 优化接口,使其更据扩展性。

头部姿态估计 - Android的更多相关文章

  1. 头部姿态估计 - OpenCV/Dlib/Ceres

    基本思想 通过Dlib获得当前人脸的特征点,然后通过旋转平移标准模型的特征点进行拟合,计算标准模型求得的特征点与Dlib获得的特征点之间的差,使用Ceres不断迭代优化,最终得到最佳的旋转和平移参数. ...

  2. 使用unity3d和tensorflow实现基于姿态估计的体感游戏

    使用unity3d和tensorflow实现基于姿态估计的体感游戏 前言 之前做姿态识别,梦想着以后可以自己做出一款体感游戏,然而后来才发现too young.但是梦想还是要有的,万一实现了呢.趁着p ...

  3. Facebook提出DensePose数据集和网络架构:可实现实时的人体姿态估计

    https://baijiahao.baidu.com/s?id=1591987712899539583 选自arXiv 作者:Rza Alp Güler, Natalia Neverova, Ias ...

  4. PCL学习(五)如何在mesh模型上sample更多点及三维物体姿态估计

    ---恢复内容开始--- 最近在做关于物体姿态估计的项目 基本思路就是 我们在估计物体的pose的时候,需要用分割得到的点云与模型库中的模型做匹配 1.通过基于RANSANC的SAC-IA将点云和模型 ...

  5. CVPR2020文章汇总 | 点云处理、三维重建、姿态估计、SLAM、3D数据集等(12篇)

    作者:Tom Hardy Date:2020-04-15 来源:CVPR2020文章汇总 | 点云处理.三维重建.姿态估计.SLAM.3D数据集等(12篇) 1.PVN3D: A Deep Point ...

  6. CVPR 2020几篇论文内容点评:目标检测跟踪,人脸表情识别,姿态估计,实例分割等

    CVPR 2020几篇论文内容点评:目标检测跟踪,人脸表情识别,姿态估计,实例分割等 CVPR 2020中选论文放榜后,最新开源项目合集也来了. 本届CPVR共接收6656篇论文,中选1470篇,&q ...

  7. 快速人体姿态估计:CVPR2019论文阅读

    快速人体姿态估计:CVPR2019论文阅读 Fast Human Pose Estimation 论文链接: http://openaccess.thecvf.com/content_CVPR_201 ...

  8. 相机姿态估计(Pose Estimation)

    (未完待续.....) 根据针孔相机模型,相机成像平面一点的像素坐标p和该点在世界坐标系下的3D坐标P有$p=KP$的关系,如果用齐次坐标表示则有: $$dp=KP$$ 其中d是空间点深度(为了将p的 ...

  9. openpose-opencv 的body数据多人体姿态估计

    介绍 opencv除了支持常用的物体检测模型和分类模型之外,还支持openpose模型,同样是线下训练和线上调用.这里不做特别多的介绍,先把源代码和数据放出来- 实验模型获取地址:https://gi ...

随机推荐

  1. java Https工具类

    import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import ja ...

  2. 机器学习读书笔记(一)k-近邻算法

    一.机器学习是什么 机器学习的英文名称叫Machine Learning,简称ML,该领域主要研究的是如何使计算机能够模拟人类的学习行为从而获得新的知识和技能,并且重新组织已学习到的知识和和技能,使之 ...

  3. LinkedList源码分析:JDK源码分析系列

    如果本文中有不正确的地方请指出由于没有留言可以在公众号添加我的好友共同讨论. 1.介绍 LinkedList 是线程不安全的,允许元素为null的双向链表. 2.继承结构 我们来看一下LinkedLi ...

  4. Java内存模型与内存结构

    Java内存模型 一.简介 Java内存模型(JMM)主要是为了规定线程和内存之间的一些关系:根据JMM的设计,系统存在一个主内存(Main Memory)和工作内存(Work Memory),Jav ...

  5. ICC中用Tcl脚本给版图中的Port/Terminal加Label的方法

    本文转自:自己的微信公众号<数字集成电路设计及EDA教程> 里面主要讲解数字IC前端.后端.DFT.低功耗设计以及验证等相关知识,并且讲解了其中用到的各种EDA工具的教程. 考虑到微信公众 ...

  6. android_layout_linearlayout(二)

    android的线性布局linearlayout的研究没有尽头.看了官网关于线性布局的一个例子,捣鼓一阵,发现的迷惑记录在此. 一.先看看官网xml <?xml version="1. ...

  7. 一次线上遇到磁盘IO瓶颈的问题处理

    Load  average %wa    的含义是等待输入输出的CPU时间百分比 结合iostat命令可以发现磁盘已经在100%满负荷在跑 await:每一个IO请求的处理的平均时间(单位是毫秒).这 ...

  8. 从后端到前端之Vue(一)写个表格试试水

    目录: 1.脚本式开发. 2.工程化开发 3.工程化和脚本的区别 4.来个table试试水 4,1.目标 4.2.思路 4.3.设计与编码 4.4.效果 5.业务分离 6.功能拓展——个性化设置    ...

  9. 网页内嵌html遇到的问题

    在项目中遇到个问题 充值功能是点击一个按钮这个按钮会弹出模态框,输入充值金额会执行一段脚本自动提交数据到https://openapi.alipay.com/gateway.do上 结果:本网页跳转到 ...

  10. numpy表示图片详解

    我自己的一个体会,在学习机器学习和深度学习的过程里,包括阅读模型源码的过程里,一个比较大的阻碍是对numpy掌握的不熟,有的时候对矩阵的维度,矩阵中每个元素值的含义晕乎乎的. 本文就以一个2 x 2 ...