本文所有代碼公開於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 博客)
自己的解釋
光流經常出現在視頻或圖像序列(多張圖像)中,用來刻畫運動物體(相機或被觀察的物體)瞬時的運動轉態(運動方向和運動偏移量)
光流的表示
光流的表示也是數字化的。它一般使用一個三維的數組()表示,其中表示圖像的高度,也就是數組中的行數,表示圖像的寬度,也就是數組中的列數,表示兩個方向。
直白地解釋:在光流數組的第三維上,第一通道(即)表示圖像在方向的偏移方向和大小。這裏的方向是水平方向,即圖像數組中的行向量方向;
第二通道(即)表示圖像在方向的偏移方向和大小。這裏的方向是豎直方向,即圖像數組中的列向量方向。
這裏還要注意的一點:偏移量的大小當然就是通過光流數組中的數值大小體現出來的,而偏移的方向是通過光流數組中的正負體現出來的。在方向上,正值表示物體向左移動,而負值表示物體向右移動;在方向上,正值表示物體向上移動,而負值表示物體向下移動。 至於爲什麼是這樣的,後面我們在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爲,如下圖(光流的可視化下面講解):
也可以看看這個光流數組裏面對應的值到底是什麼:
# 第一通道
[[ 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
所以這裏就不過多介紹了,能用就行~~
這裏只放一個該代碼的運行結果,還是上面展示的那個光流,唯一區別就是這裏的光流數組大小爲, 這個也是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)軸反轉,軸移到頂部,這是爲了符合我們對數組的習慣,因爲numpy數組都是從左上角開始的,不是matplotlib中的默認左下角
4)第三行:flow[:,:,0] = -flow[:,:,0],爲什麼要對第一通道取反?這是爲了滿足光流的特性,也就是 在方向上,正值表示物體向左移動,而負值表示物體向右移動;在方向上,正值表示物體向上移動,而負值表示物體向下移動。 這與matplotlib的特性相反,所以要取反,那爲什麼第二通道不取反呢,那是因爲在做軸反轉時,這個取反的操作就相當於已經做了
光流映射(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)現在來解釋這句話:在方向上,正值表示物體向左移動,而負值表示物體向右移動;在方向上,正值表示物體向上移動,而負值表示物體向下移動。 是因爲第54和第55行:x0 = pos_x + x, y0 = pos_y + y。簡單解釋一下這幾個變量:pos_x和pos_y是原始的像素座標,x和y是光流(向下取整),x0和y0就是warp後的像素座標。以方向爲例,原始座標加上一個負值,得到的結果變小了,也就相當於這個像素像左移了!如果加一個正值,結果變大,像素右移。
2)參數mode:可選的有兩種分別是nearest 或者 bilinear。 就是兩種插幀方式,爲什麼需要插值?是因爲座標變換後,很多座標上並沒有相應的原始像素與之對應,需要通過插值來處理
最後放兩個所有的代碼綜合的例子:
1)中心膨脹
2)向右下角偏移:
這兩個例子的全部代碼以公佈在github上:
https://github.com/weihuang527/optical-flow