简介VideoIMUCapture-Android 是一款面向 SLAM 与 Structure from MotionSfM研究者的 Android 数据采集工具用于同步录制相机帧与 IMU 数据为三维重建提供原始素材。应用以约 30Hz 采集视频帧、约 100Hz 采集惯性测量数据并统一到同一时钟视频以 H.264/MP4 存储帧元数据与 IMU 数据则写入 protobuf3 文件。针对现代手机普遍存在的光学防抖OIS问题该工具会在录制时给出明确警告并在设备支持时提供 OIS 数据便于研究者区分 OIS 与数字视频稳定DVS对相机参数的影响。资源包共 121 个文件约 3.37MB以 Java 源码、XML 布局、PNG 图标、Python 脚本及 Gradle 构建文件为主另含 proto 定义与 Docker 相关配置结构完整、便于二次开发。目前已有 617 人学习下载适合具备 Android 与计算机视觉基础的研究人员参考使用。1. 从一台手机到一份SLAM数据集这个Android采集项目到底在解决什么做过视觉SLAM或三维重建的开发者大概都经历过这样的场景算法在仿真环境里跑得挺好一换到真实手机采集的数据就各种漂移、尺度对不上、时间戳错乱。问题往往不在算法本身而在数据源——手机相机的自动曝光在变、OIS光学防抖在偷偷补偿位移、IMU和图像帧的时间戳根本不在一个时钟域里。这个项目就是冲着这些痛点去的它把Android手机变成一个可控的、可复现的传感器数据采集终端同时抓取视频帧、IMU原始数据和相机内参并且明确区分OIS和DVS两种防抖状态下的数据差异。适合谁用做视觉惯性里程计、SfM重建、SLAM算法验证的研究者和工程师尤其是需要批量采集真实场景数据来喂给算法的那批人。技术栈上走的是Camera2 API SensorManager Java原生路线不依赖第三方视觉库采集端足够轻。2. 架构拆解Camera2、IMU与存储三条线怎么并行跑2.1 整体模块划分与数据流这个项目的架构并不复杂但胜在职责清晰。核心分成三块相机采集模块、IMU采集模块、数据落盘模块。相机模块基于Camera2 API负责打开摄像头、配置输出Surface、接收每一帧的Image数据IMU模块通过SensorManager注册加速度计和陀螺仪监听器拿到的是原始三轴数据落盘模块则把图像帧写成文件、把IMU数据按时间戳追加到文本或二进制文件里。数据流是这样的Camera2的ImageReader回调触发后先取出该帧的传感器时间戳Image.getTimestamp()同时从IMU缓冲区里取出与该时间戳最接近的若干条IMU记录一起打包写入。这里有个关键设计——图像帧和IMU数据不是各写各的而是在写入时做时间对齐保证后续算法拿到的是已经粗对齐的数据。常见做法是给每条记录分配一个单调递增的序号图像帧记录里带上帧号和对应的时间戳范围IMU记录里带上自己的时间戳和序号。这样即使后续需要重新对齐也有据可查。2.2 Camera2会话配置与OIS/DVS状态读取Camera2的会话配置是整个采集流程里最容易翻车的地方。项目需要同时拿到图像数据和相机特性信息所以CameraCharacteristics的读取要在打开相机之前完成。下面这段代码展示了如何查询设备是否支持OIS以及当前OIS模式// 获取相机特性 CameraCharacteristics characteristics cameraManager.getCameraCharacteristics(cameraId); // 查询OIS支持情况 int[] oisModes characteristics.get( CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION); boolean oisSupported false; if (oisModes ! null) { for (int mode : oisModes) { if (mode CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON) { oisSupported true; break; } } } // 查询DVS支持情况数字视频稳定 int[] videoStabModes characteristics.get( CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES); boolean dvsSupported false; if (videoStabModes ! null) { for (int mode : videoStabModes) { if (mode CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON) { dvsSupported true; break; } } } // 查询当前OIS状态 Integer currentOisMode characteristics.get( CameraCharacteristics.LENS_INFO_OPTICAL_STABILIZATION_MODE);这段代码的逻辑很直接先拿到CameraCharacteristics对象然后分别查询OIS和DVS的可用模式列表。LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION返回的是该设备支持的OIS模式数组如果里面包含LENS_OPTICAL_STABILIZATION_MODE_ON说明硬件支持OIS。DVS的查询类似用的是CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES。最后一行读取的是当前生效的OIS模式这个值会随拍摄状态变化需要在每帧采集时重新读取。参数说明cameraId是相机ID字符串通常后置摄像头是0LENS_INFO_OPTICAL_STABILIZATION_MODE返回的是当前OIS模式可能为OFF、ON或ON_DYNAMIC。注意OIS是硬件级防抖DVS是软件级防抖两者可以同时开启但开启后图像帧的像素坐标会发生变化做SLAM时需要知道当前处于哪种状态。2.3 IMU数据采集与时间戳对齐IMU采集用的是SensorManager注册TYPE_ACCELEROMETER和TYPE_GYROSCOPE两个传感器。采样率通过registerListener的第三个参数控制通常设成SENSOR_DELAY_GAME或直接指定微秒数。关键点在于IMU回调的时间戳是纳秒级的System.nanoTime()而Camera2的图像时间戳是SensorEvent.timestamp同源的两者可以对齐。// 注册IMU监听器 SensorManager sensorManager (SensorManager) getSystemService(Context.SENSOR_SERVICE); Sensor accelerometer sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); Sensor gyroscope sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); // 采样率设为100Hz10000微秒 sensorManager.registerListener(imuListener, accelerometer, 10000); sensorManager.registerListener(imuListener, gyroscope, 10000); // IMU回调中缓存数据 private final SensorEventListener imuListener new SensorEventListener() { Override public void onSensorChanged(SensorEvent event) { // event.timestamp是纳秒级时间戳 ImuRecord record new ImuRecord( event.timestamp, event.sensor.getType(), event.values[0], event.values[1], event.values[2] ); // 写入环形缓冲区供图像帧对齐时取用 imuBuffer.add(record); } Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } };逻辑说明registerListener的第三个参数是采样周期单位微秒10000对应100Hz。onSensorChanged回调里拿到的event.timestamp是纳秒级和Camera2的Image.getTimestamp()在同一个时钟源上这是后续做时间对齐的基础。imuBuffer建议用ConcurrentLinkedQueue或加锁的ArrayDeque因为IMU回调在传感器线程图像回调在另一个线程两者并发访问缓冲区。参数说明event.values对于加速度计是三个轴的加速度值含重力对于陀螺仪是三个轴的角速度。event.sensor.getType()用来区分是哪个传感器的数据。缓冲区大小建议至少覆盖两帧图像之间的时间跨度按30fps算就是33毫秒100Hz采样率下大约3到4条记录。2.4 数据落盘格式与目录组织采集到的数据最终要写成文件目录结构建议按会话组织/sdcard/VideoIMUCapture/session_20250101_120000/ ├── images/ │ ├── frame_000001.jpg │ ├── frame_000002.jpg │ └── ... ├── imu_data.csv ├── camera_info.json └── session_meta.jsonimu_data.csv每行格式为timestamp_ns,sensor_type,x,y,z。camera_info.json存内参矩阵、畸变系数、分辨率。session_meta.json存会话开始时间、设备型号、OIS/DVS状态、帧率等元信息。图像帧用JPEG格式存质量设成95以上避免压缩伪影影响特征点提取。写文件时注意用BufferedWriter批量写不要每来一条IMU数据就flush一次否则IO会成为瓶颈。图像帧的写入建议放到单独线程用ExecutorService管理避免阻塞相机回调。3. 跑起来从零编译到第一次采集的完整步骤3.1 环境准备与项目导入这个项目是标准的Android Studio工程用Gradle构建。需要准备的环境Android Studio建议最新稳定版、JDK 17、Android SDKAPI 24以上因为Camera2的完整功能在API 21引入但部分特性如OIS状态读取需要更高版本。项目导入后先同步Gradle如果卡在下载依赖检查build.gradle里的仓库配置。# 克隆项目后进入目录 cd VideoIMUCapture-Android # 查看Gradle版本 cat gradle/wrapper/gradle-wrapper.properties # 命令行编译可选Android Studio里直接点Run也行 ./gradlew assembleDebug编译产物在app/build/outputs/apk/debug/下。如果编译报错找不到CameraCharacteristics.LENS_INFO_OPTICAL_STABILIZATION检查compileSdk是否设成了33以上。3.2 权限配置与运行时申请Android 6.0以上需要运行时申请相机和存储权限。AndroidManifest.xml里声明uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-feature android:nameandroid.hardware.camera / uses-feature android:nameandroid.hardware.sensor.accelerometer / uses-feature android:nameandroid.hardware.sensor.gyroscope /运行时申请用ActivityCompat.requestPermissions在onRequestPermissionsResult里判断结果。注意Android 10以上要用MediaStore或getExternalFilesDir()直接写/sdcard/会失败。3.3 采集参数配置与启动流程启动采集前需要配置几个关键参数分辨率、帧率、IMU采样率、是否开启OIS。这些参数建议做成UI上的选项方便不同场景切换。下面是一个配置和启动的示例// 配置采集参数 CaptureConfig config new CaptureConfig.Builder() .setResolution(1920, 1080) // 1080p .setFrameRate(30) // 30fps .setImuSampleRate(100) // 100Hz .setEnableOIS(false) // 关闭OIS保留原始运动 .setEnableDVS(false) // 关闭DVS .setOutputDir(getExternalFilesDir(null) /session_ timestamp) .build(); // 启动采集 CaptureSession session new CaptureSession(this, config); session.start();逻辑说明CaptureConfig用Builder模式构造每个参数都有默认值。setEnableOIS(false)表示关闭光学防抖这样采集到的图像帧保留真实的相机运动适合SLAM算法验证。如果要做OIS对比实验就设成true再采一组。CaptureSession封装了相机打开、会话创建、IMU注册、文件写入的完整流程start()方法里按顺序执行。参数说明分辨率建议用1920x1080或1280x720太高会导致帧率下降和存储压力。帧率30fps是平衡选择60fps对IMU对齐要求更高。IMU采样率100Hz起步200Hz更好但耗电增加。输出目录用getExternalFilesDir()避免权限问题。3.4 采集过程中的状态监控采集启动后需要实时监控几个指标实际帧率、IMU数据量、丢帧数、存储剩余空间。建议在界面上显示一个简单的状态栏。如果发现帧率远低于设定值检查是不是图像格式设成了JPEG直接输出应该用YUV_420_888再转JPEG或者写入线程阻塞了相机回调。// 简单的帧率统计 private long lastFrameTime 0; private int frameCount 0; private float currentFps 0; private void onFrameAvailable(Image image) { long now image.getTimestamp(); if (lastFrameTime ! 0) { float instantFps 1e9f / (now - lastFrameTime); currentFps currentFps * 0.9f instantFps * 0.1f; // 滑动平均 } lastFrameTime now; frameCount; // ... 后续处理 }这段代码用滑动平均算实时帧率系数0.9是经验值越大越平滑但响应越慢。image.getTimestamp()返回纳秒所以除以1e9换算成秒。4. 避坑与排查采集端最容易翻车的五个地方4.1 图像时间戳和IMU时间戳对不上现象采集完的数据拿去跑VIO发现轨迹漂移严重检查发现图像帧和IMU数据的时间戳差了十几毫秒甚至更多。原因Camera2的Image.getTimestamp()返回的是传感器曝光开始的时间而IMU的SensorEvent.timestamp是事件上报到应用层的时间两者之间隔着驱动和框架的处理延迟。另外不同厂商对时间戳的实现不一致有的用System.nanoTime()有的用SystemClock.elapsedRealtimeNanos()。解决在采集开始时记录一个基准时间所有时间戳都减去这个基准。同时采集结束后做一次离线对齐用互相关或者简单的最近邻匹配检查时间偏移量。如果偏移固定可以在后处理时补偿。4.2 OIS开启后内参失效现象开了OIS采集的数据用标定好的内参去做去畸变和三角化结果误差很大。原因OIS工作时镜头会移动等效于内参在每一帧都在变。标定得到的内参是OIS关闭状态下的开了OIS就不适用了。解决做SLAM或SfM采集时关闭OIS用纯机械固定或者后期做数字稳像。如果必须开OIS需要在每帧记录OIS的补偿量部分设备通过CaptureResult的LENS_OPTICAL_STABILIZATION_MODE和统计信息可以拿到但支持度参差不齐。4.3 写入阻塞导致丢帧现象采集过程中帧率从30fps掉到十几fps日志里看到ImageReader的onImageAvailable回调间隔变大。原因在相机回调线程里直接做JPEG编码和文件写入IO耗时太长ImageReader的缓冲区被占满后续帧直接丢弃。解决相机回调里只做最轻量的操作——把Image对象丢进一个阻塞队列由单独的写入线程池处理编码和落盘。队列长度设成3到5满了就丢弃最旧的帧并记录丢帧计数。4.4 不同设备IMU坐标系不一致现象在A手机上跑得挺好的算法换到B手机就完全失效。原因Android的传感器坐标系定义是固定的X轴向右Y轴向上Z轴指向屏幕外但相机坐标系在不同设备上的朝向可能不同导致IMU和相机之间的外参需要重新标定。解决采集时同时记录设备型号和传感器方向信息Sensor.getOrientation()后处理时根据设备型号加载对应的外参。更稳妥的做法是每次换设备都重新做一次相机-IMU联合标定。4.5 存储空间不足导致采集中断现象采集到一半突然停止日志显示IOException: No space left on device。原因1080p 30fps的JPEG图像每帧约200KB一分钟就是360MB加上IMU数据10分钟就超过3GB。解决采集前检查剩余空间估算可采集时长并提示用户。采集过程中定期检查空间低于阈值时自动停止并保存已采集的数据。另外可以用YUV原始格式存虽然文件更大但省去了JPEG编码时间后处理时再转。5. 二次开发从采集工具到算法验证平台的改造思路这个项目本身是一个采集工具但它的价值在于可以作为一个算法验证平台的基础。我拿到手之后做的第一件事是把数据落盘格式改成HDF5因为CSV在IMU数据量大的时候读写都慢HDF5支持分块压缩和随机访问后处理时方便按时间范围切片。改的时候注意保持时间戳精度HDF5的int64存纳秒时间戳没问题。第二个改造点是加一个实时预览和特征点叠加的界面。用OpenCV的Android SDK在相机预览的SurfaceView上叠加ORB特征点采集时就能看到当前场景的特征丰富程度。这个功能对采集质量把控很有用——特征点太少的地方采了也是白采。实现上是在onFrameAvailable里把YUV转成RGBA送进ORB检测器再把关键点画到预览层上。注意别在相机回调线程里做会拖慢帧率。第三个改造是加一个采集质量评估模块。采集结束后自动算几个指标IMU数据的连续性有没有大段缺失、图像帧的时间间隔方差稳不稳定、特征点数量的时间序列够不够丰富。这些指标用Python脚本做后处理也行但集成到App里可以当场给采集人员反馈避免采完回去才发现数据不能用。# 后处理质量评估示例Python脚本在PC上跑 import numpy as np import pandas as pd # 读取IMU数据 imu pd.read_csv(imu_data.csv) timestamps imu[timestamp_ns].values dt np.diff(timestamps) / 1e9 # 转成秒 # 检查IMU连续性 expected_dt 0.01 # 100Hz对应10ms gaps np.where(dt expected_dt * 3)[0] # 超过3倍采样周期算缺失 print(fIMU数据总时长: {timestamps[-1] - timestamps[0]:.2f}秒) print(f缺失段数: {len(gaps)}) if len(gaps) 0: print(f最大缺失间隔: {dt[gaps].max():.4f}秒) # 检查图像帧时间间隔 frame_times np.array([...]) # 从图像文件名或元数据读取 frame_dt np.diff(frame_times) / 1e9 print(f平均帧率: {1.0 / frame_dt.mean():.2f}fps) print(f帧间隔标准差: {frame_dt.std() * 1000:.2f}ms)这段脚本的逻辑是先算IMU时间戳的差分找出超过3倍采样周期的间隔作为缺失段再算图像帧的时间间隔评估帧率稳定性。参数expected_dt根据实际IMU采样率调整100Hz就是0.01秒。gaps数组存的是缺失段起始位置的索引可以用来定位问题时间段。还有一个容易忽略的点采集时的设备温度。手机长时间采集会发热发热后相机和IMU的性能都会下降表现为帧率不稳、IMU噪声增大。我一般会在采集前让设备冷却到室温采集过程中监控电池温度超过40度就暂停休息。这个在session_meta.json里记一下温度曲线后处理时可以把高温段的数据标记出来。从那以后我每次做数据采集都强制走一遍「冷却-标定-试采-质量检查-正式采集」的流程试采30秒检查时间戳对齐和帧率稳定性没问题再开始正式采集。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?