QXRService:基于高通QXRService获取SLAM Camera图像

上一篇博文讲解了基于Snapdragon XR OpenXR SDK v1.x系列SDK怎么进行QXRService实战开发,以及通过QXRService相关API如何获取到头显位姿和IMU数据。

这一篇就详细讲解如何基于QXRService获取到头显SLAM Camera摄像头的图像数据

我们继续沿用上一篇博文中创建的qvr-test工程,在qvr-test的基础上进行开发。

qvr-test工程的创建以及对QXRService相关资源加载、结构体初始化等在上一讲中已有完整介绍,这里就不再重复了。

废话不多说,开始了。

QXRService对摄像头图像的获取总结起来目前有如下规则:
1.不支持单颗SLAM摄像头图像获取,只支持底部或顶部SLAM摄像头融合后的图像获取
2.支持设置图像融合方式:垂直融合或水平融合
3.需要先传入当前要获取的底部或顶部SLAM摄像头设备名给QXRService
4.顶部SLAM摄像头名称需要开发者自行查找并设置,QXRService并未对其进行定义(坑!)
5.
支持VST(Video See Through)摄像头图像获取

对上述规则再详细说明下:

(1).高通不支持单颗摄像头图像的获取,只支持配对后的“底部两颗SLAM摄像头” 或 “顶部两颗SLAM摄像头”融合后图像的获取,以及“VST单颗摄像头”图像 或 “VST两颗摄像头”融合后图像的获取。

在高通文档《80-pj703-29_aa_xr2_qvrservice_camera_configuration.pdf》中有这样的注解:                                                                    也就是说对Native层的CameraService和Camera Hardware层来说,每个摄像头都是具备单独物理Camera Id的摄像头,但是QVRService会将两个配对的物理Camera进行融合成一个逻辑Camera,同时做软件同步。

(2).在使用QXRService的QVRCameraDevice_GetFrame()获取SLAM Camera图像之前,可以使用QVRCameraDevice_SetParam()设置要获取的图像的融合方式:水平融合还是垂直融合

还可以调用QVRCameraDevice_SetCropRegion()对融合的两帧图像分别进行裁剪,最终获取到的是两张裁剪后图像的融合图像。

QVRCameraClient.h中关于QVRCameraDevice_GetFrame()的函数注释中对此有较为详细的讲解,截取注释片段如下:

 

(3).在调用QVRCameraDevice_GetFrame()获取图像前,我们需要在QXRCamClient_AttachCamera()时传入Camera device name,也就是我们要获取图像的Camera设备名,QVRCameraClient.h中对Camera device name已有定义的有如下几个:


我们现在要拿的是SLAM Camera的图像,但是可以看到其中只有底部SLAM Camera设备名:
#define QVRSERVICE_CAMERA_NAME_TRACKING     "tracking"
并没有顶部SLAM Camera设备名。

起初我认为是QXRService不支持顶部SLAM Camera的图像获取,高通的FAE最初也反馈说不支持,但是跟高通的多个FAE多次确认后却分别有着不同的结论,最后通过综合高通反馈的信息和自己实践才发现,QVRCameraClient.h中虽然没有对顶部SLAM Camera名称进行定义,但是如果我们自己传入"顶部SLAM Camera名称"依然是可以正常获取到图像的。

至于需要传入什么样的"顶部SLAM Camera设备名"呢?

在《QXRService:高通SnapdragonXR OpenXR SDK v1.x 概略》这篇博文中曾经提到过,在头显设备的 /vendor/etc/ 路径下有一个 qvrservice_config.txt 文件,高通native系统中会将头显设备比较重要的一些参数在这个文件中进行定义。

但是qvrservice_config.txt这个文件它的修改会影响整个头显,所以除了开发人员临时使用外,一般我们会将其中的参数在 /vendor/etc/trinity.txt 文件中进行定义和修改。

可以看到,对SLAM Camera的设备名定义一共有两对:
一个是底部SLAM Camera,名称和高通QVRCameraClient.h文件中定义的是一样的(如果trinity.txt中的名称与高通QVRCameraClient.h文件中定义的名字不一致,那么应该以trinity.txt文件中的名称为准)
一个就是顶部SLAM Camera设备名,也就是ctrl-tracking

(4).高通的VST采用了一种高通独有的区别于传统Camera图像数据传输链路(Full Mode)的Partial Frame模式,就是VST图像从mipi出来之后,经过CAMX(IFE)/CameraService、QXRService(QVRCameraService)后,直接会被传输到一个Ring Buffer中,Ring Buffer负责将输送过来的Frame再直接输出到GPU显存中,号称做到零延时传输。

我们这里对高通VST的Partial Frame模式不做扩展讲解,详细细节可以参看高通的《80-PJ703-19-XR Video See Through.pdf》这篇文档。

这里主要是介绍对VST图像的获取。

在最初询问多个高通FAE QXRService是否支持对VST图像获取,得到的都是斩钉截铁的答复:不支持!直到现在他们也是这么答复。

但是在QVRCameraClient.h中找一找就可以很清楚的看到这样一个API:
QVRCameraDevice_GetFrameEx()
通过这个Api的注释就能看到,这就是QXRService中专门用来获取VST图像的Api:

总体感觉,高通国内XR相关的FAE对QXRService的Api、结构体、变量的功能属性以及调用流程,一是压根不重视,二是基本不了解。问到相关问题不是不知道就是回答错误,或者直接答复QXRService将来会被封装起来不对外暴露了事。

这可能也与高通所说的往后要弱化QXRService,主推OpenXR+Runtiem的一体化SDK有关吧。

讲到这里,基本上通过QXRService获取图像的相关技术点都已作阐述,接下来就开始具体代码开发

一.activity_main.xml

我们依然是添加两个Button,分别用于控制 开始和结束 对 顶部和底部 SLAM Camera的图像抓取。

activity_main.xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_get_pose"
        android:layout_width="155dp"
        android:layout_height="60dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="52dp"
        android:text="Start-GetPose"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_get_imu"
        android:layout_width="155dp"
        android:layout_height="60dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="128dp"
        android:text="Start-GetIMU"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!--用于顶部SLAM Camera图像抓取的Button-->
    <Button
        android:id="@+id/btn_get_frame_up"
        android:layout_width="155dp"
        android:layout_height="60dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="204dp"
        android:text="Start-GetFrameUp"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!--用于底部SLAM Camera图像抓取的Button-->
    <Button
        android:id="@+id/btn_get_frame_bottom"
        android:layout_width="155dp"
        android:layout_height="60dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="284dp"
        android:text="Start-GetFrameBottom"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

二.MainActivity.java

在MainActivity.java中需要增添如下操作:
1.实例化Button,并添加点击事件
2.添加两个用于流程控制的boolean变量
3.调用新增JNI Api

QXRService的开发主要是在Jni层,Java层的逻辑越简单越好

代码如下:

package com.qvr.test;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private String TAG = "APP_LOG";

    private Button btnGetPose;
    private Button btnGetIMU;
    //添加两个用于抓取图像的Button
    private Button btnGetFrameUp;
    private Button btnGetFrameBottom;

    private boolean mStartGetPose = false;
    private boolean mStartGetIMU = false;
    //添加两个用于控制抓取图像的boolean变量
    private boolean mStartGetFrameUp = false;
    private boolean mStartGetFrameBottom = false;

    private JNI mJni = new JNI();

    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();

        //在java app加载完成native库后把 context传到native库中。
        mJni.nativeStoreContext(getApplicationContext());

        //context传入到native之后,对QXRService进行初始化
        mJni.nativeInitQxrService();
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    public void initView() {
        btnGetPose = (Button) this.findViewById(R.id.btn_get_pose);
        btnGetPose.setOnClickListener(this);

        btnGetIMU = (Button) this.findViewById(R.id.btn_get_imu);
        btnGetIMU.setOnClickListener(this);

        btnGetFrameUp = (Button) this.findViewById(R.id.btn_get_frame_up);
        btnGetFrameUp.setOnClickListener(this);

        btnGetFrameBottom = (Button) this.findViewById(R.id.btn_get_frame_bottom);
        btnGetFrameBottom.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_get_pose: {
                if (!mStartGetPose) {
                    btnGetPose.setText("Stop-GetPose");
                    mStartGetPose = true;

                    //调用jni api,开始获取QXRService输出的SLAM Pose
                    mJni.nativeStartSavePose();
                } else {
                    btnGetPose.setText("Start-GetPose");
                    mStartGetPose = false;

                    //调用jni api,结束获取QXRService输出的SLAM Pose
                    mJni.nativeStopSavePose();
                }
            }
            break;

            case R.id.btn_get_imu: {
                if (!mStartGetIMU) {
                    btnGetIMU.setText("Stop-GetIMU");
                    mStartGetIMU = true;

                    //调用jni api,开始获取QXRService输出的IMU data
                    mJni.nativeStartGetIMU();
                } else {
                    btnGetIMU.setText("Start-GetIMU");
                    mStartGetIMU = false;

                    //调用jni api,结束获取QXRService输出的IMU data
                    mJni.nativeStopGetIMU();
                }
            }
            break;

            //抓取顶部SLAM Camera Button的响应事件
            case R.id.btn_get_frame_up: {
                if (!mStartGetFrameUp) {
                    btnGetFrameUp.setText("Stop-GetFrameUp");
                    mStartGetFrameUp = true;

                    mJni.nativeStartGetUpFrame();
                } else {
                    btnGetFrameUp.setText("Start-GetFrameUp");
                    mStartGetFrameUp = false;

                    mJni.nativeStopGetUpFrame();
                }
            }
            break;

            //抓取底部SLAM Camera Button的响应事件
            case R.id.btn_get_frame_bottom: {
                if (!mStartGetFrameBottom) {
                    btnGetFrameBottom.setText("Stop-GetFrameBottom");
                    mStartGetFrameBottom = true;

                    mJni.nativeStartGetBottomFrame();
                } else {
                    btnGetFrameBottom.setText("Start-GetFrameBottom");
                    mStartGetFrameBottom = false;

                    mJni.nativeStopGetBottomFrame();
                }
            }
            break;

            default:
                break;
        }
    }
}

三.JNI.java

新增4个Native Api

用于Java调用控制 开始和结束 对 顶部和底部 SLAM Camera的图像抓取

代码如下:

package com.qvr.test;

import android.content.Context;

import androidx.annotation.NonNull;

public class JNI {
    {
        System.loadLibrary("qxrtest");
    }

    public native void nativeStoreContext(@NonNull Context context);

    public native void nativeInitQxrService();

    public native void nativeStartSavePose();

    public native void nativeStopSavePose();

    public native void nativeStartGetIMU();

    public native void nativeStopGetIMU();

    //以下为四个新增用于SLAM Camera图像抓取的native api
    public native void nativeStartGetBottomFrame();

    public native void nativeStopGetBottomFrame();

    public native void nativeStartGetUpFrame();

    public native void nativeStopGetUpFrame();
}

四.JNI部分:qxrtest.h

qxrtest.h中新增如下操作:
1.添加两个用于分别停止循环获取顶部和底部SLAM Camera融合图像线程的bool变量
2.添加一个void writeBufferToFile()的函数声明,该函数用于将获取到的图像数据保存成文件

代码如下:

//
// Created by shawn1.xiao on 2022/6/20.
//
#include <jni.h>
#include <string>
#include <unistd.h>
#include <android/log.h>

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#include "QXRCamClient.h"
#include "QXRCoreClient.h"
#include "QVRServiceClient.h"

#include <pthread.h>
#include <fstream>
#include <iostream>
#include <typeinfo>
#include <string>
#include <cmath>

#define LOG_TAG "QVR-Test"
#define LOGW(...) __android_log_print( ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__ )

#ifdef __cplusplus
extern "C" {

static struct {
    struct _JavaVM *vm;
    jobject context;
} jni_android_info;

//QVRServiceClient handler
static qvrservice_client_helper_t *qvrservice_client = NULL;
static qvrservice_head_tracking_data_t *head_tracking_data = NULL;
static qvrservice_sensor_data_raw_t *sensor_raw_data = NULL;

bool mIsStopSavePose = false;
bool mIsStopGetIMU = false;

//新增两个bool变量和一个函数声明
bool mIsStopGetBottomFrame = false;
bool mIsStopGetUpFrame = false;
void writeBufferToFile(qvrcamera_frame_t qvr_frame, bool isBottom);

}
#endif

五.JNI部分:qxrtest.cpp

qxrtest.cpp中主要需增加如下操作:
1.在/data/data/com.qvr.test/路径下,创建两个名为camera8和camera9的文件夹,用于保存顶部或底部SLAM Camera抓取到的图像,camera8对应底部SLAM Camera,camera9对应顶部SLAM Camera.
2.新增两个分别用于循环抓取顶部和底部SLAM Camera融合图像的线程
3.实现对图像数据的保存

功能实现详细说明:
1.图像抓取线程的创建与上一讲获取头显Pose和IMU数据线程类似,仍然是由pthread_create()创建,分别创建如下两个线程:
void *saveUpFrameThread(void *arg){...}
void *saveBottomFrameThread(void *arg) {...}

2.线程中仍然是使用while()循环,由bool变量来控制循环结束

3.QXRService输出的图像(Frame)是在QVRCameraClient.h中定义的一个名为qvrcamera_frame_t的结构体:

其中每个成员变量的注释都很详细,不再逐一标注,
值得注意的几点是:

(1).每一帧图像的时间戳是摄像头开始曝光的Boottime,单位是纳秒。

可能有小伙伴会有疑问,QXRSevice输出的不是两个摄像头分别输出后再做融合处理的图像吗,怎么只有一个曝光开始时间了?

这就是我们先前提到过的高通软件同步机制的作用,软件同步会让需要做融合图像处理的摄像头同时曝光。

高通目前没有硬件同步,若需要实现硬件同步,需要自己添加硬件同步模块,并进行软件实现。

(2).图像数据也就是Frame data的内存地址,保存在一个uint8_t指针中,我们需要对buffer内容进行保存操作,将图像数据保存成文件。

SLAM Camera一般使用的是黑白摄像头,输出的是纯灰度图,我们直接对图像数据按字节进行保存即可。

前文中提到过的void writeBufferToFile(...){...}函数就是用来实现图像的保存

(3).从QXRService获取到的是融合后的图像,所以在不做QVRCameraDevice_SetCropRegion()处理的且水平融合的情况下,获取到的融合图像width是每个摄像头输出的单个图像width的2倍,垂直融合则反之。

如果对图像进行过QVRCameraDevice_SetCropRegion()处理,width和height就是分别两张Crop Image的width和height之和。

由于QVRCameraDevice_SetCropRegion()可以对融合前的两张图像分别进行操作,所以就有了width、height、secondary_width;、secondary_height四个成员变量,分别用于表示Crop Image1的width、height和Crop Image2的width、height。

至于图像的Crop融合的细节,参看先前提到过的QVRCameraDevice_GetFrame()函数的注释。

(4).SLAM Camera输出的图像格式是Y8格式,Y8是由Width*Hight Y平面组成的YUV平面格式,每个像素由8位表示。它仅相当于YV12格式中的Y平面。

代码如下:
(由于CSDN代码数限制,QXRService初始化、Pose和IMU数据获取等部分代码就先简略掉,具体可参看上一篇博文):

/*
/* Created by shawn1.xiao on 2022/6/20.
*/
#include "qxrtest.h"

using namespace std;

#ifdef __cplusplus
extern "C" {

string txt = ".txt";
//根目录和保存图像的目录
string rootPath = "/data/data/com.qvr.test/";
string folderUpPath = rootPath + "camera9/";
string folderBottomPath = rootPath + "camera8/";

JNIEXPORT void JNICALL
Java_com_qvr_test_JNI_nativeStoreContext(JNIEnv *env, jobject thiz, jobject context) {
    //代码参看上一篇博文
}

//Init QVRService Start
JNIEXPORT void JNICALL
Java_com_qvr_test_JNI_nativeInitQxrService(JNIEnv *env, jobject thiz) {
    //代码参看上一篇博文
}
//Init QVRService End

string getDateTime() {  //24H data format
    //代码参看上一篇博文
}

//GetPose Start
void *savePoseThread(void *arg) {
    //代码参看上一篇博文
}

JNIEXPORT void JNICALL
Java_com_qvr_test_JNI_nativeStartSavePose(JNIEnv *env, jobject obj) {
    //代码参看上一篇博文
}

JNIEXPORT void JNICALL
Java_com_qvr_test_JNI_nativeStopSavePose(JNIEnv *env, jobject obj) {
    //代码参看上一篇博文
}
//GetPose End

//GetImu Start
void *saveImuThread(void *arg) {
    //代码参看上一篇博文
}

JNIEXPORT void JNICALL
Java_com_qvr_test_JNI_nativeStartGetIMU(JNIEnv *env, jobject obj) {
    //代码参看上一篇博文
}

JNIEXPORT void JNICALL
Java_com_qvr_test_JNI_nativeStopGetIMU(JNIEnv *env, jobject obj) {
    //代码参看上一篇博文
}
//GetImu End

//创建用于保存图像的文件夹
void createFolder(string folderPath) {
    string command;
    command = "mkdir -p " + folderPath;
    system(command.c_str());
}

//Get Bottom Frame Start
void *saveBottomFrameThread(void *arg) {
    int ret = QVR_ERROR;

    createFolder(folderBottomPath);

    //QVRCameraClient handler
    qvrcamera_client_helper_t *qvrcamera_client = NULL;
    qvrcamera_client = QXRCamClient_Create(jni_android_info.vm,(jobject) jni_android_info.context);
    if (qvrcamera_client == NULL) {
        LOGW("Fail to create qvrcamera_client!");
        return NULL;
    }

    //camera name MUST match with qvrservice_config.txt,
    //if not find the camera define name in qvrservice_config.txt, please find trinity.txt under /vendor/etc
    qxrcamdevice_t *qvrcamera_device = NULL;
    qvrcamera_device = QXRCamClient_AttachCamera(qvrcamera_client, QVRSERVICE_CAMERA_NAME_TRACKING); //"uptracking"; "ctrl-tracking"
    if (qvrcamera_device == NULL) {
        LOGW("Fail to Attach qvrcamera_device!");
        return NULL;
    }

    ret = QVRCameraDevice_Start(qvrcamera_device);
    if (QVR_CAM_SUCCESS != ret) {
        LOGW("Fail to start camera device");
        return NULL;
    }

    sleep(1);

    int32_t old_frame_num = 0;
    int32_t latest_frame_num = 0;
    mIsStopGetBottomFrame = false;

    //循环抓取图像,当mIsStopGetBottomFrame为true时结束循环,mIsStopGetBottomFrame值的改变由java层调度
    while(!mIsStopGetBottomFrame){
        ret = QVRCameraDevice_GetCurrentFrameNumber(qvrcamera_device, &latest_frame_num);
        if (ret != QVR_CAM_SUCCESS) {
            LOGW("Failed to get qvr current frame number !");
            return NULL;
        }else{
            LOGW("success get old_frame_num:%d, latest_frame_num:%d", old_frame_num, latest_frame_num);
        }

        //判断frame number,frame num不一致才进行图像保存
        if (old_frame_num == latest_frame_num) {
            continue;
        } else {
            old_frame_num = latest_frame_num;
        }

        qvrcamera_frame_t qvr_frame;
        ret = QVRCameraDevice_GetFrame(qvrcamera_device,
                                       &latest_frame_num,
                                       QVRCAMERA_MODE_NON_BLOCKING,
                                       QVRCAMERA_MODE_NEWER_IF_AVAILABLE,
                                       &qvr_frame);
        if (ret != QVR_CAM_SUCCESS) {
            LOGW("Failed to get qvr frame !");
            return NULL;
        }

        writeBufferToFile(qvr_frame,true);

        ret = QVRCameraDevice_ReleaseFrame(qvrcamera_device, qvr_frame.fn);
        if (ret != QVR_CAM_SUCCESS) {
            LOGW("Failed to release qvr frame !");
            return NULL;
        }

        usleep(16000);
    }

    ret = QVRCameraDevice_Stop(qvrcamera_device);
    if (ret != QVR_CAM_SUCCESS) {
        LOGW("Failed to stop camera device !");
        return NULL;
    }

    if (qvrcamera_device) {
        LOGW("Detach qvrcamera_device !");
        QVRCameraDevice_DetachCamera(qvrcamera_device);
    }

    if (qvrcamera_client) {
        LOGW("Destroy qvrcamera_client !");
        QVRCameraClient_Destroy(qvrcamera_client);
    }

    pthread_exit(NULL);
    return NULL;
}

JNIEXPORT void  JNICALL
Java_com_qvr_test_JNI_nativeStartGetBottomFrame(JNIEnv *env, jobject obj) {
    //创建用于保存底部SLAM Camera图像的线程
    pthread_t myThread;
    int res = pthread_create(&myThread, NULL, saveBottomFrameThread, NULL);
    if (res != 0) {
        LOGW("myThread create failed!");
        return;
    }
}

JNIEXPORT void JNICALL
Java_com_qvr_test_JNI_nativeStopGetBottomFrame(JNIEnv *env, jobject obj) {
    LOGW("nativeStopGetBottomFrame()    mIsStopGetBottomFrame:%d", mIsStopGetBottomFrame);
    //结束抓取底部SLAM Camera图像的while()循环,由Java层调度
    mIsStopGetBottomFrame = true;
}
//Get Bottom Frame End

///Get Up Frame Start
void *saveUpFrameThread(void *arg) {
    int ret = QVR_ERROR;

    createFolder(folderUpPath);

    //QVRCameraClient handler
    qvrcamera_client_helper_t *qvrcamera_client = NULL;
    qvrcamera_client = QXRCamClient_Create(jni_android_info.vm,(jobject)jni_android_info.context);
    if (qvrcamera_client == NULL) {
        LOGW("Fail to create qvrcamera_client!");
        return NULL;
    }

    //camera name MUST match with qvrservice_config.txt,
    //if not find the camera define name in qvrservice_config.txt, please find trinity.txt
    qxrcamdevice_t *qvrcamera_device = NULL;
    qvrcamera_device = QXRCamClient_AttachCamera(qvrcamera_client, "ctrl-tracking"); //"uptracking"; "ctrl-tracking"
    if (qvrcamera_device == NULL) {
        LOGW("Fail to Attach qvrcamera_device!");
        return NULL;
    }

    ret = QVRCameraDevice_Start(qvrcamera_device);
    if (QVR_CAM_SUCCESS != ret) {
        LOGW("Fail to start camera device");
        return NULL;
    }

    sleep(1);

    int32_t old_frame_num = 0;
    int32_t latest_frame_num = 0;
    mIsStopGetUpFrame = false;
    while(!mIsStopGetUpFrame){
        ret = QVRCameraDevice_GetCurrentFrameNumber(qvrcamera_device, &latest_frame_num);
        if (ret != QVR_CAM_SUCCESS) {
            LOGW("Failed to get qvr current frame number !");
            return NULL;
        }else{
            LOGW("success get old_frame_num:%d, latest_frame_num:%d", old_frame_num, latest_frame_num);
        }

        if (old_frame_num == latest_frame_num) {
            continue;
        } else {
            old_frame_num = latest_frame_num;
        }

        qvrcamera_frame_t qvr_frame;
        ret = QVRCameraDevice_GetFrame(qvrcamera_device,
                                       &latest_frame_num,
                                       QVRCAMERA_MODE_NON_BLOCKING,
                                       QVRCAMERA_MODE_NEWER_IF_AVAILABLE,
                                       &qvr_frame);
        if (ret != QVR_CAM_SUCCESS) {
            LOGW("Failed to get qvr frame !");
            return NULL;
        }

        writeBufferToFile(qvr_frame,false);

        ret = QVRCameraDevice_ReleaseFrame(qvrcamera_device, qvr_frame.fn);
        if (ret != QVR_CAM_SUCCESS) {
            LOGW("Failed to release qvr frame !");
            return NULL;
        }

        usleep(16000);
    }

    ret = QVRCameraDevice_Stop(qvrcamera_device);
    if (ret != QVR_CAM_SUCCESS) {
        LOGW("Failed to stop camera device !");
        return NULL;
    }

    if (qvrcamera_device) {
        LOGW("Detach qvrcamera_device !");
        QVRCameraDevice_DetachCamera(qvrcamera_device);
    }

    if (qvrcamera_client) {
        LOGW("Destroy qvrcamera_client !");
        QVRCameraClient_Destroy(qvrcamera_client);
    }

    pthread_exit(NULL);
    return NULL;
}

JNIEXPORT void  JNICALL
Java_com_qvr_test_JNI_nativeStartGetUpFrame(JNIEnv *env, jobject obj/*, jstring str*/) {
    pthread_t myThread;
    int res = pthread_create(&myThread, NULL, saveUpFrameThread, NULL);
    if (res != 0) {
        LOGW("saveUpFrameThread create failed!");
        return;
    }
}

JNIEXPORT void JNICALL
Java_com_qvr_test_JNI_nativeStopGetUpFrame(JNIEnv *env, jobject obj) {
    LOGW("nativeStopGetUpFrame()    mIsStopGetUpFrame:%d", mIsStopGetUpFrame);
    mIsStopGetUpFrame = true;
}
//Get Up Frame End

//保存图像的buffer成文件
void writeBufferToFile(qvrcamera_frame_t qvr_frame, bool isBottom) {
    LOGW("qvr_frame fn: %d,  qvr_frame.start_of_exposure_ts:%d, qvr_frame.exposure:%d, qvr_frame.width: %d, qvr_frame.height:%d, qvr_frame.format:%d",
         qvr_frame.fn, qvr_frame.start_of_exposure_ts, qvr_frame.exposure, qvr_frame.width, qvr_frame.height, qvr_frame.format);

    //文件名:时间戳 + Size + 格式
    string ts = to_string(qvr_frame.start_of_exposure_ts);
    string frameSize = "_1280x480";
    /*SLAM Camera输出的黑白图像是Y8格式纯灰度图,从QXRService中获取到的qvr_frame.format也是y8 
    * 但是保存的图像文件我们自己将其格式写为".Gray",原因后面会有详细讲解*/
    string format = ".Gray";//to_string(qvr_frame.format);

    char file[120];

    string folderPath;
    if (isBottom) {
        folderPath = folderBottomPath + ts + frameSize + format;
    } else {
        folderPath = folderUpPath + ts + frameSize + format;
    }

    strcpy(file,folderPath.c_str());

    FILE *fp = fopen(file, "wb+");
    if (!fp) {
        LOGW("OPEN FILE ERROR");
        return;
    }
    //直接将buffer数据按字节写入文件即可
    fwrite((void *) qvr_frame.buffer, qvr_frame.width * qvr_frame.height, 1, fp);
    fclose(fp);
}

}
#endif

到此,对SLAM Camera图像的抓取和保存就开发完成了。

现在基于QXRService Api我们已经可以成功地对SLAM Camera图像进行抓取和保存。

接下来我们就来验证抓取到的图像

六.验证

接下来就开始我们的验证过程

1.安装apk

将编译出来的apk使用adb install命令安装到Native版本上,然后打开qvrtest应用。

注意由于我们为了简洁,并没有在Java代码中去申请Camera和File Storage权限,所以初次打开应用不会弹窗申请权限,在初次打开qvrtest应用之前,手动在App Info中开启Camera和File Storage权限就行了。

打开后就可以看到新增的 Start-GetFrameUp 和 Start-GetFrameBottom 两个Button:

2.抓取图像

分别点击 Start-GetFrameUp 和 Start-GetFrameBottom 两个Button,JNI抓取图像的两个线程就会启动,同时两个按钮切换为 Stop-GetFrameUp 和 Stop-GetFrameBottom状态

如果再次点击的话,就会触发JNI层的 mIsStopGetUpFrame和mIsStopGetBottomFrame 两个bool变量为true,从而跳出线程中图像抓取的while()循环。

3.Pull数据

图像抓取完成后,执行adb pull命令,将/data/data/com.qvr.test 中的数据pull到本地:
adb pull /data/data/com.qvr.test D:\temp

可以看到除了上一讲中我们抓取的头显Pose和IMU数据外,又多了camera8和camera9两个文件夹,这两个文件夹中存储的就是我们抓取到的图像

 

4. 查看图像文件目录

分别打开camera8和camera9文件夹,就能看到我们抓取到的图像:
camera8:对应Bottom SLAM Camera图像
camera9:对应Up SLAM Camera图像
图像名:时间戳+size+格式

图像文件格式是我们自己写的".Gray"

前文中提到过,SLAM Camera一般使用的是黑白摄像头,抓取到图像是Y8纯灰度图,从QXRService中获取到的Frame format也是"y8",但是我们在保存文件的时候,并没有将文件格式写成"y8"而是定义成".Gray",这是为了方便我们使用某些YUV看图软件时,能够直接通过文件名和后缀格式对图片进行解析。

camera8:

 

camera9: 

 

5.查看图像: 

我们使用一款名为YUVView的工具来查看抓取的图像

先看camera8目录的前两张图像,分别是底部SLAM Camera一张长曝和一张短曝图像:

再看camera9目录的前两张图像,也分别是顶部SLAM Camera一张长曝和一张短曝图像:

查看其他图片,可以看到长曝和短曝图像是交错的,这是SLAM Camera长短曝交替其中一种模式,可以是 "长曝-短曝-长曝-短曝...." 也可以是 "长曝-短曝-短曝-长曝-短曝-短曝-....",根据需求可以自行设置,例如有手势手柄切换时就需要用到后一种长短曝模式。

七.结束

至此,对SLAM Camera图像抓取和保存的开发工作就以结束。

综合上一讲和这一讲的内容

1.基于QXRService的自研QVRDatalogger工具就已初步开发完成,我们可以自由地获取到高通QVRDatalogger工具所能获取到的所有数据。

2.使用HIDL、AIDL等进程间通信机制,也可以将获取到的数据作为Input Data接入到自研的手柄,手势等算法,在拿到手柄、手势输出的Pose后,再结合高通的snapdragon-openxr-input-plugin.aar插件,就可以打通整个自研手柄,手势的数据链路。

这两讲内容有点多,希望对大家在高通平台上的XR开发起到一些帮助。

猜你喜欢

转载自blog.csdn.net/geyichongchujianghu/article/details/127770614