文章目录
使用Android相机的使用我们可以在拍摄的时候拿到当前画面,以及当前画面对应的深度信息,本文主要介绍自己获取预览图以及深度信息过程中的一些用到的知识点,欢迎大家留言讨论
基础准备
获取深度的时候需要深度摄像头的支持,我们需要查阅ARCore AREngine官方的设备支持列表,寻找带TOF获取深度硬件支持的设备,当然更多的设备是没有深度摄像头支持的,这样的设备上我们通过ARCore的depthAPI我们获取到的图片和深度图片不匹配,因为设备没有硬件支持的话,即使调用的是full depth api,最终获取到的也是raw depthapi经过采样的数据,原始图和深度图是不成比例的,自己测试过程中发现华为手机不带深度摄像头根本拿不到深度信息,ARCore可以拿到一个depth from motion的深度图。
下面我们分别介绍如何使用ARCore和ArEngine的API去获取预览图和深度信息,主要介绍C的API,C和Java接口一样,arcore可ArEngine都提供了demo,可以在demo中直接编编写代码,获取相应的数据进行测试:
ARCore hello_ar_c demo:
AREngine ndk-demo:
深度图 ARCore
ArStatus ArFrame_acquireRawDepthImage16Bits(
const ArSession *session,
const ArFrame *frame,
ArImage **out_depth_image
)
上面的返回以后,我们需要从out_depth_image中取出数据,从out_depth_image获取数据我们需要调用下面的方法:
// 获取平面数据
ArImage_getPlaneData(const ArSession *session, const ArImage *image, int32_t plane_index, const uint8_t **out_data, int32_t *out_data_length
通过上面的接口返回的是一个D_16格式的数据,该数据是一个little-endian的16位深度数据,我们需要将他转换为一个整数然后使用。由于ARCore对于没有深度深摄像头头的手机获取深度的时候是相对深度,只有手机移动的时候我们才能获取到深度。测试的时候我们需要移动手机才可以。
D_16数据转为int:,out_data为平面数据
for(int i=0;i<out_data_length;i+=2){
uint8_t low_bit = (*out_data)&0xFF);
out_data++;
uint16_t result = low_bit | (((*out_data)&0xFF)<<8);
out_data++; // 这里也要++
}
小端存储模式:指数据的低位储存在内存的底地址中,数据的高位储存在内存的高地址中
上面我们进行小端存储的数据格式转换,更多信息可以参考ARCore官方-获取Raw-depth
AREngine
AREngine的接口设计和ARCore类似,只不过接口名称会变化我们在获取深度的时候调用的是:
HwArFrame_acquireDepthImage(HwArSession *session, HwArFrame *frame, HwArImage **outImage)
通过上面的acquireDepthImage我们得到输出数据是在一个中的HwArImage,通过这个对象我们如何获取到平面数据呢?这个和google的ArImage中的接口并没有对齐
HwArImage_getNdkImage(const HwArImage *image, const AImage **outNdkImage)
华为提供了一个上面的接口,这个接口可以将acquireDepth得到的额HwArImage转换为 AImage,这个AImage是什么呢?查看Android的Media库中接口,android develop AImage
AImage_getPlaneData(const AImage *image, int planeIdx, uint8_t **data, int *dataLength)
AImage_getHeight(const AImage *image, int32_t *height)
AImage_getWidth(const AImage *image, int32_t *width)
...
AImage提供了很多和ArImage类似的接口,通过这个接口我们可以获取到图片中的数据,那么如何在代码中调用这个接口呢?上面的接口在Android的ndk库中,我们在我们的代码中引入该库
// 在我们的代码中引入media库提供的头文件
#include "media/NdkImage.h"
// 调用AImage提供的接口
AImage_getPlaneData(
// 编译的时候需要在cmake中link一下so
target_link_libraries(XXXX mediandk)
通过上面的操作有可能调用不到AImage_接口,查看NdkImage.h头文件中定义的源码:
#if __ANDROID_API__ >= 24
#if __ANDROID_API__ >= 26
我们需要的API被这个if判断包裹着,当Android API大于24、26的时候才能使用
因此我们需要修改我们cmake中携带的指定平台版本的编译参数为-DANDROID_PLATFORM=android-26,这样我们就可以正常使用AImage_XXX接口获取数据了。
预览图 ARCore&AREngine
ARCore和AREngine获取预览图的接口比较类似,只是AREngine的名称前面会多一个HW,下面主要介绍ARCore的API
ArFrame_acquireCameraImage
ArStatus ArFrame_acquireCameraImage(ArSession *session,
ArFrame *frame,
ArImage **out_image);
acquireCameraImage我们获取到ArImage,ArImage提供的一些接口_getWidth,_getHeight,_getFormat 我们可以获取到图片的一些基础信息,而ArImage的图片数据是存储在平面中的,我们需要通过获取平面数据才能拿到图片图像unity摄像机存图片,
void ArImage_getPlaneData(const ArSession *session,
const ArImage *image,
int32_t plane_index,
const uint8_t **out_data,
int32_t *out_data_length);
平面的数量和平面中数据的存储格式与获取到的图像格式有着密切的关系,只有获取到确定的格式我们才能解析平面数据。
Android侧获取到的ArImage是YUV420_888格式,如何将这个格式的图片转为RGB格式可以参考这个博主的文章YUV_420_888 to RGBunity摄像机存图片,进行格式转换前2d游戏素材,还是需要对YUV格式有一定的了解,不然看运算会很蒙蔽YUV实践.
解析了平面数据我们会得到NV21/NV12格式的data,然后我们只需要将图片格式转为RGB就可以,同时获取到的图片和实际屏幕方向会有90度的差异,我们也需要最后将图片转转90度。
AREngine获取预览图的时得到的是HwArImage,HwArImage中并没有上面getPlaneData这种类似接口,参考上文深度图获取的时候HwArImage转换为 AImage的方式,也可以获取到需要的数据。
其它 获取手机型号
在进行深度数据获取的时候,我们通常会获取手机型号,进行机型之间的区分,Android中我们可以通过android.os.Build.去获取厂商,设备型号等等,但是如果想要获取其他ro属性(adb shell getprop 可以查看)可以使用Android framework中自带的SystemProperties.java类去获取,通过这个类,我们可以拿到品牌的market name 市场营销名称,这样我们会很好根据设备区分配置。
#在vivo手机上,我们可以通过下面的属性获取设备市场营销名称
ro.vivo.market.name=iQOO 7
不同厂商这个配置属性名称会不同,如果要读取多厂商的可能需要进行兼容。
保存图片调试
在进行图像调试的时候,我们经常需要保存图像查看,可以使用下面的函数:
stbi_write_png
函数用法参考stackoverflow,
stbi_write_png(outputPath.c_str(), width, height, channels_num, odata, channels_num*width);
outputPath表示存储路径,例如:/data/data/com.example.test/cache/test.png
channels_num表示通道数:1表示灰度图(8位),3表示红绿蓝RGB三色图(24位),4表示RGBA图片(32位)
最后一个传入的是channels_num*width表示rowstride,即每一行我们占用的字节数。
坐标映射转换
比如:
acquireCameraImage获取到直接是GPU中的图像,一般是640*480比例的图片,图片比例是4:3,但是我们手机实际屏幕比例可能不是这样,经过对比之后发现,上屏之后显示的图像范围会比GPU图像的范围窄,获取的的GPU中的预览图和实际图像之间的坐标如何换算呢?
_transformDisplayUvCoords
ARCore和ArEngine提供了上述类似的接口,我们可以进行坐标转换游戏素材,但是无奈本人opengl知识太欠缺了,不知道如何调用,但是从github的一个老哥的提问中
issue 225 找到了解决办法:
主要在这里的:
private void updateTextureCoordinates(
...
// Crop the CPU image to fit the screen aspect ratio.
// 重点是这块的逻辑
float imageAspectRatio = (float) (imageWidth) / imageHeight;
float croppedWidth = 0.f;
float croppedHeight = 0.f;
if (screenAspectRatio < imageAspectRatio) {
croppedWidth = imageHeight * screenAspectRatio;
croppedHeight = imageHeight;
} else {
croppedWidth = imageWidth;
croppedHeight = imageWidth / screenAspectRatio;
}
float u = (imageWidth - croppedWidth) / imageWidth / 2.f;
float v = (imageHeight - croppedHeight) / imageHeight / 2.f;
float[] texCoords;
switch (cameraToDisplayRotation) {
case Surface.ROTATION_90:
texCoords = new float[] {1 - u, 1 - v, u, 1 - v, 1 - u, v, u, v};
break;
case Surface.ROTATION_180:
texCoords = new float[] {1 - u, v, 1 - u, 1 - v, u, v, u, 1 - v};
break;
case Surface.ROTATION_270:
texCoords = new float[] {u, v, 1 - u, v, u, 1 - v, 1 - u, 1 - v};
break;
case Surface.ROTATION_0:
default:
texCoords = new float[] {u, 1 - v, u, v, 1 - u, 1 - v, 1 - u, v};
break;
}
// Save CPU image texture coordinates.
quadImgCoordTransformed.put(texCoords);
quadImgCoordTransformed.position(0);
// Update GPU image texture coordinates.
frame.transformDisplayUvCoords(quadTexCoord, quadTexCoordTransformed);
}
上面的代码是google arcore sdk中的代码,在计算上屏区域和原始图片区域之间关系的时候,先通过屏幕比和图片比确定是裁剪高度还是裁剪宽度,自己的场景是高度不变裁剪宽度,裁剪宽度的时候两边是等分的,我们可以获取裁剪后的图像宽度croppedWidth,这样我们的预览图的宽高和实际屏幕显示的宽高可以成比例,这样就可以进行坐标转换映射了。
绘制结果保存
如果你是使用opengl进行绘制的话,需要截取当前的屏幕图像,可以参考下面的方法:Android 中 GLSurfaceView 截图,自己使用的是方法一,因为方法二获取到的图像自己测试得到的图片是个反的。
参考链接:
AREngineNDK开发介绍
Camera2API获取深度图并可视化
AREngineJavaAPI获取深度
YUV420888格式介绍
YUV420转NV21
ARCore get depth and confidence
tensorflow yuv 2 rgb
camera api get focal length
camera 2 api get depth demo
Get cx cy fx fy
文章来源:https://blog.csdn.net/liu_12345_liu/article/details/129806956