光流为追踪源图像某个点在其他图像中的运动, 一般分为稀疏光流和稠密光流,其本质是估计像素在不同时刻图像中的运动

本文主要介绍LK光流。
设 t 时刻位于 x,y 处像素点的灰度值为
I
(
x
,
y
,
t
)
I(x,y,t)
I(x,y,t),在 t+dt 时刻,该像素运动到了
I
(
x
+
d
x
,
y
+
d
y
,
t
+
d
t
)
I(x+dx,y+dy,t+dt)
I(x+dx,y+dy,t+dt),光流希望计算运动 dx, dy。这里引入灰度不变假设
I
(
x
+
d
x
,
y
+
d
y
,
t
+
d
t
)
I(x+dx,y+dy,t+dt)
I(x+dx,y+dy,t+dt) =
I
(
x
,
y
,
t
)
I(x,y,t)
I(x,y,t)(注:实际当中由于相机曝光、阴影、材质等不同,很可能不成立)。对 t+dt 时刻的灰度进行Taylor展开并保留一阶项:

由于灰度不变,所以

因此,两边除以
d
t
dt
dt

希望求解
d
x
/
d
t
dx/dt
dx/dt,
d
y
/
d
t
dy/dt
dy/dt,但本式是一个二元一次线性方程,欠定,因此,需要引用额外的约束,这里假定一个窗口[w,w]内光度不变,则可以通过超定最小二乘解求得运动 u,v。


LK光流的结果依赖于图像梯度,但梯度不够平滑,可能剧烈变化,局部的梯度不能用于预测长期图像走向,因此采用图像金字塔,通过coarse to fine的方式进行跟踪。

LK光流问题可以看成最小化像素误差的非线性优化,每次使用了Taylor一阶近似,在离优化点较远时效果不佳,因此往往需要迭代多次,并且对于快速运动来说可以通过IMU或者patch匹配提供先验,在先验的基础上再进行光流跟踪。
按算法分类,有两种分法,一种可以分为迭加法或组合法(additive or compositional),向前或者反向算法( forwards or inverse )
(有人翻译:叠加式(additive)和构造式(compositional),前向(forwards),逆向(inverse))。从而导出四种图像对齐算法( all four image alignment algorithms ),分别是
设有图像 1.png,2.png,我们在 1.png 中提取了 GFTT 角点 ,然后希望在 2.png中追踪这些关键点。设两个图分别为
I
1
I_1
I1 ,
I
2
I_2
I2 ,第一张图中提取的点集为
P
=
p
i
P = {p_i }
P=pi,其中
p
i
=
[
x
i
,
y
i
]
T
p_i = [ x_i , y_i ]^T
pi=[xi,yi]T为像素坐标值。考虑第 i 个点,我们希望计算 ∆
x
i
x_i
xi , ∆
y
i
y_i
yi ,满足:

for (int x = -half_patch_size; x < half_patch_size; x++) {
for (int y = -half_patch_size; y < half_patch_size; y++) {
// TODO START YOUR CODE HERE (~8 lines)
float p_x = kp.pt.x;
float p_y = kp.pt.y;
int k = x + y + 2 * half_patch_size;
double error = 0;
Eigen::Vector2d J; // Jacobian
if (inverse == false) {// Forward Jacobian
J[0] = GetPixelValue(img2, p_x + x, p_y + y) - GetPixelValue(img2, p_x + x - 1, p_y + y);
J[1] = GetPixelValue(img2, p_x + x, p_y + y) - GetPixelValue(img2, p_x + x, p_y + y - 1);
} else {// Inverse Jacobian
J[0] = GetPixelValue(img1, p_x + x, p_y + y) - GetPixelValue(img1, p_x + x - 1, p_y + y);
J[1] = GetPixelValue(img1, p_x + x, p_y + y) - GetPixelValue(img1, p_x + x, p_y + y - 1);
// NOTE this J does not change when dx, dy is updated, so we can store it and only compute error
}
// compute H, b and set cost; //H = JT * J
H += J * J.transpose();
error = GetPixelValue(img2, p_x + x+ dx, p_y + y + dy) - GetPixelValue(img1, p_x + x, p_y + y);
b += -error * J;
cost += error * error / 2;
}
}
上面只是最简单的一种实现方式,认为窗口内像素只有平移运动,没有旋转变换,实际使用时我们会将cost function模型化为下面这种形式:

其中
I
I
I表示
I
2
I_2
I2目标图像,
T
T
T表示提取特征点的模板图像,
W
(
x
,
p
)
W(x,p)
W(x,p)为warp函数,最简单的warp就是:

此时认为窗口内像素块只有平移运动,但如果窗口内的像素存在旋转,那么两个像素块的匹配,只是
p
1
,
p
2
p_1,p_2
p1,p2就无法表达,需要使用下面仿射变换:

Lucas-Kanade算法假设已知p的当前估计值,然后迭代地求解参数p的增量;也就是说,以下表达式被(近似地)最小化

然后通过如下方式更新:

GN方法求解增量为:

H为:

上面warp对应jacobian为:

在迭代开始时, Gauss-Newton 的计算依赖于
I
2
I_2
I2 在
(
x
i
+
∆
x
i
,
y
i
+
∆
y
i
)
(x_i +∆x i , y_i + ∆y i )
(xi+∆xi,yi+∆yi) 处的梯度信息。然而,角点提取算法仅保证了
I
1
(
x
i
,
y
i
)
I_1 (x_i , y_i )
I1(xi,yi) 处是角点(可以认为角度点存在明显梯度),但对于
I
2
I_2
I2 ,我们并没有办法假设
I
2
I_2
I2 在
x
i
,
y
i
x_i , y_i
xi,yi 处亦有梯度,从而 Gauss-Newton 并不一定成立。反向的光流
法(inverse)则做了一个巧妙的技巧,即用
I
1
(
x
i
,
y
i
)
I_1 (x_i , y_i)
I1(xi,yi) 处的梯度,替换掉原本要计算的
I
2
(
x
i
+
∆
x
i
,
y
i
+
∆
y
i
)
I_2 (x_i + ∆x i , y_i + ∆y i)
I2(xi+∆xi,yi+∆yi)的梯度。这样做的好处有:
具体代码实现,上面的inverse为true时,即对应inverse的方法。
实验结果:正向法和反向法在计算中体现在计算关键点在 x i , y i x_i,y_i xi,yi处的梯度上,正向法采用当前追踪图像 I 2 I_2 I2的图像像素梯度,反向法使用 I 1 I_1 I1的像素梯度。通常单层图像的反向法比正向法效果要好。
for (int x = -half_patch_size; x < half_patch_size; x++) {
for (int y = -half_patch_size; y < half_patch_size; y++) {
// TODO START YOUR CODE HERE (~8 lines)
float p_x = kp.pt.x;
float p_y = kp.pt.y;
int k = x + y + 2 * half_patch_size;
double error = 0;
Eigen::Vector2d J; // Jacobian
inverse = true;
if (inverse == false) {// Forward Jacobian
J[0] = GetPixelValue(img2, p_x + x, p_y + y) - GetPixelValue(img2, p_x + x - 1, p_y + y);
J[1] = GetPixelValue(img2, p_x + x, p_y + y) - GetPixelValue(img2, p_x + x, p_y + y - 1);
} else {// Inverse Jacobian
J[0] = GetPixelValue(img1, p_x + x, p_y + y) - GetPixelValue(img1, p_x + x - 1, p_y + y);
J[1] = GetPixelValue(img1, p_x + x, p_y + y) - GetPixelValue(img1, p_x + x, p_y + y - 1);
// NOTE this J does not change when dx, dy is updated, so we can store it and only compute error
}
// compute H, b and set cost; //H = JT * J
H += J * J.transpose();
error = GetPixelValue(img2, p_x + x+ dx, p_y + y + dy) - GetPixelValue(img1, p_x + x, p_y + y);
b += -error * J;
cost += error * error / 2;
}
}
相对于additive的目标函数:


compositional的目标函数为:


也就是说,组合方法迭代地求解增量warp W(x; p),而不是参数p的加法更新。compositional方法和additive方法在一阶p近似中如下等式成立,即:


展开后为:

compositional操作通常涉及到乘两个矩阵来计算组合warp的参数,就像上式中的仿射warp一样。对于更复杂的warp,两个warp的组合可能更为复杂。因此,我们对warp集合有两个要求:(1)warp集合必须包含identity warp;(2)warp集合必须在组合下是封闭的。因此,变形集合必须形成一个半群。这个要求并不难达到,大多数在计算机视觉中使用的warp,包括单应性和3D旋转,自然就形成了半群。
cost function为

更新为:

其中warp的inverse为:

可以看到,向前方法对于输入
I
I
I图像进行参数化(包括warp变换及warp增量). 反向方法则同时参数输入
I
I
I图像和模板图像, 其中输入
I
I
I图像参数化warp变换, 模板图像参数化warp增量. 因此反向方法的计算量显著降低.。由于图像灰度值和运动参数非线性, 整个优化过程为非线性。
光流其实和直接法很相似,直接法或者半直接法更像进化版的光流,主要是光流仅估计了像素间的平移,或者warp变换,但
假设有两个帧,运动未知,但有初始估计 R,t,第1帧上看到了点P,投影为p1,按照初始估计,P在第2帧上投影为p2,投影关系为


在光流中,我们估计的是每个像素的平移(在 additive 的情况下),而在直接法当中,我们最小化光流误差,来估计相机的旋转和平移,因此建立如下最小二乘问题,最小化光度误差

这里待估计的量为相机运动,因此需要计算误差相对于相机位姿的导数,jacobian计算就是标准的链式求导:

为简化表示,令:


三部分连乘:
第一部分:图像梯度
第二部分:像素对投影点的导数

第三部分:投影点对位姿的导数


参考文献:Lucas-Kanade 20 Years On: A Unifying Framework