?? 光流是由對象或相機的移動引起的兩個連續幀之間的圖像對象的明顯運動的模式.它是2D矢量場,其中每個矢量是位移矢量,表示從第一幀到第二幀的點的移動。
如下圖所示,是一個球在連續的5幀圖像中的運動,箭頭顯示其位移矢量.
光流有很多應用場景如下:
光流法的工作原理基於如下假設:
數學原理
?? 第一幀的像素 ,表示在 t 時刻的像素值,那麼經過 時間後,像素在下一幀移動的距離為 。基於像素相同,亮度不變,可得:
假設移動很小,使用泰勒公式得:
其中 為無窮小,由第一個假設得:
設 , ,則有得到光流方程為:
其中, 和 分別是圖像梯度, 是圖像沿著時間的梯度。
?? 為了求解未知的 ,採用Lucas-Kanada 方法解決,這個演算法最早是有Bruce D. Lucas and Takeo Kanade兩位作者提出來的,所以又被稱為KLT。
KLT演算法工作有三個假設前提條件:
?? Opencv中使用cv2.calcOpticalFlowPyrLK()函數計算一個稀疏特徵集的光流,使用金字塔中的迭代 Lucas-Kanade 方法。
cv2.calcOpticalFlowPyrLK()
nextPts,status,err = cv2.calcOpticalFlowPyrLK(prevImg, #上一幀圖片 nextImg, #當前幀圖片 prevPts, #上一幀找到的特徵點向量 nextPts #與返回值中的nextPtrs相同 [, status[, err[, winSize [, maxLevel[, criteria [, flags[, minEigThreshold]]]]]]])
輸入值:
返回值:
實現原理: - 在第一幀圖像中檢測Shi-Tomasi角點, - 使用LK演算法來迭代的跟蹤這些特徵點。迭代的方式就是不斷向cv2.calcOpticalFlowPyrLK()中傳入上一幀圖片的特徵點以及當前幀的圖片。 - 函數會返回當前幀的點,這些點帶有狀態1或者0,如果在當前幀找到了上一幀中的點,那麼這個點的狀態就是1,否則就是0。
實現流程:
cv2.GoodFeaturesToTrack
cv2.CalcOpticalFlowPyrLK
1. 刪除靜止點的光流分析:
import numpy as np import cv2 as cv import math cap = cv.VideoCapture(car_flow.mp4)
#角點檢測參數 feature_params = dict(maxCorners=100, qualityLevel=0.1, minDistance=7, blockSize=7)
#KLT光流參數 lk_params = dict(winSize=(15, 15), maxLevel=2, criteria=(cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.02))
# 隨機顏色 color = np.random.randint(0,255,(100,3))
# 讀取第一幀 ret, old_frame = cap.read() old_gray = cv.cvtColor(old_frame, cv.COLOR_BGR2GRAY) p0 = cv.goodFeaturesToTrack(old_gray, mask=None, **feature_params,useHarrisDetector=False,k=0.04) good_ini=p0.copy()
def caldist(a,b,c,d): return abs(a-c)+abs(b-d)
mask = np.zeros_like(old_frame) # 光流跟蹤 while True: ret, frame = cap.read() if ret is False: break frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) # 計算光流 p1, st, err = cv.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params) # 根據狀態選擇
good_new = p1[st == 1] good_old = p0[st == 1] #刪除靜止點 k=0 for i, (new0, old0) in enumerate(zip(good_new,good_old)): a0,b0 = new0.ravel() c0,d0 = old0.ravel() dist=caldist(a0,b0,c0,d0) if dist>2: good_new[k]=good_new[i] good_old[k]=good_old[i] good_ini[k]=good_ini[i] k=k+1
# 提取動態點 good_ini=good_ini[:k] good_new=good_new[:k] good_old=good_old[:k]
# 繪製跟蹤線 for i, (new, old) in enumerate(zip(good_new,good_old)): a,b = new.ravel() c,d = old.ravel() mask = cv.line(mask, (a,b),(c,d), color[i].tolist(), 2) frame = cv.circle(frame,(a,b),5,color[i].tolist(),-1) cv.imshow(frame,cv.add(frame,mask)) k = cv.waitKey(30) & 0xff if k == 27: cv.imwrite("flow.jpg", cv.add(frame,mask)) break
# 更新 old_gray = frame_gray.copy() p0 = good_new.reshape(-1, 1, 2)
if good_ini.shape[0]<40: p0 = cv.goodFeaturesToTrack(old_gray, mask=None, **feature_params) good_ini=p0.copy()
cv.destroyAllWindows() cap.release() 輸出
2. 反向檢測的光流分析:
height = cap.get(cv.CAP_PROP_FRAME_HEIGHT) width = cap.get(cv.CAP_PROP_FRAME_WIDTH) fps = cap.get(cv.CAP_PROP_FPS) #out = cv.VideoWriter("reslut.avi", cv.VideoWriter_fourcc(D, I, V, X), fps, #(np.int(width), np.int(height)), True)
tracks = [] track_len = 15 frame_idx = 0 detect_interval = 5 while True:
ret, frame = cap.read() if ret: frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) vis = frame.copy()
if len(tracks)>0: img0 ,img1 = prev_gray, frame_gray p0 = np.float32([tr[-1] for tr in tracks]).reshape(-1,1,2) # 上一幀的角點和當前幀的圖像作為輸入來得到角點在當前幀的位置 p1, st, err = cv.calcOpticalFlowPyrLK(img0, img1, p0, None, **lk_params)
# 反向檢查,當前幀跟蹤到的角點及圖像和前一幀的圖像作為輸入來找到前一幀的角點位置 p0r, _, _ = cv.calcOpticalFlowPyrLK(img1, img0, p1, None, **lk_params)
# 得到角點回溯與前一幀實際角點的位置變化關係 d = abs(p0-p0r).reshape(-1,2).max(-1)
#判斷d內的值是否小於1,大於1跟蹤被認為是錯誤的跟蹤點 good = d < 1
new_tracks = []
for i, (tr, (x, y), flag) in enumerate(zip(tracks, p1.reshape(-1, 2), good)):
# 判斷是否為正確的跟蹤點 if not flag: continue
# 存儲動態的角點 tr.append((x, y))
# 只保留track_len長度的數據,消除掉前面的超出的軌跡 if len(tr) > track_len: del tr[0] # 保存在新的list中 new_tracks.append(tr)
cv.circle(vis, (x, y), 2, (0, 255, 0), -1)
# 更新特徵點 tracks = new_tracks
# #以上一振角點為初始點,當前幀跟蹤到的點為終點,畫出運動軌跡 cv.polylines(vis, [np.int32(tr) for tr in tracks], False, (0, 255, 0), 1)
# 每隔 detect_interval 時間檢測一次特徵點 if frame_idx % detect_interval==0: mask = np.zeros_like(frame_gray) mask[:] = 255
if frame_idx !=0: for x,y in [np.int32(tr[-1]) for tr in tracks]: cv.circle(mask, (x, y), 5, 0, -1)
p = cv.goodFeaturesToTrack(frame_gray, mask=mask, **feature_params) if p is not None: for x, y in np.float32(p).reshape(-1,2): tracks.append([(x, y)])
frame_idx += 1 prev_gray = frame_gray
cv.imshow(track, vis) #out.write(vis) ch = cv.waitKey(1) if ch ==27: cv.imwrite(track.jpg, vis) break else: break
?? Lucas-Kanade方法計算稀疏特徵集的光流,OpenCV提供了另一種演算法來查找密集的光流,它計算幀中所有點的光流。它基於Gunner Farneback於2003年的《Two-Frame Motion Estimation Based on Polynomial Expansion》。
?? 我們可以通過Opencv的函數cv2.calcOpticalFlowFarneback尋找稠密光流,我們得到的一個兩個通道的向量(u,v)。得到的該向量的大小和方向。用不同的顏色編碼來使其可視化。
cv2.calcOpticalFlowFarneback
?? 方向與Hue值相關,大小與Value值相關。
flow=cv.calcOpticalFlowFarneback( prev, next, flow, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags)
輸入
- 輸出
import cv2 as cv import numpy as np cap = cv.VideoCapture("vtest.avi") ret, frame1 = cap.read() prvs = cv.cvtColor(frame1,cv.COLOR_BGR2GRAY) hsv = np.zeros_like(frame1) hsv[...,1] = 255 while(1): ret, frame2 = cap.read() next = cv.cvtColor(frame2,cv.COLOR_BGR2GRAY)
# 返回一個兩通道的光流向量,實際上是每個點的像素位移值 flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0)
# 笛卡爾坐標轉換為極坐標,獲得極軸和極角 mag, ang = cv.cartToPolar(flow[...,0], flow[...,1])
hsv[...,0] = ang*180/np.pi/2 #角度 hsv[...,2] = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) cv.imshow(frame2,bgr) cv.imshow("frame1", frame2) k = cv.waitKey(30) & 0xff if k == 27: break elif k == ord(s): cv.imwrite(opticalfb.png,frame2) cv.imwrite(opticalhsv.png,bgr) prvs = next cap.release() cv.destroyAllWindows() 原圖
------------------------------------------可愛の分割線------------------------------------------
更多Opencv教程可以 Follow github的opencv教程,中文&English??????歡迎Star??????
??????- 機器視覺 OpenCV—python目標跟蹤(光流)
??????- opencv python 光流法
??????- OpenCV Tutorial 官網