如何在ARCore中使用SurfaceView获取相机Pose
解决ARCore Pose类的OpenGL上下文错误及SurfaceView使用指南
核心问题原因
ARCore的Pose类依赖OpenGL上下文运行,而Android Studio默认相机教程用的PreviewView是基于View体系的,没有绑定ARCore所需的GL上下文。改用SurfaceView后,需配合ARCore的Session来完成相机预览、GL上下文绑定及Pose数据获取。
步骤1:替换布局为SurfaceView
修改你的布局文件(比如activity_main.xml),用SurfaceView替代原来的PreviewView,同时添加拍照按钮:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <SurfaceView android:id="@+id/surface_view" android:layout_width="match_parent" android:layout_height="match_parent" /> <Button android:id="@+id/capture_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:layout_marginBottom="32dp" android:text="拍照" /> </FrameLayout>
步骤2:初始化ARCore Session与SurfaceView回调
在Activity中完成ARCore会话初始化、SurfaceView绑定,确保GL上下文正确创建:
public class CameraPoseActivity extends AppCompatActivity { private Session arSession; private SurfaceView surfaceView; private Button captureBtn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); surfaceView = findViewById(R.id.surface_view); captureBtn = findViewById(R.id.capture_btn); // 初始化ARCore会话 try { arSession = new Session(this); Config config = new Config(arSession); config.setCameraFacing(Config.CameraFacing.BACK); arSession.configure(config); } catch (UnavailableException e) { e.printStackTrace(); Toast.makeText(this, "设备不支持ARCore", Toast.LENGTH_SHORT).show(); finish(); return; } // 绑定SurfaceView回调,处理GL上下文生命周期 surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { try { arSession.setDisplayGeometry(getDisplay().getRotation(), surfaceView.getWidth(), surfaceView.getHeight()); arSession.setSurface(holder.getSurface()); } catch (Exception e) { e.printStackTrace(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { arSession.setDisplayGeometry(getDisplay().getRotation(), width, height); } @Override public void surfaceDestroyed(SurfaceHolder holder) { arSession.pause(); } }); // 拍照按钮点击事件 captureBtn.setOnClickListener(v -> capturePhotoWithPose()); } // 处理ARCore会话生命周期 @Override protected void onResume() { super.onResume(); if (arSession != null) { try { arSession.resume(); } catch (CameraNotAvailableException e) { e.printStackTrace(); arSession = null; } } } @Override protected void onPause() { super.onPause(); if (arSession != null) arSession.pause(); } @Override protected void onDestroy() { super.onDestroy(); if (arSession != null) arSession.close(); } }
步骤3:拍照并同步保存Pose数据
实现拍照逻辑,从ARCore的Frame中获取Pose,并与照片关联保存:
private void capturePhotoWithPose() { if (arSession == null) return; // 获取当前帧与设备Pose Frame frame = arSession.update(); Pose devicePose = frame.getCamera().getPose(); // 获取相机图像并保存为照片 try (Image image = frame.acquireCameraImage()) { String photoPath = savePhotoToStorage(image); if (photoPath != null) { savePoseToExif(devicePose, photoPath); Toast.makeText(this, "照片与Pose已保存", Toast.LENGTH_SHORT).show(); } } catch (Exception e) { e.printStackTrace(); } } // 将Image转为JPEG保存到本地 private String savePhotoToStorage(Image image) { Image.Plane[] planes = image.getPlanes(); ByteBuffer yBuffer = planes[0].getBuffer(); ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); byte[] nv21 = new byte[yBuffer.remaining() + uBuffer.remaining() + vBuffer.remaining()]; yBuffer.get(nv21, 0, yBuffer.remaining()); vBuffer.get(nv21, yBuffer.remaining(), vBuffer.remaining()); uBuffer.get(nv21, yBuffer.remaining() + vBuffer.remaining(), uBuffer.remaining()); YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, image.getWidth(), image.getHeight(), null); ByteArrayOutputStream outStream = new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0, 0, image.getWidth(), image.getHeight()), 90, outStream); String photoDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); String photoName = "pose_photo_" + System.currentTimeMillis() + ".jpg"; String photoPath = photoDir + File.separator + photoName; try (FileOutputStream fos = new FileOutputStream(photoPath)) { fos.write(outStream.toByteArray()); return photoPath; } catch (IOException e) { e.printStackTrace(); return null; } } // 将Pose数据写入照片Exif元数据 private void savePoseToExif(Pose pose, String photoPath) { float[] translation = pose.getTranslation(); float[] rotation = pose.getRotationQuaternion(); try { ExifInterface exif = new ExifInterface(photoPath); // 保存平移向量(自定义字段,可替换为合适的Exif标签) exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, String.valueOf(translation[0])); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, String.valueOf(translation[1])); exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, String.valueOf(translation[2])); // 保存旋转四元数到用户注释字段 String rotationStr = String.format("%f,%f,%f,%f", rotation[0], rotation[1], rotation[2], rotation[3]); exif.setAttribute(ExifInterface.TAG_USER_COMMENT, rotationStr); exif.saveAttributes(); } catch (IOException e) { e.printStackTrace(); } }
步骤4:配置权限与清单
在AndroidManifest.xml中添加必要权限与ARCore声明:
<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera.ar" android:required="true" /> <application ...> <meta-data android:name="com.google.ar.core" android:value="required" /> </application>
关键注意事项
- 动态申请相机权限:Android 6.0+需在代码中请求
Manifest.permission.CAMERA权限,否则无法启动相机。 - ARCore兼容性:部分旧设备不支持ARCore,需在初始化时捕获
UnavailableException并提示用户。 - 图像格式处理:ARCore返回的
Image为YUV格式,需转换为JPEG才能保存为常规照片。
内容的提问来源于stack exchange,提问作者Kiran George
相关产品推荐
相关产品推荐

