上一篇博文讲解了基于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开发起到一些帮助。