☰
Android多传感器同步采集:Camera2、IMU与OIS数据融合实战
2026/10/16 7:35:13 网站建设 项目流程

简介:VideoIMUCapture-Android 是一款面向计算机视觉与机器人研究者的 Android 数据采集工具,用于同步录制视频帧与 IMU 数据,服务于 SLAM 与 Structure from Motion 三维重建实验。它按约 30Hz 记录相机帧、约 100Hz 记录惯性测量数据并统一时钟,视频存为 H.264/MP4,帧元数据与 IMU 数据写入 protobuf3 文件。针对现代手机光学防抖导致每帧相机参数不一致、且多数设备无法关闭 OIS 的痛点,应用会在录制时给出明确警告,并在设备支持时提供 OIS 数据,帮助研究者区分光学防抖与数字视频稳定的影响。资源包共 121 个文件,约 3.37MB,以 Java 源码、XML 布局、PNG 资源为主,另含 Python 脚本、Gradle 构建文件、proto 定义与 Docker 配置,覆盖 Camera2 API 调用、数据同步与工程构建流程。目前已有 617 人学习,适合需要采集真实设备 IMU 与视频数据、开展视觉惯性里程计或三维重建实验的开发者参考。

1. 从一次视觉建图翻车说起:这个 Android 采集项目到底解决什么问题

做视觉 SLAM 或者 Structure from Motion 的开发者,大概率都经历过这种崩溃:手机举着绕了一圈,回来看轨迹发现尺度飘得离谱,重建出来的点云像被揉皱的纸。问题往往不在算法,而在数据源——你只拿到了视频帧,却不知道每一帧对应的手机姿态、角速度、线加速度,更不知道这一帧有没有被 OIS 光学防抖悄悄挪过像素。VideoIMUCapture-Android 就是冲着这个痛点来的:它是一个 Android 采集端应用,把相机帧、IMU 数据、以及设备支持时的 OIS 稳定信息同步落盘,专门喂给 Motion 研究、SLAM 和 SfM 这类对时空对齐敏感的下游任务。它用 Camera2 API 拿原始帧,用传感器框架拿加速度计和陀螺仪,还特意区分了 OIS 和数字视频稳定(DVS)——前者是物理镜片位移,后者是算法裁切,两者对几何标定的意义完全不同。适合谁?做多传感器融合、视觉惯性里程计、或者需要真实手持数据的实验室和工程团队。如果你只是想要个美颜录像 App,这项目不是给你准备的。

2. 技术选型与目录结构:Camera2、IMU 与 OIS 数据怎么被组织起来

2.1 为什么是 Camera2 而不是 CameraX

这个项目对帧的控制粒度要求很高。CameraX 封装得太舒服,舒服到你拿不到精确的传感器曝光时间戳和帧号,而 SLAM 里时间戳对不齐,后面优化就是玄学。Camera2 虽然 API 啰嗦,但能通过CaptureRequest.SENSOR_TIMESTAMP拿到纳秒级硬件时间,还能控制输出格式为 YUV 或 RAW,方便后续做特征提取。项目里常见的做法是开一个CameraCaptureSession,把ImageReader的 Surface 和预览 Surface 一起塞进去,每来一帧就在OnImageAvailableListener里把 buffer 和对应的时间戳写进队列。

// 典型的 Camera2 采集会话配置片段 CameraCaptureSession.StateCallback sessionCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { try { CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); builder.addTarget(previewSurface); builder.addTarget(imageReader.getSurface()); // 强制开启 OIS(如果设备支持) if (oisSupported) { builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON); } // 关闭数字视频稳定,避免算法裁切干扰几何 builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF); session.setRepeatingRequest(builder.build(), null, backgroundHandler); } catch (CameraAccessException e) { Log.e(TAG, "配置采集会话失败", e); } } };

这段代码的关键在最后两行:OIS 开、DVS 关。很多设备默认会开电子防抖,结果你拿到的画面是被裁切并重采样过的,内参标定直接废掉。项目把这两个开关暴露出来,就是为了让研究者自己决定要不要物理稳定数据。

2.2 目录结构:数据落盘的分层设计

一个采集类项目最怕文件乱成一锅粥。这个项目的目录组织通常按会话(session)切分,每个会话一个文件夹,里面再按传感器类型分文件。常见结构如下:

/Android/data/<包名>/files/sessions/ └── session_20250101_120000/ ├── video_frames/ # 原始帧或编码后的视频 │ ├── frame_000001.yuv │ └── frame_000002.yuv ├── imu_data.csv # 加速度计+陀螺仪,带时间戳 ├── ois_data.csv # OIS 镜片位移或状态标记 └── metadata.json # 设备型号、分辨率、帧率、内参初值

imu_data.csv里一般有timestamp_ns, accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z这几列,时间戳统一用SystemClock.elapsedRealtimeNanos()或者传感器自身的SensorEvent.timestamp,后者更贴近硬件但不同厂商对齐方式有差异。ois_data.csv不是所有设备都有,只有支持 OIS 且驱动上报的机型才能拿到,通常记录的是LENS_OPTICAL_STABILIZATION_MODE的状态变化或者更底层的镜片位置(取决于厂商 HAL 实现)。metadata.json是给下游工具读的,省得每次手动填参数。

2.3 IMU 采集:频率、批次与时间对齐

IMU 采集看起来简单,注册个SensorEventListener就完事,但真要做到和视频帧对齐,坑不少。项目里一般会同时注册TYPE_ACCELEROMETER和TYPE_GYROSCOPE,采样率请求SENSOR_DELAY_FASTEST,然后在回调里把数据塞进一个线程安全的环形缓冲区,由写文件线程批量刷盘,避免频繁 IO 拖垮采集。

// 传感器回调中带时间戳写入缓冲区 @Override public void onSensorChanged(SensorEvent event) { long timestampNs = event.timestamp; // 传感器硬件时间 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { imuBuffer.add(new ImuSample(timestampNs, event.values[0], event.values[1], event.values[2], 0, 0, 0)); } else if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { imuBuffer.add(new ImuSample(timestampNs, 0, 0, 0, event.values[0], event.values[1], event.values[2])); } }

注意这里没有用System.currentTimeMillis(),因为那个时间戳会被系统调度和 GC 影响,抖动可能到几十毫秒,对 VIO 来说是灾难。event.timestamp虽然不同设备基准不同,但同一设备内是一致的,下游做时间对齐时只需要一个偏移量。

3. 跑起来:从编译到第一次采集的完整操作链路

3.1 环境准备与权限申请

这个项目是标准 Android 工程,用 Android Studio 打开即可。编译前确认minSdkVersion不低于 21(Camera2 的完整功能从 21 开始),targetSdkVersion建议 33 以上,否则新系统上文件写入和传感器后台限制会让你抓狂。权限方面,除了CAMERA和RECORD_AUDIO(如果录视频带音轨),还需要WRITE_EXTERNAL_STORAGE或者用MediaStoreAPI 写到应用私有目录。传感器本身不需要运行时权限,但 Android 12 以后后台采集需要FOREGROUND_SERVICE。

<!-- AndroidManifest.xml 关键权限 --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.sensor.accelerometer" /> <uses-feature android:name="android.hardware.sensor.gyroscope" />

uses-feature里把加速度计和陀螺仪标成必需,是为了在应用商店过滤掉没有 IMU 的设备,免得用户装完发现采不了数据。

3.2 采集流程:一次会话的启动与停止

启动采集的典型顺序是:检查权限 → 打开相机 → 配置会话 → 注册传感器 → 创建会话文件夹 → 开始写文件。停止时反过来,先注销传感器,再关相机会话,最后 flush 缓冲区并关闭文件句柄。项目里一般会封装一个CaptureSessionManager类来管这些状态,避免 Activity 生命周期一乱就泄漏。

// 启动采集的简化流程 public void startCapture() { if (!hasPermissions()) { requestPermissions(); return; } sessionDir = createSessionDirectory(); // 创建带时间戳的文件夹 openCamera(); // 异步打开,回调里配置会话 registerImuSensors(); // 注册加速度计和陀螺仪 isCapturing = true; } public void stopCapture() { isCapturing = false; unregisterImuSensors(); closeCamera(); flushAndCloseFiles(); // 把缓冲区剩余数据写完 }

这里有个血泪经验:stopCapture()里一定要先停传感器再关相机,否则相机关闭过程中传感器还在回调,写文件线程可能已经关了,直接抛IllegalStateException。

3.3 验证数据:怎么确认采到的数据能用

采完一段数据,别急着扔给 SLAM 算法。先做三件事:第一,看imu_data.csv的行数是否和采集时长匹配,比如 100Hz 采 10 秒应该有 1000 行左右,差太多说明传感器回调被系统丢了;第二,用 Python 画一下加速度计三轴的波形,静止时应该接近重力加速度,晃动时能看到明显变化;第三,检查视频帧数和 IMU 时间戳的覆盖范围是否一致,如果视频比 IMU 长很多,说明相机启动比传感器慢,下游对齐时要裁掉。

# 快速检查 IMU 数据的 Python 脚本 import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('imu_data.csv') print(f"总样本数: {len(df)}") print(f"时间跨度: {(df['timestamp_ns'].max() - df['timestamp_ns'].min()) / 1e9:.2f} 秒") plt.plot(df['timestamp_ns'] / 1e9, df['accel_x'], label='accel_x') plt.plot(df['timestamp_ns'] / 1e9, df['accel_y'], label='accel_y') plt.plot(df['timestamp_ns'] / 1e9, df['accel_z'], label='accel_z') plt.legend() plt.show()

如果波形里出现长时间平直段或者剧烈跳变,大概率是传感器被系统降频或者数据竞争了,得回去检查缓冲区实现。

4. 避坑与排查:采集端最容易翻车的五个地方

4.1 现象:视频帧时间戳和 IMU 时间戳对不上,差出几百毫秒

原因:Camera2 的SENSOR_TIMESTAMP和SensorEvent.timestamp用的是不同时钟源。前者通常是CLOCK_MONOTONIC,后者在不同厂商实现里可能是CLOCK_BOOTTIME或者别的。解决:在采集开始时记录一次两个时钟的差值,存进metadata.json,下游对齐时统一减掉。别指望它们天生一致。

4.2 现象:OIS 数据文件是空的,但设备明明支持光学防抖

原因:很多厂商的 Camera HAL 不把 OIS 状态通过CaptureResult上报,或者只在特定分辨率/帧率下才启用。解决:先查CameraCharacteristics里LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION是否包含ON,再在CaptureCallback里监听CaptureResult.LENS_OPTICAL_STABILIZATION_MODE,如果一直拿不到,就接受这个设备采不到 OIS 数据的事实,别硬编。

4.3 现象:采集几分钟后应用卡死或者文件写入失败

原因:写文件在主线程或者缓冲区无限增长。解决:把文件 IO 放到独立 HandlerThread,缓冲区设上限,满了就丢最旧的数据并记日志。另外 Android 11 以后应用私有目录有配额,长时间采集要定期检查剩余空间。

4.4 现象:不同设备采出来的 IMU 坐标系方向不一样

原因:Android 传感器坐标系定义是固定的(X 右、Y 上、Z 朝屏幕外),但相机坐标系通常是 X 右、Y 下、Z 朝前。解决:在metadata.json里记录设备型号和传感器到相机的旋转矩阵,下游做外参标定时用。别假设所有设备都一样。

4.5 现象:录出来的视频有果冻效应,特征点匹配全乱

原因:卷帘快门(Rolling Shutter)加上手持晃动。解决:这个项目本身不解决果冻效应,但你可以通过提高帧率、缩短曝光时间来缓解。如果设备支持全局快门(很少见),优先用。否则下游 SLAM 得用能处理卷帘快门的模型。

5. 二次开发:从采集端到标定工具的改造思路

拿到这个项目后,最直接的改造方向是加一个在线标定模块。比如在采集开始前,让用户对着棋盘格晃几秒,同步采集图像和 IMU,然后用现成的 Kalibr 或者自己写个优化器,把相机内参和 IMU 外参标出来,直接写进metadata.json。这样下游拿到数据就不用再猜参数了。

另一个方向是加数据回放和可视化。采集完一段数据,在应用里直接画个轨迹预览,或者把 IMU 积分出来的姿态叠在视频上,方便快速判断这段数据质量如何。实现上可以用SurfaceView加 OpenGL ES,把每帧的旋转矩阵画成一个小坐标系。

// 简单的姿态可视化思路:用 SensorManager 的旋转矢量 @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { float[] rotationMatrix = new float[9]; SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values); // 把 rotationMatrix 传给渲染线程,更新坐标系朝向 renderer.updateRotation(rotationMatrix); } }

注意TYPE_ROTATION_VECTOR是融合后的姿态,精度比纯陀螺仪积分好,但延迟可能略高。如果做实时预览够用,做高精度标定还是得用原始陀螺仪数据。

还有一个实用改造是加一个「采集质量评分」功能。采集结束后,自动检查 IMU 丢帧率、视频帧时间戳抖动、OIS 数据完整性,给个红黄绿评级。我一般会设几个阈值:IMU 丢帧超过 5% 直接红,时间戳抖动标准差超过 2ms 黄,OIS 数据缺失但设备支持黄。这样用户采完就知道要不要重采,省得跑完 SLAM 才发现数据废了。

从那以后我每次做采集类项目,都强制在停止采集后加一步「数据自检」,把关键指标打印到日志里,确认没问题才允许退出。这个习惯帮我省了无数次重采的麻烦。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询