一文搞懂光流 光流的生成,可視化以及映射(backward warp)

本文所有代碼公開於github:
https://github.com/weihuang527/optical-flow

什麼是光流

官方定義

Optical flow or optic flow is the pattern of apparent motion of objects, surfaces, and edges in a visual scene caused by the relative motion between an observer and a scene. Optical flow can also be defined as the distribution of apparent velocities of movement of brightness pattern in an image. (from 維基百科

其他解釋:
光流是空間運動物體在觀察成像平面上的像素運動的瞬時速度,是利用圖像序列中像素在時間域上的變化以及相鄰幀之間的相關性來找到上一幀跟當前幀之間存在的對應關係,從而計算出相鄰幀之間物體的運動信息的一種方法。一般而言,光流是由於場景中前景目標本身的移動、相機的運動,或者兩者的共同運動所產生的。(from 博客)

光流表示的是相鄰兩幀圖像中每個像素的運動速度和運動方向。(from 博客

自己的解釋

光流經常出現在視頻或圖像序列(多張圖像)中,用來刻畫運動物體(相機或被觀察的物體)瞬時的運動轉態(運動方向和運動偏移量)

光流的表示

光流的表示也是數字化的。它一般使用一個三維的數組([height,width,2][height, width, 2])表示,其中heightheight表示圖像的高度,也就是數組中的行數,widthwidth表示圖像的寬度,也就是數組中的列數,22表示x,yx, y兩個方向。

直白地解釋:在光流數組的第三維上,第一通道(即[height,width,0][height, width, 0])表示圖像在xx方向的偏移方向和大小。這裏的xx方向是水平方向,即圖像數組中的行向量方向;
第二通道(即[height,width,1][height, width, 1])表示圖像在yy方向的偏移方向和大小。這裏的yy方向是豎直方向,即圖像數組中的列向量方向。

這裏還要注意的一點:偏移量的大小當然就是通過光流數組中的數值大小體現出來的,而偏移的方向是通過光流數組中的正負體現出來的。xx方向上,正值表示物體向左移動,而負值表示物體向右移動;在yy方向上,正值表示物體向上移動,而負值表示物體向下移動。 至於爲什麼是這樣的,後面我們在backward warp中的源碼中進行解釋。

光流的生成

光流提取

爲了提取光流,一般就需要輸入視頻中的相鄰兩幀,或者圖像序列中的相鄰兩張圖像,然後通過算法提取出光流。算法包括傳統方法也有目前基於深度學習的方法,比如flownet。由於提取光流的算法不是本文的重點,這裏就不進行贅述。光流的提取方法也已經有很多優秀的博文進行了介紹,大家可以去參考,這裏我也給出一些較優秀博文的鏈接:
https://zhuanlan.zhihu.com/p/74460341
https://blog.csdn.net/qq_41368247/article/details/82562165
https://my.oschina.net/u/3702502/blog/1815343
https://www.cnblogs.com/sddai/p/10275837.html
https://blog.csdn.net/carson2005/article/details/7581642

生成光流

爲了能真實感受光流,以及它的格式。這裏寫了一個小代碼來生成一個由一個點向四周擴散的光流,這裏的光流數組的shape爲[11,11,2][11, 11, 2],如下圖(光流的可視化下面講解):
光流可視化
也可以看看這個光流數組裏面對應的值到底是什麼:

# 第一通道
[[ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]
 [ 2.5  2.   1.5  1.   0.5  0.  -0.5 -1.  -1.5 -2.  -2.5]]
# 第二通道
[[ 2.5  2.5  2.5  2.5  2.5  2.5  2.5  2.5  2.5  2.5  2.5]
 [ 2.   2.   2.   2.   2.   2.   2.   2.   2.   2.   2. ]
 [ 1.5  1.5  1.5  1.5  1.5  1.5  1.5  1.5  1.5  1.5  1.5]
 [ 1.   1.   1.   1.   1.   1.   1.   1.   1.   1.   1. ]
 [ 0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5]
 [ 0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0. ]
 [-0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5]
 [-1.  -1.  -1.  -1.  -1.  -1.  -1.  -1.  -1.  -1.  -1. ]
 [-1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5]
 [-2.  -2.  -2.  -2.  -2.  -2.  -2.  -2.  -2.  -2.  -2. ]
 [-2.5 -2.5 -2.5 -2.5 -2.5 -2.5 -2.5 -2.5 -2.5 -2.5 -2.5]]

同時再附上生成這個簡單光流的代碼:

def gen_flow_circle(center, height, width):
    x0, y0 = center
    if x0 >= height or y0 >= width:
        raise AttributeError('ERROR')
    flow = np.zeros((height, width, 2), dtype=np.float32)

    grid_x = np.tile(np.expand_dims(np.arange(width), 0), [height, 1])
    grid_y = np.tile(np.expand_dims(np.arange(height), 1), [1, width])

    grid_x0 = np.tile(np.array([x0]), [height, width])
    grid_y0 = np.tile(np.array([y0]), [height, width])

    flow[:,:,0] = grid_x0 - grid_x
    flow[:,:,1] = grid_y0 - grid_y

    return flow

if __name__ == "__main__":
    # Function: gen_flow_circle
    center = [5, 5]
    flow = gen_flow_circle(center, height=11, width=11)
    flow = flow / 2 # 改變光流的值,也就是改變像素的偏移量,這個不重要

也有其他的光流生成方式:
https://www.cnblogs.com/xianhan/p/10401442.html

光流的可視化

稠密光流可視化

光流的可視化代碼摘抄於博客:
https://blog.csdn.net/qq_34535410/article/details/89976801

所以這裏就不過多介紹了,能用就行~~
這裏只放一個該代碼的運行結果,還是上面展示的那個光流,唯一區別就是這裏的光流數組大小爲[101,101,2][101, 101, 2], 這個也是Color wheel,它的作用就是給你一個由該代碼生成的光流可視化圖,你參考這個Color wheel就會知道物體的偏移方向和大小,例如綠色就代表往右上角偏移,而顏色的深度就表示偏移的大小:
稠密光流可視化

稀疏光流可視化

先碼代碼:

def sparse_flow(flow, X=None, Y=None, stride=1):
    flow = flow.copy()
    flow[:,:,0] = -flow[:,:,0]
    if X is None:
        height, width, _ = flow.shape
        xx = np.arange(0,height,stride)
        yy = np.arange(0,width,stride)
        X, Y= np.meshgrid(xx,yy)
        X = X.flatten()
        Y = Y.flatten()

        # sample
        sample_0 = flow[:, :, 0][xx]
        sample_0 = sample_0.T
        sample_x = sample_0[yy]
        sample_x = sample_x.T
        sample_1 = flow[:, :, 1][xx]
        sample_1 = sample_1.T
        sample_y = sample_1[yy]
        sample_y = sample_y.T

        sample_x = sample_x[:,:,np.newaxis]
        sample_y = sample_y[:,:,np.newaxis]
        new_flow = np.concatenate([sample_x, sample_y], axis=2)
    flow_x = new_flow[:, :, 0].flatten()
    flow_y = new_flow[:, :, 1].flatten()
    
    # display
    ax = plt.gca()
    ax.xaxis.set_ticks_position('top')
    ax.invert_yaxis()
    # plt.quiver(X,Y, flow_x, flow_y, angles="xy", color="#666666")
    ax.quiver(X,Y, flow_x, flow_y, color="#666666")
    ax.grid()
    # ax.legend()
    plt.draw()
    plt.show()

這裏有幾個點需要說說:
1)參數 X 和 Y,不建議自行輸入,除非你畫圖的時候想改變數軸的範圍
2)參數stride的目的是是否對光流進行採樣,如果是1就表示不進行採樣,如果是2或者其他,就表示隔2步或者其他步進行採樣
3)yy軸反轉,xx軸移到頂部,這是爲了符合我們對數組的習慣,因爲numpy數組都是從左上角開始的,不是matplotlib中的默認左下角
4)第三行:flow[:,:,0] = -flow[:,:,0],爲什麼要對第一通道取反?這是爲了滿足光流的特性,也就是 xx方向上,正值表示物體向左移動,而負值表示物體向右移動;在yy方向上,正值表示物體向上移動,而負值表示物體向下移動。 這與matplotlib的特性相反,所以要取反,那爲什麼第二通道不取反呢,那是因爲在做yy軸反轉時,這個取反的操作就相當於已經做了

光流映射(backward warp)

backward warp我也不知道怎麼翻譯纔好,這裏我用映射來解釋,就是將生成的光流應用到一張圖像中
還是先碼代碼:

import numpy as np

def image_warp(im, flow, mode='bilinear'):
    """Performs a backward warp of an image using the predicted flow.
    numpy version

    Args:
        im: input image. ndim=2, 3 or 4, [[num_batch], height, width, [channels]]. num_batch and channels are optional, default is 1.
        flow: flow vectors. ndim=3 or 4, [[num_batch], height, width, 2]. num_batch is optional
        mode: interpolation mode. 'nearest' or 'bilinear'
    Returns:
        warped: transformed image of the same shape as the input image.
    """
    # assert im.ndim == flow.ndim, 'The dimension of im and flow must be equal '
    flag = 4
    if im.ndim == 2:
        height, width = im.shape
        num_batch = 1
        channels = 1
        im = im[np.newaxis, :, :, np.newaxis]
        flow = flow[np.newaxis, :, :]
        flag = 2
    elif im.ndim == 3:
        height, width, channels = im.shape
        num_batch = 1
        im = im[np.newaxis, :, :]
        flow = flow[np.newaxis, :, :]
        flag = 3
    elif im.ndim == 4:
        num_batch, height, width, channels = im.shape
        flag = 4
    else:
        raise AttributeError('The dimension of im must be 2, 3 or 4')

    max_x = width - 1
    max_y = height - 1
    zero = 0

    # We have to flatten our tensors to vectorize the interpolation
    im_flat = np.reshape(im, [-1, channels])
    flow_flat = np.reshape(flow, [-1, 2])

    # Floor the flow, as the final indices are integers
    flow_floor = np.floor(flow_flat).astype(np.int32)

    # Construct base indices which are displaced with the flow
    pos_x = np.tile(np.arange(width), [height * num_batch])
    grid_y = np.tile(np.expand_dims(np.arange(height), 1), [1, width])
    pos_y = np.tile(np.reshape(grid_y, [-1]), [num_batch])

    x = flow_floor[:, 0]
    y = flow_floor[:, 1]

    x0 = pos_x + x
    y0 = pos_y + y

    x0 = np.clip(x0, zero, max_x)
    y0 = np.clip(y0, zero, max_y)

    dim1 = width * height
    batch_offsets = np.arange(num_batch) * dim1
    base_grid = np.tile(np.expand_dims(batch_offsets, 1), [1, dim1])
    base = np.reshape(base_grid, [-1])

    base_y0 = base + y0 * width

    if mode == 'nearest':
        idx_a = base_y0 + x0
        warped_flat = im_flat[idx_a]
    elif mode == 'bilinear':
        # The fractional part is used to control the bilinear interpolation.
        bilinear_weights = flow_flat - np.floor(flow_flat)

        xw = bilinear_weights[:, 0]
        yw = bilinear_weights[:, 1]

        # Compute interpolation weights for 4 adjacent pixels
        # expand to num_batch * height * width x 1 for broadcasting in add_n below
        wa = np.expand_dims((1 - xw) * (1 - yw), 1) # top left pixel
        wb = np.expand_dims((1 - xw) * yw, 1) # bottom left pixel
        wc = np.expand_dims(xw * (1 - yw), 1) # top right pixel
        wd = np.expand_dims(xw * yw, 1) # bottom right pixel

        x1 = x0 + 1
        y1 = y0 + 1

        x1 = np.clip(x1, zero, max_x)
        y1 = np.clip(y1, zero, max_y)

        base_y1 = base + y1 * width
        idx_a = base_y0 + x0
        idx_b = base_y1 + x0
        idx_c = base_y0 + x1
        idx_d = base_y1 + x1

        Ia = im_flat[idx_a]
        Ib = im_flat[idx_b]
        Ic = im_flat[idx_c]
        Id = im_flat[idx_d]

        warped_flat = wa * Ia + wb * Ib + wc * Ic + wd * Id
    warped = np.reshape(warped_flat, [num_batch, height, width, channels])

    if flag == 2:
        warped = np.squeeze(warped)
    elif flag == 3:
        warped = np.squeeze(warped, axis=0)
    else:
        pass
    warped = warped.astype(np.uint8)

    return warped

1)現在來解釋這句話:xx方向上,正值表示物體向左移動,而負值表示物體向右移動;在yy方向上,正值表示物體向上移動,而負值表示物體向下移動。 是因爲第54和第55行:x0 = pos_x + x, y0 = pos_y + y。簡單解釋一下這幾個變量:pos_x和pos_y是原始的像素座標,x和y是光流(向下取整),x0和y0就是warp後的像素座標。以xx方向爲例,原始座標加上一個負值,得到的結果變小了,也就相當於這個像素像左移了!如果加一個正值,結果變大,像素右移。
2)參數mode:可選的有兩種分別是nearest 或者 bilinear。 就是兩種插幀方式,爲什麼需要插值?是因爲座標變換後,很多座標上並沒有相應的原始像素與之對應,需要通過插值來處理

最後放兩個所有的代碼綜合的例子:
1)中心膨脹
center
2)向右下角偏移:
topleft

這兩個例子的全部代碼以公佈在github上:
https://github.com/weihuang527/optical-flow

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章