前言:因为想在安卓设备上显示深度图的3D效果画面,经过查找资料,发现使用opengles比较方便。本文基于opengles在安卓设备实现3D点云效果图显示,而且深度图上点的颜色由近及远,从红-黄-绿-蓝渐变,有点类似matlab的点云图。
一、字节数组工具类:BufferUtil.java
// package
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
public class BufferUtil {
//将int[]数组转换为OpenGLES所需的IntBuffer
public static IntBuffer intBufferUtil(int[] arr)
{
IntBuffer buffer;
// 初始化ByteBuffer,长度为arr数组的长度*4,因为一个int占4字节
ByteBuffer bb = ByteBuffer.allocateDirect(arr.length * 4);
bb.order(ByteOrder.nativeOrder()); //数组排列用nativeOrder
buffer = bb.asIntBuffer();
buffer.put(arr);
buffer.position(0);
return buffer;
}
//将float[]数组转换为OpenGLES所需的FloatBuffer
public static FloatBuffer floatBufferUtil(float[] arr)
{
FloatBuffer buffer;
//初始化ByteBuffer,长度为arr数组的长度*4,因为一个float占4字节
ByteBuffer bb = ByteBuffer.allocateDirect(arr.length * 4);
bb.order(ByteOrder.nativeOrder());
buffer = bb.asFloatBuffer();
buffer.put(arr);
buffer.position(0);
return buffer;
}
/**
* 读取到字节数组1
*
* @param file
* @return
* @throws IOException
*/
public static byte[] toByteArray(File file) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
BufferedInputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(file));
int buf_size = 1024;
byte[] buffer = new byte[buf_size];
int len = 0;
while (-1 != (len = in.read(buffer, 0, buf_size))) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bos != null) {
bos.close();
}
}
}
/**
* 大端、小端方式,两字节数据转short值(有符号)
**/
public static short getShort(byte[] b, boolean isBigEndian) {
if (b == null || b.length <= 0 || b.length > 2) {
return 0;
}
if (isBigEndian) {
return (short) (((b[1] << 8) | b[0] & 0xff));
} else {
return (short) (((b[1] & 0xff) | b[0] << 8));
}
}
}
二、主要类,深度数据渲染类:DepthImageRender.java
// package
import android.opengl.GLSurfaceView;
import android.util.Log;
//import xxx.BufferUtil;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class DepthImageRender implements GLSurfaceView.Renderer {
// 最大深度
public static float depthMax = 0;
// 最小非0深度
public static float depthMin = 0;
//TODO 根据情况设置
public static final int imgWidth = 720;
//TODO 根据情况设置
public static final int imgHeight = 1280;
// 根据情况设置
//TODO x、y轴坐标的范围(0.0~1.0)*imgXYMagnification
public static final int imgXYMagnification = 8;
// 定义图像的顶点,形如(x,y,z,x,y,z...),长度为图像宽*高*3
private float[] imageVertices;
// 定义顶点的颜色,每个顶点的颜色长度为3,形如(r,g,b,a,r,g,b,a),长度为图像宽*高*4
private int[] imageColors;
// 定义Open GL ES绘制所需要的Buffer对象
FloatBuffer imageVerticesBuffer;
IntBuffer imageColorsBuffer;
private boolean isAntiClockDir = true;
// 图像旋转角度
private float rotate = 0.0f;
// 渐变色数组的长度
private final int gradientColorsLength = 256*4;
// 红--黄--绿--蓝渐变色中的红色分量
private int[] gradientColorsR = new int[gradientColorsLength];
// 红--黄--绿--蓝渐变色中的绿色分量
private int[] gradientColorsG = new int[gradientColorsLength];
// 红--黄--绿--蓝渐变色中的蓝色分量
private int[] gradientColorsB = new int[gradientColorsLength];
// 初始化时分4阶段,各阶段颜色值变化
// 红色分量从65535-65535-->65535-0-->0-0-->0-0
// 绿色分量从0-65535-->65535-65535-->65535-65535-->65535-0
// 蓝色分量从0-0-->0-0-->0-65535-->65535-65535
// 分量起始值和变化趋势(0:不变,1:上升,2:下降)
private int[][] rgbWeightStart = new int[][]{{65535, 65535, 0, 0}, {0, 65535, 65535, 65535}, {0, 0, 0, 65535}};
private int[][] rgbTrend = new int[][]{{0, 2, 0, 0}, {1, 0, 0, 2}, {0, 0, 1, 0}};
// 颜色渐变跨度差
private final int rgbTrendGap = 255;
// TODO 也可设置成 GL10.GL_LINE_STRIP,效果会有所区别
public static int depthDrawTypeDefault = GL10.GL_POINTS;
public static int depthDrawType = depthDrawTypeDefault;
public DepthImageRender() {
generateGradientColors();
}
public void initImgVertices(byte[] rawFileBytes) {
this.imageVertices = getImageDepthVertices(rawFileBytes, imgWidth, imgHeight);
imageColors = new int[imageVertices.length * 4 / 3];
int pixelIndex = 0;
// 根据情况设置
// TODO z轴放大倍数(深度值范围:(0.0~1.0)*depthMagnify),倍数大,立体感强
int depthMagnify = 4;
float depthGap = depthMax - depthMin;
Log.i("TAG", "depthMax=" + depthMax + ", depthMin=" + depthMin);
if (depthGap <= 0.0f) {
depthGap = 1.0f;
}
for (int i = 0; i < imageColors.length; i += 4) {
// 直接从数据读取的深度值
float rawDepth = imageVertices[2 + pixelIndex * 3];
// 转化后的z轴的值
float verticesZDepth;
if (rawDepth > 0.0f) {
//做个转换,深度值小的,z轴数据更大。深度大的,z轴数据更小。在坐标系中才能显示正常立体图
verticesZDepth = (1.0f - (((float) (rawDepth - depthMin)) / depthGap)) * depthMagnify;
//重新赋值
imageVertices[2 + pixelIndex * 3] = verticesZDepth;
}
// Log.e("TAG", "depth222=" + verticesZDepth);
int colorR = 0x0, colorG = 0x0, colorB = 0x0, colorA = 0x0;
int[] colors = getRGBAByDepth(rawDepth, depthMin, depthMax);
if (colors != null && colors.length > 3) {
colorR = colors[0];
colorG = colors[1];
colorB = colors[2];
colorA = colors[3];
}
imageColors[i] = colorR;
imageColors[i + 1] = colorG;
imageColors[i + 2] = colorB;
imageColors[i + 3] = colorA;
pixelIndex++;
}
// 将立方体的顶点位置数据数组包装成FloatBuffer
imageVerticesBuffer = BufferUtil.floatBufferUtil(imageVertices);
imageColorsBuffer = BufferUtil.intBufferUtil(imageColors);
}
private void generateGradientColors() {
// 共4阶段,各阶段颜色值变化
//红色分量从65535-65535-->65535-0-->0-0-->0-0
//绿色分量从0-65535-->65535-65535-->65535-65535-->65535-0
//蓝色分量从0-0-->0-0-->0-65535-->65535-65535
for (int stage = 0; stage < 4; stage++) {
int rWS = rgbWeightStart[0][stage];
int rT = rgbTrend[0][stage];
int gWS = rgbWeightStart[1][stage];
int gT = rgbTrend[1][stage];
int bWS = rgbWeightStart[2][stage];
int bT = rgbTrend[2][stage];
for (int color = 0; color < 256; color++) {
if (rT == 1) {
rWS += rgbTrendGap;
} else if (rT == 2) {
rWS -= rgbTrendGap;
}
gradientColorsR[stage * 256 + color] = rWS;
if (gT == 1) {
gWS += rgbTrendGap;
} else if (gT == 2) {
gWS -= rgbTrendGap;
}
gradientColorsG[stage * 256 + color] = gWS;
if (bT == 1) {
bWS += rgbTrendGap;
} else if (bT == 2) {
bWS -= rgbTrendGap;
}
gradientColorsB[stage * 256 + color] = bWS;
}
}
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glDisable(GL10.GL_DITHER);//关闭防抖
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT//设置透视修正
, GL10.GL_FASTEST);
gl.glClearColor(0, 0, 0, 0);
gl.glShadeModel(GL10.GL_SMOOTH);// 设置为阴影平滑模式
gl.glEnable(GL10.GL_DEPTH_TEST);// 启用深度测试
gl.glDepthFunc(GL10.GL_LEQUAL);// 设置深度测试的类型
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height); //设置3D视窗的大小及位置
gl.glMatrixMode(GL10.GL_PROJECTION); //设置矩阵模式设为投影矩阵
gl.glLoadIdentity(); //初始化单位矩阵
float ratio = (float) width / height; //计算视窗宽高比
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);//设置视窗空间大小
}
@Override
public void onDrawFrame(GL10 gl) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);//启用vertex shader 数据
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);//启用fragment shader数据
gl.glMatrixMode(GL10.GL_MODELVIEW);//设置当前矩阵堆栈为模型堆栈
gl.glLoadIdentity();
// 跟图像缩放有关
gl.glTranslatef(0.0f, 0.0f, -5.0f);
// 沿着(0,1,0)向量为轴的方向旋转
//TODO 旋转相关,若不旋转,则第一个参数设置成0.0f
gl.glRotatef(rotate/*0.0f*/, 0.0f, 1.0f, 0.0f);
// 设置顶点的位置数据
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, imageVerticesBuffer);
// 设置顶点的颜色数据
gl.glColorPointer(4, GL10.GL_FIXED, 0, imageColorsBuffer);
// 第一个参数如果是 GL10.GL_LINE_STRIP 则点会相互连接(柱状图像),GL10.GL_POINTS 只画点
gl.glDrawArrays(depthDrawType, 0, imageVerticesBuffer.capacity() / 3);
gl.glFinish();//绘制结束
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
if (isAntiClockDir) {
if (rotate < 78.0f)
rotate += 2.0f;
if (rotate >= 78.0f) {
isAntiClockDir = false;
}
} else {
if (rotate >= 2.0f)
rotate -= 2.0f;
if (rotate <= 0.0f) {
isAntiClockDir = true;
}
}
}
private int[] getRGBAByDepth(float rawDepth, float minDepth, float maxDepth) {
// rgba color
int r = 0x0, g = 0x0, b = 0x0, a = 0x0;
if (rawDepth <= 0.0f || minDepth <= 0.0f || maxDepth <= 0.0f) {
return new int[] {r, g, b, a};
}
// 深度分层的层数
int depthLayerNum = (int) (maxDepth - minDepth);
// 当前深度所在的层
int curDepthRelativeLayer = (int) (rawDepth - minDepth);
// 渐变色每一层的索引跨度
int gradientColorsLayerGap = gradientColorsLength / depthLayerNum;
if (gradientColorsLayerGap * curDepthRelativeLayer >= 0
&& gradientColorsLayerGap * curDepthRelativeLayer < gradientColorsLength) {
r = gradientColorsR[gradientColorsLayerGap * curDepthRelativeLayer];
g = gradientColorsG[gradientColorsLayerGap * curDepthRelativeLayer];
b = gradientColorsB[gradientColorsLayerGap * curDepthRelativeLayer];
}
return new int[] {r, g, b, a};
}
/**
* @param personBytes 原图的字节数组,每个像素16位存储
* @param w 图像宽度
* @param h 图像高度
* */
public float[] getImageDepthVertices(byte[] personBytes, int w, int h) {
// 每个像素占3个长度
float[] vertices = null;
if (personBytes != null && personBytes.length > 0) {
vertices = new float[personBytes.length * 3 / 2];
// Log.i("tag", "personBytes length=" + personBytes.length);
// Log.i("tag", "vertices length=" + vertices.length);
int verticesIndex = 0;
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
// opengl坐标,X轴向右,右手定则,Y向上,Z向外
// x轴的值要进行缩小
vertices[verticesIndex] = (float) (((float) col * imgXYMagnification / w) - imgXYMagnification / 2);
// y轴的值要进行缩小
vertices[verticesIndex + 1] = (float) ((-((float) row * imgXYMagnification / h)) + imgXYMagnification / 2);
int firsBytesIndex = ((w * row) + col) * 2;
// 根据情况设置
// 根据深度存储方式去读取深度值,本例为16位小端存储的读取方式
float depth = (float) ((personBytes[firsBytesIndex] & 0xFF)
| ((personBytes[firsBytesIndex + 1] << 8) & 0xFF00));
// 获取深度最大、最小值(非0)
if (depth != 0) {
if (depthMin == 0) {
depthMin = depth;
} else if (depth < depthMin) {
depthMin = depth;
}
}
if (depth > depthMax) {
depthMax = depth;
}
vertices[verticesIndex + 2] = depth;
verticesIndex += 3;
}
}
}
return vertices;
}
}
三、显示点云图或3D效果图的活动类: Depth3DViewerActivity.java
// package
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
//import xxx.DepthImageRender;
//import xxx.BufferUtil;
import java.io.File;
import java.io.IOException;
public class Depth3DViewerActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//TODO 必须有读取SD卡权限
GLSurfaceView glSurfaceView = new GLSurfaceView(this);
setContentView(glSurfaceView);
byte[] rawFileBytes = readPersonRawFile();
if (rawFileBytes != null) {
DepthImageRender myRender = new DepthImageRender();
myRender.initImgVertices(rawFileBytes);
glSurfaceView.setRenderer(myRender);
} else {
Log.e("Depth3DViewerActivity", "空数据!");
finish();
}
}
/**从深度图原图读取到字节数组,深度图为16位小端数据,深度数据在imagej中打开时,深度值小的越靠近相机,深度值大的远离相机**/
public byte[] readPersonRawFile() {
String filePath = "sdcard/depth_raw.raw";
try {
return BufferUtil.toByteArray(new File(filePath));
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e1) {
e1.printStackTrace();
}
return null;
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
效果图如下:
安卓源码见我的gitee仓库(深度文件在工程根目录,放到sdcard根目录进行测试,设备要支持gpu):Depth3DViewer: 使用opengles,在安卓设备上显示深度图的3D效果。深度图文件在工程根目录,使用时放在sdcard根目录。 (gitee.com)