光流为追踪源图像某个点在其他图像中的运动, 一般分为稀疏光流和稠密光流,其本质是估计像素在不同时刻图像中的运动
设 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;
}
}
也就是说,组合方法迭代地求解增量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增量. 因此反向方法的计算量显著降低.。由于图像灰度值和运动参数非线性, 整个优化过程为非线性。
参考文献:Lucas-Kanade 20 Years On: A Unifying Framework