據(jù)?VideoIMUCapture-Android采集對(duì)齊的IMU與視頻)
簡(jiǎn)介VideoIMUCapture-Android是一款面向SLAM與SfM研究者的Android數(shù)據(jù)采集工具可同步捕獲視頻、IMU及相機(jī)元數(shù)據(jù)并對(duì)光學(xué)防抖OIS導(dǎo)致的逐幀相機(jī)參數(shù)漂移問(wèn)題提供警告與OIS數(shù)據(jù)支持適合需要構(gòu)建高質(zhì)量3D重建數(shù)據(jù)集的開(kāi)發(fā)者、科研人員學(xué)習(xí)使用。資源共121個(gè)文件以Java源碼、XML資源、Python腳本和Gradle構(gòu)建配置為主涵蓋Camera2 API調(diào)用、protobuf數(shù)據(jù)格式定義、Dockerfile及Shell輔助腳本壓縮包僅3.37MB工程結(jié)構(gòu)緊湊便于按模塊閱讀或二次開(kāi)發(fā)。目前已有618人學(xué)習(xí)下載。借助這套工程讀者可快速掌握同步時(shí)鐘下H.264視頻與約100Hz IMU數(shù)據(jù)的采集框架理解OIS與DVS的差異并基于現(xiàn)成代碼擴(kuò)展自己的數(shù)據(jù)記錄流程為后續(xù)SLAM算法測(cè)試或視覺(jué)慣性里程計(jì)實(shí)驗(yàn)提供可直接復(fù)用的基礎(chǔ)實(shí)現(xiàn)。1. 錄SLAM數(shù)據(jù)為什么必須關(guān)OISVideoIMUCapture-Android 解決的不是“能錄視頻”用 ORB-SLAM3 跑手機(jī)上錄好的視頻軌跡卻直接飛掉這是我在測(cè)試視覺(jué)慣性里程計(jì)時(shí)最常見(jiàn)的翻車(chē)現(xiàn)場(chǎng)。后來(lái)才意識(shí)到問(wèn)題多半不在算法而在采集端手機(jī)默認(rèn)開(kāi)了 OIS 和 DVS畫(huà)面穩(wěn)了內(nèi)參卻變了。VideoIMUCapture-Android 就是為這個(gè)場(chǎng)景準(zhǔn)備的——一次抓齊視頻、IMU 數(shù)據(jù)、相機(jī)參數(shù)并把設(shè)備是否支持、當(dāng)前是否開(kāi)啟 OIS 一并告訴下游。它要解決的不是“能錄像”而是“錄出來(lái)的數(shù)據(jù)能被 SLAM 與 Structure from MotionSfM直接使用”。適合兩類(lèi)人跑 VIO/SLAM 算法做數(shù)據(jù)集驗(yàn)證的以及用 SfM 重建需要干凈視頻源的。2. OIS 與 DVS 的差別兩種防抖如何毀掉 SLAM 與 SfM 的內(nèi)參和時(shí)間戳要理解這個(gè)采集工具為什么把 OIS 和 DVS 單獨(dú)拎出來(lái)講得先把這兩種防抖的本質(zhì)說(shuō)清楚。光學(xué)圖像穩(wěn)定OIS是物理層面的錄制時(shí)鏡頭模組或感光元件會(huì)沿 XY 方向平移用來(lái)抵消手部抖動(dòng)。數(shù)字視頻穩(wěn)定DVS是算法層面的相機(jī)先把整個(gè)傳感器畫(huà)面讀出來(lái)再做邊緣裁剪和幀間對(duì)齊最終輸出一幀“看起來(lái)沒(méi)抖”的圖像。兩者最直接的區(qū)別是作用對(duì)象不同但它們的副作用對(duì) SLAM/SfM 是一樣的——?dú)?nèi)參、亂時(shí)間戳。2.1 OIS 是鏡頭漂移DVS 是畫(huà)面裁剪兩者都讓內(nèi)參“時(shí)變”先看 OIS。當(dāng)鏡頭模組為補(bǔ)償抖動(dòng)而平移時(shí)光線(xiàn)到達(dá)傳感器的路徑改變了等效焦距和主點(diǎn)坐標(biāo)都在微變也就是內(nèi)參矩陣不再是常量。SLAM 里三角化、PnP、光束法平差全都建立在“內(nèi)參恒定”這個(gè)前提上一旦內(nèi)參隨每一幀波動(dòng)特征點(diǎn)的幾何關(guān)系就被系統(tǒng)性污染。更要命的是Android 公共 Camera2 API 通常只能讀出 OIS 的開(kāi)關(guān)模式拿不到鏡頭平移的位移量。這意味著你無(wú)法在后處理里補(bǔ)償它只能關(guān)掉它。再看 DVS。數(shù)字穩(wěn)定典型的實(shí)現(xiàn)是“先多讀幾行再裁剪平移窗口”。裁剪窗口每幀都可能不一樣等效焦距就跟著變幀間對(duì)齊還會(huì)引入時(shí)間緩沖讓視頻幀的真實(shí)曝光時(shí)間被平移了幾個(gè)幀周期。對(duì) SfM 來(lái)說(shuō)裁剪只是損失一點(diǎn) FOV但特征點(diǎn)匹配本身還能做對(duì)基于慣性數(shù)據(jù)緊耦合的 VIO 來(lái)說(shuō)視頻幀的時(shí)間戳一旦和 IMU 時(shí)間戳不對(duì)齊視覺(jué)和慣性的約束關(guān)系就錯(cuò)位了軌跡發(fā)散是必然的。兩者對(duì) SLAM/SfM 的影響差異可以放在一起看對(duì)比項(xiàng)OIS 光學(xué)防抖DVS 數(shù)字穩(wěn)定作用機(jī)制鏡頭/傳感器物理平移畫(huà)面裁剪與幀間對(duì)齊對(duì)內(nèi)參影響等效焦距和主點(diǎn)微變裁剪窗口變化導(dǎo)致 FOV 變化對(duì)時(shí)間戳影響影響較小主要是曝光時(shí)刻改變引入幀緩沖時(shí)間戳明顯偏移是否可補(bǔ)償公共 API 一般拿不到位移量可通過(guò)關(guān)閉算法避免SLAM 采集合適性必須關(guān)閉必須關(guān)閉有一類(lèi)設(shè)備支持“OIS 數(shù)據(jù)輸出”這也是標(biāo)題里特別強(qiáng)調(diào)的點(diǎn)。所謂“可以提供 OIS 數(shù)據(jù)”指的是系統(tǒng)能在 CaptureResult 里返回當(dāng)前幀 OIS 是否生效、生效模式是哪種。這個(gè)信息對(duì)算法有實(shí)際價(jià)值即便你忘了關(guān) OIS下游至少能通過(guò)這個(gè)字段把可疑的幀過(guò)濾掉。但請(qǐng)注意大多數(shù)手機(jī)不會(huì)把鏡頭位移的原始數(shù)值開(kāi)放給應(yīng)用層所以別指望拿它做精確補(bǔ)償。2.2 用 Camera2 檢測(cè)設(shè)備是否支持 OIS一份可控的 Kotlin 代碼理解了這兩種防抖的原理落地第一步就是在采集前檢測(cè)設(shè)備能力。VideoIMUCapture-Android 這類(lèi)工具一般會(huì)在啟動(dòng)時(shí)把相機(jī)能力讀出來(lái)寫(xiě)入 sensor_info.json方便后續(xù)排查。檢測(cè) OIS 和 DVS 支持情況的代碼不難用 Camera2 的兩個(gè)固定字段即可fun checkStabilizationSupport(cameraId: String): PairBoolean, Boolean { val characteristics cameraManager .getCameraCharacteristics(cameraId) // OIS 支持列表LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION val oisModes characteristics.get( CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION ) ?: intArrayOf(CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF) // DVS 支持列表CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES val dvsModes characteristics.get( CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES ) ?: intArrayOf(CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF) val oisSupported oisModes.any { it ! CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF } val dvsSupported dvsModes.any { it ! CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF } return oisSupported to dvsSupported }這段代碼先通過(guò)LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION拿到設(shè)備支持的 OIS 模式數(shù)組。數(shù)組里出現(xiàn)OFF之外的模式比如ON、AUTO說(shuō)明硬件支持光學(xué)防抖。同理CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES用于確認(rèn)是否支持?jǐn)?shù)字穩(wěn)定。代碼里?: intArrayOf(...)的兜底寫(xiě)法很關(guān)鍵部分低端設(shè)備或定制 ROM 不實(shí)現(xiàn)這兩個(gè)字段返回 null按“不支持”處理而不是崩潰。要注意的是這個(gè)檢測(cè)結(jié)果只代表“硬件/系統(tǒng)能力”不代表錄制時(shí) OIS 真的開(kāi)啟。是否生效要看每一幀 CaptureResult 里的LENS_OPTICAL_STABILIZATION_MODE實(shí)際值。我一般會(huì)在每一幀的回調(diào)里把這個(gè)值拉出來(lái)寫(xiě)入日志道理后文會(huì)講OIS 不是設(shè)置了就一定生效個(gè)別設(shè)備的 HAL 層會(huì)偷偷改回 AUTO。本節(jié)這個(gè)函數(shù)只是“設(shè)備畫(huà)像”真正起約束作用的是后面采集時(shí)的 CaptureRequest 設(shè)置。3. 從零搭建采集管線(xiàn)VideoIMUCapture-Android 的最小可用配置這一章解決“怎么跑起來(lái)”的問(wèn)題。VideoIMUCapture-Android 通常是一個(gè) Android Studio 工程Gradle 構(gòu)建核心模塊一般分成 camera相機(jī)預(yù)覽與控制、sensorIMU 采樣、recorder編碼落盤(pán)、calib參數(shù)導(dǎo)出幾塊。多數(shù)情況下你不需要改核心邏輯只需要配置好權(quán)限、選對(duì)目標(biāo)設(shè)備就能開(kāi)始錄第一段數(shù)據(jù)。但配置過(guò)程中有幾個(gè)位置很容易踩坑下面按順序展開(kāi)。3.1 工程結(jié)構(gòu)與權(quán)限AndroidManifest 里必須聲明什么采集應(yīng)用要碰的外設(shè)很直接相機(jī)、陀螺儀、加速度計(jì)有時(shí)還有麥克風(fēng)MediaRecorder 在部分版本上即使不錄音頻也可能要求聲明音頻權(quán)限。先看一份最小可用的 AndroidManifest 配置uses-feature android:nameandroid.hardware.camera.any android:requiredfalse / uses-feature android:nameandroid.hardware.sensor.gyroscope android:requiredfalse / uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion29 / uses-permission android:nameandroid.permission.RECORD_AUDIO /三點(diǎn)說(shuō)明。第一相機(jī)和陀螺儀都聲明為requiredfalse這樣應(yīng)用可以安裝到?jīng)]有陀螺儀的低端設(shè)備上運(yùn)行時(shí)再做能力檢測(cè)并降級(jí)處理。第二WRITE_EXTERNAL_STORAGE加maxSdkVersion29是給 Android 10 及以下版本用的Android 11 之后強(qiáng)制分區(qū)存儲(chǔ)應(yīng)用直接寫(xiě)自己的外部目錄不需要這個(gè)權(quán)限。第三錄音權(quán)限看著多余但 MediaRecorder 在視頻源設(shè)置為 SURFACE 時(shí)部分 OEM 驅(qū)動(dòng)仍會(huì)檢查RECORD_AUDIO不聲明會(huì)直接啟動(dòng)失敗所以寧可保留。權(quán)限的動(dòng)態(tài)申請(qǐng)建議在進(jìn)入錄制頁(yè)面前一次性完成不要在錄制中途彈窗。原因很簡(jiǎn)單彈窗會(huì)打斷錄制倒計(jì)時(shí)而且 Camera2 的打開(kāi)流程和權(quán)限回調(diào)交織在一起代碼會(huì)變得很難維護(hù)。錄制過(guò)程中一旦出現(xiàn)權(quán)限被撤回部分國(guó)產(chǎn) ROM 有“后臺(tái)權(quán)限回收”MediaRecorder 會(huì)以異常結(jié)束數(shù)據(jù)文件全部作廢。3.2 錄制 H.264 視頻與高頻 IMU兩套時(shí)鐘域的統(tǒng)一方案采集管線(xiàn)的核心工作有兩件用 Camera2 MediaRecorder 錄 H.264 視頻用 SensorManager 以最高頻率收加速度計(jì)和陀螺儀數(shù)據(jù)。先看 MediaRecorder 的配置片段val recorder MediaRecorder() recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE) recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264) recorder.setVideoEncodingBitRate(20 * 1000 * 1000) recorder.setVideoFrameRate(30) recorder.setVideoSize(1280, 720) recorder.setOrientationHint(0)參數(shù)上我一般這樣定編碼選 H.264 MP4這是所有 SLAM 工具鏈和后處理腳本最容易解析的組合碼率給 20 Mbps 對(duì) 720p 30fps 足夠不要盲目拉到 50 Mbps會(huì)讓中端手機(jī)寫(xiě)入卡頓、掉幀更嚴(yán)重分辨率選 1280x720 而不是 4K因?yàn)橐曈X(jué)里程計(jì)和 SfM 用的特征點(diǎn)分辨率并不需要多高720p 在室內(nèi)紋理下已經(jīng)能提取充足特征還省存儲(chǔ)空間。真正需要花心思的是 IMU 的時(shí)鐘域問(wèn)題。SensorManager 的SensorEvent.timestamp單位是納秒但它的時(shí)鐘起點(diǎn)因設(shè)備而異有芯片直接給 boot time也有驅(qū)動(dòng)換算過(guò)。直接把event.timestamp寫(xiě)進(jìn) CSV會(huì)遇到“同一秒內(nèi) IMU 時(shí)間戳和視頻幀時(shí)間戳對(duì)不上”的怪問(wèn)題。我一般不用它作為 CSV 主時(shí)間戳而是在回調(diào)到達(dá)時(shí)取SystemClock.elapsedRealtimeNanos()val listener object : SensorEventListener { override fun onSensorChanged(event: SensorEvent) { // event.timestamp 只用于同一傳感器隊(duì)列內(nèi)的排序 val unifiedTimestampNs SystemClock.elapsedRealtimeNanos() imuWriter.writeLine( event.sensor.type, unifiedTimestampNs, event.values[0], event.values[1], event.values[2] ) } override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} } sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_FASTEST) sensorManager.registerListener(listener, gyroscope, SensorManager.SENSOR_DELAY_FASTEST)這段代碼里的關(guān)鍵是“統(tǒng)一用elapsedRealtimeNanos()落盤(pán)”。elapsedRealtimeNanos()基于系統(tǒng)單調(diào)時(shí)鐘包含休眠時(shí)間原生支持跨傳感器比較。event.timestamp則保留用于調(diào)試——如果它和你寫(xiě)入的elapsedRealtimeNanos()之間存在固定偏移且漂移穩(wěn)定說(shuō)明驅(qū)動(dòng)時(shí)鐘沒(méi)毛病如果偏移亂跳那是設(shè)備驅(qū)動(dòng)的問(wèn)題換機(jī)比換代碼快。IMU 采樣率上SENSOR_DELAY_FASTEST是能申請(qǐng)到的最高頻率實(shí)際收到的頻率通常在 200 Hz 到 500 Hz 之間取決于設(shè)備型號(hào)。對(duì) VIO 算法來(lái)說(shuō) 200 Hz 的陀螺儀已經(jīng)夠用不需要更高。還要提醒一句注冊(cè)監(jiān)聽(tīng)時(shí)不要漏了onAccuracyChanged回調(diào)里至少要記錄精度等級(jí)精度突然變?yōu)?0不可信時(shí)對(duì)應(yīng)的 IMU 片段最好在數(shù)據(jù)集說(shuō)明里標(biāo)注出來(lái)。3.3 落盤(pán)文件組織video、imu.csv、camera_params 各放哪采集結(jié)束不能只有一個(gè)視頻文件。SLAM 算法需要的是視頻、IMU、相機(jī)參數(shù)、設(shè)備信息多路對(duì)齊的數(shù)據(jù)包。我習(xí)慣按會(huì)話(huà)建目錄每次錄制生成一個(gè)時(shí)間戳命名的文件夾session_20250101_153000/ video.mp4 imu.csv frames.csv camera_params.json sensor_info.json每個(gè)文件的職責(zé)要明確。video.mp4是 H.264 編碼的視頻流imu.csv保存加速度計(jì)和陀螺儀的逐條數(shù)據(jù)frames.csv是每一幀視頻的捕獲時(shí)間戳camera_params.json放相機(jī)內(nèi)參和畸變參數(shù)sensor_info.json記錄設(shè)備能力檢測(cè)結(jié)果和錄制時(shí)實(shí)際生效的 OIS/DVS 狀態(tài)。這套組織結(jié)構(gòu)的好處是后續(xù)跑標(biāo)定或數(shù)據(jù)集轉(zhuǎn)換時(shí)所有信息都在一個(gè)目錄里不用翻聊天記錄找參數(shù)。camera_params.json的內(nèi)容通常長(zhǎng)這樣{ camera_id: 0, resolution: [1280, 720], fps: 30, camera_matrix: [[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dist_coeffs: [k1, k2, p1, p2, k3], ois_mode: OFF, dvs_mode: OFF }注意ois_mode和dvs_mode記錄的是錄制過(guò)程中實(shí)際生效的模式不是設(shè)備支持列表。這兩個(gè)字段是后處理程序判斷數(shù)據(jù)可信度的依據(jù)如果發(fā)現(xiàn)某一段視頻的ois_mode變成了ON那這段的幾何約束必須打上標(biāo)記。這里也回應(yīng)了標(biāo)題里的那句“如果設(shè)備支持則可以提供 OIS 數(shù)據(jù)”——設(shè)備支持 OIS 時(shí)你至少能把這種“每幀是否被防抖污染”的狀態(tài)信息給到下游。4. 從視頻到 SLAM 數(shù)據(jù)集幀時(shí)間戳與相機(jī)標(biāo)定的補(bǔ)齊方法錄完原始視頻到真正能喂給 VINS-Mono 或 ORB-SLAM3中間還差兩步補(bǔ)逐幀時(shí)間戳、標(biāo)定相機(jī)內(nèi)參。這一步也是視頻采集工具和普通錄像 App 拉開(kāi)差距的地方。MediaRecorder 寫(xiě)出來(lái)的 MP4 文件本身不含你想要的“每一幀的真實(shí)曝光時(shí)間”相機(jī)標(biāo)定也不能只靠 JSON 里那幾個(gè)占位符得實(shí)際算出來(lái)。4.1 MediaRecorder 不給你逐幀時(shí)間戳從 CaptureResult 里撈MediaRecorder 封裝得太徹底應(yīng)用層拿不到“這一幀是什么時(shí)候曝光的”這種信息。常見(jiàn)做法是在錄制的同時(shí)通過(guò) Camera2 的CaptureCallback把每一幀的時(shí)間戳撈出來(lái)單獨(dú)寫(xiě)一個(gè)frames.csv。代碼核心是重寫(xiě)onCaptureCompletedprivate val captureCallback object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { val sensorTimestampNs result.get( CaptureResult.SENSOR_TIMESTAMP ) ?: return // 同時(shí)記錄單調(diào)時(shí)鐘用于把 sensor 時(shí)間戳統(tǒng)一到同一時(shí)基 val wallClockNs SystemClock.elapsedRealtimeNanos() framesWriter.writeLine($sensorTimestampNs,$wallClockNs) } }這里有兩個(gè)時(shí)間戳它們不是一回事。CaptureResult.SENSOR_TIMESTAMP是傳感器曝光開(kāi)始時(shí)刻來(lái)自相機(jī) HAL 的時(shí)間基準(zhǔn)SystemClock.elapsedRealtimeNanos()是應(yīng)用層收到回調(diào)的時(shí)刻。后者的作用是提供一條“換算橋”離線(xiàn)處理時(shí)用兩種時(shí)間戳的對(duì)應(yīng)關(guān)系擬合出偏移量再統(tǒng)一到單調(diào)時(shí)鐘。不要指望SENSOR_TIMESTAMP直接能用它和 IMU 數(shù)據(jù)的時(shí)基在不同設(shè)備上有不同表現(xiàn)。落盤(pán)之后校驗(yàn)幀率是否穩(wěn)定再談對(duì)齊。frames.csv行數(shù)應(yīng)該接近“錄制秒數(shù) × 幀率”時(shí)間戳差值的中位數(shù)應(yīng)該等于幀間隔。如果發(fā)現(xiàn)大量時(shí)間戳差值超過(guò)兩倍幀間隔說(shuō)明錄制過(guò)程中存在掉幀最好重錄而不是指望算法去修復(fù)。掉幀對(duì)視覺(jué) SLAM 是嚴(yán)重的輸入質(zhì)量問(wèn)題輕則軌跡抖動(dòng)重則尺度漂移。4.2 棋盤(pán)格標(biāo)定腳本用 OpenCV 解出內(nèi)參與畸變時(shí)間戳對(duì)齊只是第一步還得知道相機(jī)內(nèi)參。VideoIMUCapture-Android 輸出的camera_params.json里的內(nèi)參矩陣和畸變系數(shù)一般是錄制一段棋盤(pán)格視頻后離線(xiàn)算出來(lái)的。先抽幀mkdir -p frames ffmpeg -i video.mp4 -vf fps8 frames/%04d.jpg抽幀頻率建議在 5-8 fps太密了相鄰幀的棋盤(pán)格角度幾乎一樣對(duì)求解沒(méi)有增量信息。完成后用下面的 OpenCV 腳本做標(biāo)定import cv2 import json import glob chessboard (9, 6) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) pattern_pts np.zeros((np.prod(chessboard), 3), np.float32) pattern_pts[:, :2] np.indices(chessboard).T.reshape(-1, 2) obj_pts, img_pts [], [] frames sorted(glob.glob(frames/*.jpg)) for fname in frames: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) found, corners cv2.findChessboardCorners( gray, chessboard, None ) if found: obj_pts.append(pattern_pts) corners cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria ) img_pts.append(corners) ret, K, dist, rvecs, tvecs cv2.calibrateCamera( obj_pts, img_pts, gray.shape[::-1], None, None ) print(json.dumps({ camera_matrix: K.tolist(), dist_coeffs: dist.tolist() }, indent2))腳本做的事情是遍歷所有抽出的幀用findChessboardCorners找棋盤(pán)格內(nèi)角點(diǎn)找到就用cornerSubPix做亞像素精化最后喂給calibrateCamera解出內(nèi)參矩陣 K 和畸變系數(shù)。這里我沒(méi)寫(xiě)全import numpy as np你跑腳本時(shí)補(bǔ)上即可。幾個(gè)參數(shù)需要按你的棋盤(pán)調(diào)chessboard (9, 6)是內(nèi)角點(diǎn)數(shù)不是棋盤(pán)的格子總數(shù)買(mǎi)棋盤(pán)時(shí)商家標(biāo)的是格數(shù)內(nèi)角點(diǎn)要各減 1。如果一幀都沒(méi)檢測(cè)到先檢查棋盤(pán)是不是反光材質(zhì)或者抽幀分辨率夠不夠。成功檢測(cè)的幀數(shù)至少要 20 張并且要覆蓋畫(huà)面的中心、邊緣和俯仰角度邊緣區(qū)域的角點(diǎn)對(duì)畸變系數(shù)收斂尤其重要。4.3 IMU 與相機(jī)的對(duì)齊檢查錄制自檢三要素相機(jī)內(nèi)參有了最后還要確認(rèn) IMU 和視頻之間是否真的能對(duì)上。SLAM 后端一般有一個(gè) imu-camera 外參標(biāo)定過(guò)程但外參標(biāo)定之前你得先保證兩路數(shù)據(jù)的對(duì)齊是可解的。我自己的習(xí)慣是錄制時(shí)安排三個(gè)固定動(dòng)作回放時(shí)快速判斷數(shù)據(jù)質(zhì)量而不是錄完就急著跑算法。這三個(gè)動(dòng)作分別是靜止 3 秒、繞豎直軸緩慢轉(zhuǎn)一圈、左右快速擺頭。用一個(gè)小腳本檢查import pandas as pd imu pd.read_csv(imu.csv) frames pd.read_csv(frames.csv, headerNone, names[sensor_ts, wall_ts]) static imu.iloc[:1000] # 開(kāi)始靜止段 print(gyro mean:, static[[gyro_x, gyro_y, gyro_z]].mean()) frame_dt_ns frames[sensor_ts].diff().median() print(median frame interval ms:, frame_dt_ns / 1_000_000) print(imu first ts:, imu[ts_ns].iloc[0]) print(frame first ts:, frames[sensor_ts].iloc[0])第一個(gè)輸出檢查陀螺儀零偏靜止時(shí)陀螺均值應(yīng)該在 0.02 rad/s 以?xún)?nèi)超出這個(gè)量級(jí)說(shuō)明設(shè)備自身零偏大或靜止段混入了手抖。第二個(gè)輸出檢查幀間隔720p 30fps 的視頻幀間隔中位數(shù)應(yīng)該是 33.33 ms波動(dòng)超過(guò) 20% 就要懷疑掉幀。第三個(gè)輸出只是粗略看兩路起始時(shí)間差差得太多會(huì)影響標(biāo)定收斂。這套自檢十分鐘內(nèi)能做完能篩掉大半錄制返工的情況。5. 數(shù)據(jù)不對(duì)齊先排查這 5 個(gè)采集坑采集工具用起來(lái)不難但數(shù)據(jù)問(wèn)題幾乎都出在不起眼的地方。下面五條是我在不同設(shè)備上踩過(guò)的坑每條都按“現(xiàn)象、原因、解決”整理你錄制前對(duì)照著檢查一遍能省下好幾輪無(wú)效實(shí)驗(yàn)。5.1 畫(huà)面越穩(wěn)軌跡越飛OIS 未關(guān)閉現(xiàn)象錄出來(lái)的視頻在手機(jī)上看非常穩(wěn)定但放進(jìn) ORB-SLAM3 跑軌跡在 10 秒內(nèi)就偏到畫(huà)面外。原因手機(jī)相機(jī)默認(rèn)開(kāi)啟了 OIS鏡頭在每一幀都在做微小平移等價(jià)于內(nèi)參矩陣不斷變化而 SLAM 后端完全不知道這種變化只能把誤差算進(jìn)位姿。解決創(chuàng)建CaptureRequest時(shí)顯式設(shè)置LENS_OPTICAL_STABILIZATION_MODE_OFF同時(shí)把CONTROL_VIDEO_STABILIZATION_MODE_OFF也設(shè)上。不要只關(guān)一個(gè)個(gè)別廠商的驅(qū)動(dòng)會(huì)“好心”地把另一個(gè)模式也改掉。錄制開(kāi)始前再?gòu)腃aptureResult里讀一次兩個(gè)模式值確認(rèn)不是設(shè)置后未生效。5.2 IMU 條數(shù)與幀數(shù)對(duì)不上時(shí)鐘域混用現(xiàn)象IMU 數(shù)據(jù)看起來(lái)是連續(xù)采的視頻也正常錄完但拿時(shí)間戳比較時(shí)發(fā)現(xiàn)兩端的時(shí)間基準(zhǔn)是錯(cuò)開(kāi)的整條數(shù)據(jù)對(duì)不齊。原因IMU 的event.timestamp與 Camera2 的SENSOR_TIMESTAMP不一定同一時(shí)基直接把兩個(gè)值做差會(huì)得到無(wú)意義的結(jié)果。解決所有落盤(pán)時(shí)間戳統(tǒng)一用SystemClock.elapsedRealtimeNanos()傳感器原始時(shí)間戳另存?zhèn)溆?。如果已?jīng)錄錯(cuò)了可以在離線(xiàn)階段用兩路時(shí)間戳做線(xiàn)性擬合求出偏移但遇到驅(qū)動(dòng)時(shí)鐘跳變的設(shè)備就得重錄。5.3 畫(huà)面偏色或過(guò)曝自動(dòng)白平衡和自動(dòng)曝光在后臺(tái)改參數(shù)現(xiàn)象錄制前期畫(huà)面正常中段開(kāi)始偏綠或過(guò)曝而且色彩突變的時(shí)間點(diǎn)與手機(jī)轉(zhuǎn)動(dòng)的時(shí)刻重合。原因沒(méi)有鎖 AWB 和 AE相機(jī)的自動(dòng)算法根據(jù)場(chǎng)景動(dòng)態(tài)調(diào)整了增益和白平衡導(dǎo)致相鄰幀之間的色彩和亮度響應(yīng)不一致。解決在CaptureRequest.Builder里把CONTROL_AE_MODE設(shè)為CONTROL_AE_MODE_OFFCONTROL_AWB_MODE設(shè)為CONTROL_AWB_MODE_OFF然后手動(dòng)固定SENSOR_EXPOSURE_TIME和SENSOR_SENSITIVITY。注意鎖參數(shù)要在采集前做不要在錄制中途改否則會(huì)出現(xiàn)一幀跳變。5.4 陀螺儀數(shù)據(jù)全為 0傳感器監(jiān)聽(tīng)被系統(tǒng)靜默回收現(xiàn)象錄制結(jié)束時(shí)看imu.csv前幾分鐘有數(shù)據(jù)后面全是一串 0 或沒(méi)有新行。原因部分手機(jī)上傳感器監(jiān)聽(tīng)器沒(méi)有持有喚醒鎖系統(tǒng)在喚醒與休眠切換的邊界直接把監(jiān)聽(tīng)掛起或者進(jìn)程被低內(nèi)存回收Listener 成了空殼。解決錄制期間拿PARTIAL_WAKE_LOCK保持 CPU 喚醒并且在 Service 而不是 Activity 里持有傳感器監(jiān)聽(tīng)。錄制停止時(shí)再釋放鎖不要用應(yīng)用進(jìn)入后臺(tái)就被清理的組件持有 IMU 數(shù)據(jù)鏈路。5.5 MediaRecorder 掉幀碼率設(shè)太高與 IO 阻塞現(xiàn)象視頻文件的幀時(shí)間戳間隔分布不均勻中間出現(xiàn)明顯斷層但界面預(yù)覽看不出問(wèn)題。原因MediaRecorder 的編碼器跟不上寫(xiě)入速度或者手機(jī)存儲(chǔ)寫(xiě)入遇到瓶頸內(nèi)部緩沖區(qū)溢出后直接丟幀。解決先把碼率降到 12-20 Mbps分辨率控制在 720p。另外確保輸出目錄在設(shè)備內(nèi)置存儲(chǔ)而不是慢速 SD 卡錄制過(guò)程中不要同時(shí)做大量文件拷貝。如果懷疑編碼器問(wèn)題換一臺(tái)芯片更新的設(shè)備對(duì)比測(cè)試同一場(chǎng)景能快速定位卡點(diǎn)。6. 進(jìn)階一次三分鐘自檢讓 SLAM 軌跡不再飛錄制的最后一環(huán)是把這套采集鏈路變成你實(shí)驗(yàn)流程里的標(biāo)準(zhǔn)動(dòng)作。我現(xiàn)在每次正式采集前都會(huì)做一個(gè)三分鐘的自檢流程跑完再去碰算法事半功倍。自檢流程分三段錄制第 1 段手機(jī)平放在桌面上靜止 3 秒第 2 段手持手機(jī)對(duì)著紋理豐富的墻面緩慢平移加轉(zhuǎn)動(dòng)第 3 段快速左右甩動(dòng)手機(jī) 10 次。錄完立刻檢查三組指標(biāo)檢查段檢查指標(biāo)正常范圍靜止段陀螺儀均值各軸小于 0.02 rad/s平移段幀時(shí)間戳中位數(shù)30fps 對(duì)應(yīng) 33.3 ms波動(dòng)在 10% 內(nèi)甩動(dòng)段幀時(shí)間戳連續(xù)性無(wú)超過(guò)兩倍幀間隔的斷層這三組指標(biāo)分別對(duì)應(yīng) IMU 零偏、錄制幀率穩(wěn)定性、以及快速運(yùn)動(dòng)下編碼器是否丟幀。零偏大說(shuō)明設(shè)備本身質(zhì)量不行數(shù)據(jù)錄出來(lái)也會(huì)帶著系統(tǒng)性誤差幀間隔不穩(wěn)說(shuō)明編碼器或存儲(chǔ)跟不上甩動(dòng)段斷層說(shuō)明在高速場(chǎng)景下采集鏈路存在瓶頸。任何一項(xiàng)不過(guò)關(guān)都不建議直接進(jìn)入算法實(shí)驗(yàn)。除了數(shù)據(jù)自檢還有一個(gè)技巧很實(shí)用在正式場(chǎng)景錄制前先對(duì)著一個(gè)打印的棋盤(pán)格錄 20 秒。這段視頻不參與 SLAM 實(shí)驗(yàn)只用來(lái)反復(fù)確認(rèn)內(nèi)參標(biāo)定的穩(wěn)定性。如果兩次標(biāo)定的焦距偏差超過(guò) 0.5%說(shuō)明相機(jī)設(shè)備狀態(tài)有變化比如鏡頭模塊松動(dòng)或溫漂這時(shí)候要排查設(shè)備而不是改算法參數(shù)。我在此前項(xiàng)目里就吃過(guò)標(biāo)定漂移的虧后來(lái)把這一步固定成采集前置動(dòng)作再?zèng)]出現(xiàn)過(guò)“算法調(diào)了三天最后發(fā)現(xiàn)是標(biāo)定不對(duì)”的情況。這條鏈路走順之后VideoIMUCapture-Android 的意義才會(huì)真正體現(xiàn)出來(lái)它不只是一個(gè)錄像工具而是一套把視頻、IMU、相機(jī)參數(shù)和 OIS 狀態(tài)對(duì)齊的數(shù)據(jù)采集方案。愿你少踩幾個(gè)采集坑錄出來(lái)的數(shù)據(jù)一次就能跑通。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取