本文系微信公众号《MediaStack》原创文章,每周不定期更新,欢迎大家关注,随时进行交流。
有人在后台里问:用大疆的设备采集视频导出陀螺仪数据尝试防抖算法,经过很多次操作后,最后视频还是很抖??当时想的是,防抖嘛,不就是读陀螺仪数据、做四元数平滑、算个remap map、用OpenCV重映射?能有多难。2周之后,我看着目录下二十多个Python文件和上百个测试视频,意识到这个"不难"的初判断让自己又学到不少东西。最初版:能跑,但画面在抖
最早的版本gyro_eis.py,132行的代码里做了以下几件事:- 输入: 原始视频 + 同步的陀螺仪数据(CSV)。
- 生成参考系: 把抖动的镜头轨迹“磨平”,得到一个理想的平滑轨迹。
- 计算修正量: 每一帧画面的修正量 = 理想位置 - 实际位置。
也提供了一个视频,但是视频是从Gyroflow导出来的,并没有包含imu等数据(这个是后来分析一下视频才知道的,也浪费了一点时间来验证)。当时卡住的第一个点是时间对齐。DJI Osmo Nano的陀螺仪采样率约1000Hz,视频帧率30fps,理论上每帧对应~33个IMU采样点。但实际跑出来的画面有明显的水平抖动——不是算法问题,是帧中心时间戳和实际曝光时间有约22ms的偏移。修了这个偏移之后,增加了一些时间同步之后,画面稳定了不少,但还不够好。第二版:加了滚动快门,效果反而更差
第二个版本开始引入滚动快门校正。方案是分条带(strip)——把一帧画面分成N个水平条带,每个条带查询该行实际读取时刻对应的陀螺仪姿态,分别做变换。条带数从16开始试。16条带的效果比第一版好,但画面有明显的水平条纹感——相邻条带之间不连续。
加到36条带,条纹感消失。
加到108条带(每10行一个条带),终于和Gyroflow的效果接近了。
但这时候出了另一个问题:四元数公式的符号。于是参考Gyroflow源码里frame_transform.rs的校正公式是:smoothed_q * inv(org_q_center) * org_q_row,
而我们第一版写的是inv(stab_q) * org_q。看起来差不多,但坐标系不对——Gyroflow的旋转矩阵有一个flip_y() 操作,要把R的特定行列取反(R[0,1]*=-1; R[0,2]*=-1; R[1,0]*=-1; R[2,0]*=-1)。
现在反思一下:代码还是需要细嚼慢咽,慢慢理解才行呀。这个符号错误导致画面在俯仰方向是反的。表现为:镜头往下,画面往上;快速倾斜时画面像被"弹"了一下。修了flip_y之后,108条带+正确的四元数公式,防抖效果终于说得过去了。但和Gyroflow的输出做帧对帧对比(写了一个脚本evaluate.py来计算MSE),差异还在5-8%左右。第三版:把Gyroflow的渲染管线翻译过来
剩下的 5-8% 差异来自哪里?重新对比了一下Gyroflow源码的GPU渲染管线(stabilize.rs+lens.rs),发现两个之前完全忽略的东西。之前的版本用cv2.fisheye.initUndistortRectifyMap一步到位,然而在Gyroflow的流程是三步:把输出像素通过内参矩阵K逆投影到3D空间
应用旋转变换
用Newton法迭代求解鱼眼前向畸变模型,再乘以内参得到源像素坐标
关键区别在于:OpenCV的initUndistortRectifyMap假设了特定的畸变模型参数化方式,而Gyroflow用的是多段式多项式拟合(poly3/poly5/PTLens 等多种模型),前向畸变用 Newton迭代求解,精度更高,于是继续迭代。最新版gyroflow_python.py有881多行(含批量处理和完整注释),实现了Gyroflow渲染pipeline的Python实现:四元数运算(乘法、逆、SLERP、旋转矩阵)
Gyroflow Default平滑算法(速度自适应、双向滤波)
水平锁定(Horizon Lock)
逐行滚动快门校正
多模型鱼眼畸变(前向 + 逆向)
自适应FOV(内接矩形 + 包络跟随)
批量矩阵计算
这时候效果就可以接受了。
当然这个结果也不是最好的,还有很多可以继续优化的地方,待我有时间时候,继续迭代,做个通用版本。
好了本文集就介绍到这里了。
我是一枚爱跑步的程序猿,维护公众号和知乎专栏《MediaStack》,有兴趣可以关注,一起学习音视频知识,时不时分享实战经验。