简介面向SLAM与SfM研究场景的Android采集工具对应开源项目VideoIMUCapture-Android主打视频、IMU与相机元数据的同步采集适用于同时定位与建图SLAM、运动恢复结构SfM以及计算机视觉、机器人领域的研究者和开发者。App以约30Hz录制H.264/MP4视频以约100Hz惯性采样帧元数据与IMU数据一并写入protobuf3文件实现同一时钟下的时间戳对齐同时区分光学防抖OIS与数字视频稳定DVS检测到设备开启OIS时立刻出现警告避免用户拿带有逐帧变化内参的数据去做重建。如今绝大多数智能手机内置光学防抖且难以关闭这个提前告警功能对SLAM/SfM实验非常实用。压缩包共121个文件以Java、XML、Python、Gradle、Proto等类型为主Java代码覆盖Camera2 API调用、IMU读取与同步逻辑XML与PNG构成界面和图标Proto定义存储协议Python脚本用于数据校验与格式转换另有Dockerfile、shell脚本和Gradle构建配置便于复现环境、自动化构建和打包。工程还预置pre_build、post_build等钩子脚本可配合Dockerfile快速准备编译依赖。整个zip包仅3.37MB结构紧凑适合直接导入Android Studio使用。目前已有618人学习下载需要采集高质量视频惯性数据、规避防抖误差的SLAM/SfM研究者可将其作为基础工具或二次开发模板。1. 给SLAM/SfM研究者准备的Android数据采集端OIS与IMU时间戳到底有多关键手机采集的视觉-惯性数据是很多SLAM、SfM算法的验证基础但大多数人第一次拿到VideoIMUCapture-Android这类工程时会把它当成一个“录像传感器记录”的小工具直到在后处理阶段发现TUM格式的数据集无法复现轨迹才意识到问题出在采集端。这个Android工程解决的核心问题其实有两个一是用Camera2 API把视频帧和IMU事件按统一时间基准导出二是把光学图像稳定OIS和数字视频稳定DVS分开处理并尽可能拿到设备支持的OIS状态数据。它面向的是需要自采数据集做实验、调试VIO/SLAM前端、或者对比不同手机传感器特性的开发者不是给普通录像用户准备的。2. 技术底座Camera2 API、IMU读取与OIS/DVS稳定的本质2.1 为什么选Camera2 API帧回调里有系统级时间戳很多老工程还在用Camera 1因为它简单注册一个预览回调就能拿到NV21帧。但对于SLAM/SfM数据采集Camera 1有两个硬伤拿不到每帧的曝光起始时间戳也无法在录视频时同步读取传感器的稳定状态。Camera2 API虽然上手门槛高一些但它把采集管线的控制权完全交出来了关键参数都能从CaptureResult里逐帧读。我一般会在初始化时把这三个参数固定下来CONTROL_VIDEO_STABILIZATION_MODE_CONTINUOUS负责数字稳定DVSLENS_OPTICAL_STABILIZATION_MODE_STILL负责光学稳定OISSENSOR_TIMESTAMP负责拿到每帧的纳秒级时间基准。注意SENSOR_TIMESTAMP用的是系统单调时钟和SystemClock.elapsedRealtimeNanos()是同一个基准这点后面排查时间戳错位时非常重要。// Camera2创建捕获会话前的配置 private void configureRequest(CameraCaptureSession session) { CaptureRequest.Builder previewBuilder session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_RECORD); // 开启数字视频稳定(DVS)如果设备没有这项能力设置会被忽略 if (isDVSSupported()) { previewBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON); } // 尝试开启光学稳定(OIS)注意部分设备只支持拍照模式下的OIS if (isOISSupported()) { previewBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON); } // 把目标帧率锁在30fps避免自动曝光调整导致帧间隔抖动 previewBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range(30, 30)); previewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); // 实际录像走MediaRecorder的Surface预览走第二个Surface ListSurface surfaces new ArrayList(); surfaces.add(previewSurface); surfaces.add(recorderSurface); session.setRepeatingRequest(previewBuilder.build(), new CameraCaptureSession.CaptureCallback() { Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); // 每帧稳定状态都从result里读一次保存成camera_meta.csv } }, backgroundHandler); }这段代码的核心逻辑是把DVS和OIS都设为ON但这只是请求不是保证。设备不支持时会直接忽略该字段所以必须在CaptureCallback里用result.get(CaptureResult.LENS_OPTICAL_STABILIZATION_MODE)和result.get(CaptureResult.CONTROL_VIDEO_STABILIZATION_MODE)确认实际生效值。参数说明TEMPLATE_RECORD是录像模板它和TEMPLATE_PREVIEW的差异在于会自动匹配MediaRecorder的帧率和码率CONTROL_AE_TARGET_FPS_RANGE锁30fps能避免帧间隔在24/30/60之间跳变这对后期时间戳插值非常重要CONTROL_AE_MODE_ON_AUTO_FLASH会在暗光下自动补光但如果你要标定相机内参建议改成CONTROL_AE_MODE_OFF并固定曝光时间否则内参标定会出现亮度波动带来的边缘误差。2.2 IMU事件采集SensorManager的采样频率与时间基准IMU数据是本工程的第二个关键输出。加速度计和陀螺仪的采样频率直接决定VIO系统中的惯性约束质量一般至少需要100Hz以上。Android的SensorManager允许你请求SENSOR_DELAY_FASTEST但实际交付频率是由底层传感器和系统负载决定的不是请求什么就给什么。我习惯在onSensorChanged里做三件事记录事件自带的timestamp、记录事件到达的SystemClock.elapsedRealtimeNanos()、记录原始传感器值。这里timestamp和到达时间之间的差值就是系统的传感器事件延迟这个延迟如果波动很大说明系统调度有问题数据要谨慎使用。private final SensorEventListener imuListener new SensorEventListener() { Override public void onSensorChanged(SensorEvent event) { long sysNanos SystemClock.elapsedRealtimeNanos(); long eventNanos event.timestamp; // event.timestamp是传感器驱动层的硬件时间戳sysNanos是应用收到事件的时间 // 这两个值的差就是事件在系统里的传递延迟 long latency sysNanos - eventNanos; // 如果延迟超过2ms可能是系统休眠或CPU锁频做一个标记方便后处理过滤 boolean suspicious latency 2_000_000L; String line String.format(Locale.US, %d,%d,%d,%.6f,%.6f,%.6f,%d, eventNanos, sysNanos, latency, event.values[0], event.values[1], event.values[2], suspicious ? 1 : 0); // 写入缓冲区由后台线程落盘避免IO阻塞onSensorChanged imuBuffer.add(line); } }; private void startImuRecording() { SensorManager sm (SensorManager) getSystemService(Context.SENSOR_SERVICE); Sensor accel sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); Sensor gyro sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE); sm.registerListener(imuListener, accel, SensorManager.SENSOR_DELAY_FASTEST); sm.registerListener(imuListener, gyro, SensorManager.SENSOR_DELAY_FASTEST); }参数说明SENSOR_DELAY_FASTEST是最高频率请求但实际采样率取决于设备厂商和当前负载我需要解释一下为什么要记录latency字段因为很多SLAM数据集只给timestamp和values后处理时一旦发现时间漂移无法追查这个latency字段相当于事后诊断的后悔药。suspicious标志位也不是可有可无后期做VIO评估时这些被标记的样本可以直接从训练集里剔除避免脏数据污染。另一个容易被忽略的传感器是磁力计。很多工程只采加速度计和陀螺仪但对于纯视觉SLAM退化环境长走廊、白墙磁力计能提供一个弱航向约束。这个工程里把磁力计作为可选传感器如果实验场景没有强磁场干扰建议一并采集。2.3 OIS与DVS两种稳定机制对SfM/SLAM来说不是一回事做SfM和视觉SLAM的人对图像稳定通常持双重态度稳定能让特征点更容易匹配但稳定也会改变图像与实际相机运动的对应关系。OIS和DVS的差异尤其明显。OIS是光学层面的硬件防抖通过镜头或传感器位移补偿手抖它改变的是成像光路。一旦OIS开启不同帧之间相机内参不再是严格一致的因为镜头光心和传感器平面的相对位置在动态变化。对SfM来说这意味着每帧的principal point可能偏移几十个像素而传统SfM假设内参恒定直接用会出问题。DVS是数字层面的裁剪加变换它在ISP阶段对画面做仿射变换补偿。DVS的优点是内参不变缺点是真实视场角变小且帧裁切会引入额外的边缘模糊。下表是我在实测多台手机后整理的对比结论项目OIS光学稳定DVS数字稳定实现层镜头/传感器物理位移ISP/GPU图像变换内参一致性可能逐帧变化内参不变但FOV变小时间戳影响无直接影响无直接影响对SfM初始化的影响基础矩阵估计易受污染边缘特征点有效区域减少数据可利用性需同步稳定状态才能建模可用于大多数视觉算法这个表格的目的是让使用者先判断自己的算法能不能承受OIS带来的内参漂移。如果做稠密重建或高精度标定我建议强制关闭OIS如果做前端定位和鲁棒性测试OIS开启反而更接近真实产品状态。DVS则相对安全因为它不改变内参只是让有效FOV变小。VideoIMUCapture-Android在采集时把这两者的开关和实际生效状态都记录到了CSV里方便后处理时候按需筛选帧。3. 把工程跑起来编译、权限、采集与导出全链路3.1 导入工程与Gradle配置JDK、SDK版本与依赖拿到工程目录后第一步是确认构建环境。这个项目是纯Java实现的不依赖NDK和JNI所以只需要Android SDK和JDK省了很多交叉编译的麻烦。我用Android Studio打开工程时遇到过两个坑一个是compileSdk版本和本机SDK平台版本不一致另一个是Gradle插件版本过高导致AGP不兼容。我一般会先检查gradle.properties里有没有启用AndroidX再确认build.gradle里的targetSdk。targetSdk如果高于28需要在Manifest里显式声明摄像头权限的运行时请求逻辑。下面是这个工程比较稳妥的一套配置模板android { compileSdk 34 defaultConfig { applicationId com.research.videoimucapture minSdk 24 targetSdk 34 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.activity:activity:1.8.0 }参数说明minSdk 24对应Android 7.0这个版本开始Camera2 API的稳定性才算可用低于这个版本会有一批设备连PreviewCallback都回调不规律compileSdk 34只是编译版本不强制要求手机升级到Android 14但如果你的开发机是Android 11以下记得把sourceCompatibility改成11。代码里的applicationId只是占位符你自己改成自己的包名即可。3.2 权限声明与运行时请求只申请需要的传感器这个工程需要的权限集中在相机和部分传感器。摄像头权限是危险的必须在运行时动态申请加速度计和陀螺仪虽然是普通权限但Android 12开始部分传感器有“仅前台访问”限制需要在隐私政策里说明。不要一上来就申请所有权限按需申请更符合合规习惯。uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.RECORD_AUDIO android:requiredfalse / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 /运行时权限请求有一个容易被忽略的地方录音权限。很多SLAM采集工程并不需要音频但MediaRecorder默认可能会开启音频录制如果把RECORD_AUDIO权限拒绝掉MediaRecorder会直接抛异常。这个工程在录像前会主动检查是否有音频权限如果没有就用MediaRecorder的setAudioSource禁用音频轨道。这是一个很实用的处理方式避免了在无麦克风平板上采集直接崩溃的问题。private void requestPermissionThenStart() { String[] needed new String[]{Manifest.permission.CAMERA}; // Android 6.0必须动态申请低于6.0在安装时授权 if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { requestPermissions(needed, REQUEST_CODE_CAMERA); } else { startCapture(); } }参数说明WRITE_EXTERNAL_STORAGE只在Android 9及以下需要Android 10以后推荐用getExternalFilesDir()写入应用专属目录不需要存储权限。这个细节省掉了很多用户在Android 12设备上反复授权存储的体力活。3.3 采集实操启动录制、传感器日志与文件导出整个采集流程我建议按下面顺序操作先测试再正式采避免一趟外场实验废掉# 1. 连接设备并确认ADB能识别 adb devices -l # 2. 启动应用通过意图参数指定输出目录和录制时长 adb shell am start -n com.research.videoimucapture/.MainActivity \ --ei duration_sec 30 --es output_dir /sdcard/DCIM/imu_capture_run1 # 3. 查看logcat确认相机和IMU是否同时启动 adb logcat -s VideoIMUCapture | grep -E IMU|OIS|DVS|Timestamp应用启动后预览画面出现点击录制按钮等待倒计时结束目录下会生成下面四类文件。文件名前缀统一是毫秒级时间戳方便后处理时按时间线合并文件名示例内容1698712345678_video.mp4H.264编码视频固定30fps1698712345678_accel.csv加速度计三轴数据事件流1698712345678_gyro.csv陀螺仪三轴数据事件流1698712345678_camera_meta.csv每帧的曝光时间、ISO、OIS/DVS状态、时间戳采集过程中最重要的一步是检查传感器日志的输出频率。打开采集目录里的CSV文件如果相邻两行时间戳差值波动超过5倍说明存在传感器批量交付问题。批量交付本身不是错误但如果你做的是紧耦合VIO需要在预处理时把批量到达的事件重新分散到各自真实时间戳上。我一般会让设备在录制前静置5秒静置段数据会用来估计陀螺仪的零偏这是后期标定IMU内参的重要前提。4. 实战避坑从时间戳错位到OIS数据缺失的五个现场4.1 时间戳基准不一致导致的轨迹漂移现象采集完数据后做VIO评估发现轨迹在某些时刻突然跳变像是有外力把位姿拉了一下。我把视频帧和IMU轨迹叠加后发现轨迹跳变的时刻恰好对应视频中画面轻微抖动的帧。原因视频帧的时间戳取自MediaRecorderIMU时间戳取自SensorEvent.timestamp基准时钟不一致。MediaRecorder的帧时间戳有时带启动偏移而SensorEvent.timestamp直接对应硬件晶振两种时间戳不经转换直接强行对齐误差会在积分过程中被放大。解决统一基准。我在Camera2的CaptureCallback里从CaptureResult读SENSOR_TIMESTAMP这个值和SensorEvent.timestamp是同一个时钟源。视频文件本身只用来提取图像内容帧时间一律以SENSOR_TIMESTAMP为准通过ImageReader按帧回调记录不用MediaRecorder自带的时间戳。从那以后凡是做视觉-惯性数据采集我都强制走一遍“检查所有时间戳时钟源”这个动作。4.2 开启OIS后视频稳了但相机内参变了现象同一台手机开启OIS录制标定板用张正友法跑出的焦距和不开OIS相差约2%且重投影误差显著增大。关闭OIS后标定结果回归稳定。原因OIS在录制过程中持续调整镜头位置等效于每帧的内参都在微变。标定算法假设内参不变这个动态偏移被误差均摊到了所有参数里。解决做标定或稠密重建时强制关闭OIS。在设置请求里把LENS_OPTICAL_STABILIZATION_MODE设为OFF同时在camera_meta.csv里记录每帧的实际稳定状态方便后处理过滤。如果产品场景必须开启OIS建议在SfM流程中加入每帧principal point的补偿模型否则至少把OIS开启和关闭的数据分开评估。4.3 传感器数据看起来多但时间分布不均匀现象陀螺仪CSV文件里每秒平均有200条数据但相邻时间戳间隔从2ms到15ms不等画成时间序列分布像锯齿。VIO跑出来的数据方差忽大忽小。原因SensorManager支持批量交付batch mode事件可能在传感器驱动里积压后再一起送给应用层导致时间戳虽然真实但到达时间不规律。解决一是注册监听时设置SENSOR_DELAY_FASTEST并检查是否真的关闭了批量模式二是不要在onSensorChanged里做任何耗时操作确保事件被及时消费三是后处理时按事件自带timestamp重新采样到规则网格再用插值方法填充不要直接用到达时间做积分。这条坑最容易踩因为CSV看着数据量充足很多人直到跑算法才发现问题。4.4 设备明明支持OIS却拿不到OIS状态现象在宣传支持OIS的手机上运行camera_meta.csv里的OIS状态列一直是OFF设置请求也返回了ON但CaptureResult里读出来的稳定模式总是OFF。原因部分设备只在拍照模式TEMPLATE_STILL_CAPTURE下支持OIS录像模式TEMPLATE_RECORD下的OIS能力不开放或者需要先开启特定防抖模式才能联动生效。解决采数据之前先查询CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION确认支持的稳定模式列表再用TEMPLATE_RECORD实际跑一次验证。我一般在日志里打一条关键输出提示当前设备可用的OIS模式避免反复试错。CameraCharacteristics chars cameraManager.getCameraCharacteristics(cameraId); int[] availableModes chars.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION); // 可用模式里包含LENS_OPTICAL_STABILIZATION_MODE_ON才说明录像模式可用 boolean isOisUsable false; for (int mode : availableModes) { if (mode CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) { isOisUsable true; break; } }解决后务必记录下设备型号和系统版本因为这些能力列表在不同系统版本之间也会变化。不带设备型号的数据后期排查起来成本极高。4.5 录制中掉帧、掉数据没注意存储写入现象录制到第三分钟时视频帧率从30fps掉到15fpsIMU事件数也明显减少查看logcat发现MediaRecorder报Pending buffers。原因持续写入外部SD卡时老化的卡写入速度跟不上码率系统背压导致相机管道堵塞。有些设备在存储压力大时会让SensorManager降低采样率双路数据同时受影响。解决录制前检查存储设备的写入速度至少Class 10或U1级别优先写入手机内置存储把采集目录放在getExternalFilesDir()下而不是可移动SD卡。录制时监听MediaRecorder的info回调看到MEDIA_RECORDER_INFO_MAX_DURATION_REACHED或buffer相关警告就要准备停止。实测中高清屏设备建议把视频分辨率限制在1080P、码率12Mbps以内对采集数据质量影响不大但能显著降低掉帧概率。5. 进阶技巧用时间戳对齐自检确定数据可用的红线5.1 快速偏移估计法互相关求IMU与视频的静音间隙采集完数据后我先不跑完整SLAM而是先做一次时间戳对齐自检。做法很简单让相机做几次剧烈来回甩动同时记录陀螺仪角速度。因为角速度峰值和视频帧间光流的平均模长应该有强相关关系用互相关就能估计出两者之间的时间偏移。import numpy as np def estimate_offset(gyro_ts, gyro_data, frame_ts, flow_magnitude, max_offset_ms200): 用互相关估计IMU与视频的时间偏移 gyro_ts: 陀螺仪时间戳(ns) gyro_data: 陀螺仪主轴的角速度 frame_ts: 视频帧时间戳(ns) flow_magnitude: 每帧的平均光流模长 # 先把IMU重采样到30fps的网格上避免事件密度差异影响相关值 from scipy.signal import resample unified_ts frame_ts gyro_resampled np.interp(unified_ts, gyro_ts, gyro_data) flow_normed (flow_magnitude - flow_magnitude.mean()) / flow_magnitude.std() gyro_normed (gyro_resampled - gyro_resampled.mean()) / gyro_resampled.std() best_offset, best_corr 0, -1 # max_offset_ms对应采样率下最多40帧候选偏移 for offset_frame in range(-int(max_offset_ms / 33.3), int(max_offset_ms / 33.3) 1): shifted np.roll(gyro_normed, offset_frame) corr np.corrcoef(shifted, flow_normed)[0, 1] if corr best_corr: best_corr, best_offset corr, offset_frame * 33333333 return best_offset, best_corr这段脚本的价值在于它把抽象的时间戳对齐问题变成了一条明确的判断红线如果互相关峰值低于0.5说明IMU和视频的时间基准存在非线性问题不是简单加偏移能修正的这份数据需要重新采集。如果峰值在0.7以上就可以放心进入后面的标定和建图流程。我把这个脚本存成项目根目录下的check_sync.py每次采完一组数据先跑一遍过不了红线就立刻补采不在脏数据上浪费时间。5.2 设置冗余记录字段给后处理留后悔药数据采集是最容易偷懒也最容易翻车的环节。有些字段现在用不上但后处理一旦发现缺了它数据就没法补救。我在这个工程里维护了几个冗余字段每帧的曝光时间、ISO、设备型号、系统版本、传感器延迟标志。这些字段不直接参与SLAM计算但它们能让你在算法跑出怪异结果时快速判断是采集端问题还是算法问题。从那以后我每次采集外场数据都强制走一遍检查流程查看时间戳对齐自检报告、确认OIS/DVS状态列有值、检查传感器延迟标志位、核对设备型号。这套流程看起来多花五分钟但避免了我之后再花五个小时去排查一个对不齐的轨迹。希望帮到你。本文还有配套的精品资源点击获取