上一篇博文讲解了基于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"><Buttonandroid: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" /><Buttonandroid: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--><Buttonandroid: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--><Buttonandroid: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;//添加两个用于抓取图像的Buttonprivate 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();@Overrideprotected 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();}@Overridepublic void onResume() {super.onResume();}@Overridepublic void onPause() {super.onPause();}@Overridepublic 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);}@Overridepublic 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 PosemJni.nativeStartSavePose();} else {btnGetPose.setText("Start-GetPose");mStartGetPose = false;//调用jni api,结束获取QXRService输出的SLAM PosemJni.nativeStopSavePose();}}break;case R.id.btn_get_imu: {if (!mStartGetIMU) {btnGetIMU.setText("Stop-GetIMU");mStartGetIMU = true;//调用jni api,开始获取QXRService输出的IMU datamJni.nativeStartGetIMU();} else {btnGetIMU.setText("Start-GetIMU");mStartGetIMU = false;//调用jni api,结束获取QXRService输出的IMU datamJni.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 apipublic 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 Endstring 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 handlerqvrcamera_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/etcqxrcamdevice_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 handlerqvrcamera_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.txtqxrcamdevice_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开发起到一些帮助。

QXRService:基于高通QXRService获取SLAM Camera图像相关推荐

  1. Android O 的camera framework-hal层框架笔记(基于高通845平台)

    Android O 的camera framework/hal层框架笔记(基于高通845平台) tags: android camera 文章目录 Android O 的camera framewor ...

  2. Android O Treble框架笔记(基于高通845平台)

    Android O Treble框架笔记(基于高通845平台) tags: android 文章目录 Android O Treble框架笔记(基于高通845平台) @[toc] **0 前言** * ...

  3. 基于高通芯片组的广和通FM150 5G模组GPS测试演示

    关键词:高通  广和通  FM150  5G  GPS  GNSS 概述:基于高通芯片组的FM150模组已经内置了GNSS全球定位导航系统,默认情况下D/G天线用作分集功能,可以通过简单的AT命令打开 ...

  4. 04 基于高通8155的智能座舱功能安全设计

    前言 本篇图文跟大家分享一下基于高通8155的智能座舱仪表域功能安全设计. 全系内容可在<搞一下汽车电子>后台回复 "系列",或进入菜单栏 "分享平台&quo ...

  5. 基于高通SNPE推理引擎的yolov5目标检测算法

    上图源自Snapdragon Neural Processing Engine SDK Reference Guide,它展示了一个Deep Learning Neural Network在SNPE环 ...

  6. [转]QNX系统-基于高通骁龙SA8155平台,中科创达发布智能驾驶舱3.0解决方案

    如果你认为本系列文章对你有所帮助,请大家有钱的捧个钱场,点击此处赞助,赞助额0.1元起步,多少随意 声明:本文只用于个人学习交流,若不慎造成侵权,请及时联系我,立即予以改正 锋影 email:1741 ...

  7. 广和通5G模组成功联调基于高通IPQ系列+骁龙X62的5G CPE方案,已实现大规模量产

    随着移动终端与IoT物联网连接数日益增长,提供智能移动连接的5G CPE发挥越来越重要的作用.5G CPE能够将基站发送的5G信号转换成宽带Wi-Fi信号,提供更高的网络连接效率和更大的覆盖范围,为用 ...

  8. 基于高通骁龙X75和X72平台,移远通信5G R17模组为全球FWA和eMBB市场赋能

    日前,移远通信率先推出了符合3GPP Release 17标准的新一代工规级5G NR模组RG650E系列和RG650V系列. 相比前代5G产品,此次推出的5G R17模组在数据传输速率.网络容量.功 ...

  9. linux v4l2 示例程序,linux驱动由浅入深系列:camera驱动之二(基于高通平台的V4L2结构及代码分析)...

    在上一篇文章中介绍了camera的基础知识和相关概念,我们一起来了解一下驱动相关的代码结构.本文以高通+android平台为示例,首先看一下整体框图: 这张图是从整体上来看的 1,图中最下面的是ker ...

最新文章

  1. 图书管理系统(源码)
  2. ASP.NET运行环境配置
  3. FreeRTOS 之五 动态内存管理(heap_1.c)详解
  4. 1048:有一门课不及格的学生
  5. java for 循环执行顺序
  6. 事务的隔离级别(Transaction isolation levels)5
  7. HAproxy + keepalived 实现双机热备
  8. response和request
  9. 与组合数有关的一些求和公式
  10. “一沙一世界”的感悟
  11. 翻译: Swift 中的委托保留周期 如何在“纯”Swift(没有@objc)中进行弱协议引用
  12. 云计算世界里的企业级软件何去何从
  13. day4. jetson nx 确认 SPI 时钟
  14. M1 MacBook使用IE浏览器解决兼容问题
  15. readline library not found
  16. LAB颜色空间各通道的取值范围
  17. 行式 Excel 文件拆分
  18. 汇编:裴波那契数列前50项
  19. python 如果文件夹不存在 创建文件夹
  20. 日常用语--征求意见

热门文章

  1. cocos creator微信小游戏,使用微信云开发实现“世界排行榜”
  2. 7 款优秀 Markdown 编辑工具推荐
  3. 国密SM9算法C++实现之一:算法简介
  4. Python学习之道-烤机测试日志Log分析统计
  5. html 单元格被撑开_CSS教程:表格不被撑开的解决办法—css教程
  6. 2018千元内的UGP VR一体机开箱评测:ugp vr一体机怎么样真的好吗?
  7. 学会IDEA REST Client后,postman就可以丢掉了...
  8. RuntimeError: Address already in use 端口号冲突,解决思路总结
  9. android开发者 人数,Android开发者数量超iOS(转载)
  10. 电脑上的记账本,添加无限个账户记录