本文參考資料:
[1] OpenCV-Python Tutorials » Video Analysis » Optical Flow
[2] Good Features to Track
[3] Pyramidal Implementation of the Lucas Kanade Feature Tracker Description of the algorithmhtml
代碼地址:https://github.com/divertingPan/video_scanner/blob/main/main_v0.2.pypython
本篇是接續【硬核攝影】給火車拍個全身照的內容。使用自動腳本生成火車視頻掃描圖存在一個問題:若是火車的運動速度是變化的,只使用手動給定的固定掃描間隔是會有大問題的。例以下圖。git
顯然,火車在減速,車頭位置掃描間隔正合適,而車尾的掃描間隔太大了。若是可以根據當前車速自動判斷應該用多寬的掃描間隔就可以解決這個問題。github
那麼就應該得到先後兩幀之間物體運動的距離,最好連方向也能判斷出來,這樣直接就能夠自動判斷拼接方向了。算法
顯然,這個需求徹底能夠用光流法
來實現。具體的算法原理和例程在開頭的參考資料內,留做課後閱讀材料。利用opencv能夠直接獲取視頻中關鍵點在先後兩幀的定位,利用這個定位的橫向差值能夠得到這一刻的物體運動速度,差值的正負則表明運動方向。這樣利用自動檢測的運動信息就能夠實現變速物體的掃描了,而且還能夠省下本身去數格子算運動距離的精力。app
先獲取兩個相鄰幀,轉成灰度圖像ide
vc = cv2.VideoCapture(video_path) rval = vc.isOpened() vc.set(cv2.CAP_PROP_POS_FRAMES, 300) rval, frame_1 = vc.read() rval, frame_2 = vc.read() frame_1_gray = cv2.cvtColor(frame_1, cv2.COLOR_BGR2GRAY) frame_2_gray = cv2.cvtColor(frame_2, cv2.COLOR_BGR2GRAY)
計算光流的第一步要獲取圖像的關鍵點,這些關鍵點將做爲追蹤運動狀況的標靶,這裏對於關鍵點的檢測能夠指定一個mask蒙版,檢測時只檢測蒙版內的區域。能夠做爲一個粗篩手段,避免背景干擾。這個mask能夠在UI界面裏作成一個根據左邊圖像欄本身制定區域的功能。佈局
feature_params = dict(maxCorners=20, qualityLevel=0.3, minDistance=3, blockSize=5) mask = np.zeros((frame_height, frame_width), dtype='uint8') mask[frame_height//2:frame_height//2+600, position-300:position+300] = 1 p0 = cv2.goodFeaturesToTrack(frame_1_gray, mask=mask, **feature_params)
而後計算光流,獲得匹配的關鍵點good_new
和good_old
post
lk_params = dict(winSize=(15,15), maxLevel=5, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)) p1, st, err = cv2.calcOpticalFlowPyrLK(frame_1_gray, frame_2_gray, p0, None, **lk_params) good_new = p1[st==1] good_old = p0[st==1]
畫出來一下看看究竟對不對測試
line = np.zeros_like(frame_1) frame = frame_overlay for i,(new,old) in enumerate(zip(good_new,good_old)): a,b = new.ravel() c,d = old.ravel() line = cv2.line(line, (a,b), (c,d), [0,255,255], 1) frame = cv2.circle(frame, (a,b), 3, [0,0,255], -1) frame = cv2.circle(frame, (c,d), 3, [0,255,0], -1) img = cv2.add(frame, line) cv2.imwrite('optical_flow.jpg', img) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(img) plt.show()
這時,咱們計算good_new
和good_old
之間在x軸上的差值,就能得到運動的距離,可是因爲偶爾會有干擾點或者偏差點,因此咱們直接取這一堆數裏的衆數,做爲實際的運動距離。
moving_distance = [int(good_new[i, 0]-good_old[i, 0]) for i in range(len(good_new)) if abs(good_new[i, 1]-good_old[i, 1]) < 2] width = max(moving_distance, default=None, key=lambda v: moving_distance.count(v))
這個差值若是是正值則表明後一幀在前一幀的右邊,那麼運動距離就是從左到右,是負值的話就反之。這個正負就能夠做爲運動方向的判斷。
因爲這裏用到的是不定的width,因此拼接圖時有兩種方法可選擇:一種是動態地拼接圖片,這個好處是程序簡單,缺點是很是慢,尤爲在圖片越拼越大以後。一個10000多幀的錄像,老潘洗完澡回來發現還沒拼完。
因此不推薦上述方法,建議利用第二種方法,事先初始化一個空圖片矩陣,這樣的話,方法和上一版本的基本一致,惟一有大變樣的地方在於計算正確的矩陣大小。也就是圖片長度。
自適應檢測運動間隔的大致思路以下:首先手動指定一個開始運動檢測的第一個關鍵幀(避免開頭無運動火車的影響),這個關鍵幀以前以及這個關鍵幀以後的一段內,width爲此關鍵幀檢測到的火車運動距離,日後有第二個檢測關鍵幀,後續的一小段所用的width爲第二關鍵幀檢測到的火車運動距離,第三關鍵幀及之後同理。因此還須要指定一個檢測靈敏度,這個靈敏度就是關鍵幀的數量,越多越能靈活應對變速狀況。(靈敏度=1即只抽一幀進行速度檢測,適合勻速狀況,靈敏度=總幀數即每一幀都進行運動檢測,適合蛇皮走位的極度複雜狀況,但每幀都要計算光流會慢到爆),具體靈敏度能夠根據上一篇裏面1像素可以接納的火車運動速度變化區間
來指定。
窗口寬度爲1像素,則火車速度就應該爲6.83x60 mm/s,即0.41m/s。
這種狀況下,火車在±0.41m/s內的速度變化並不會影響到當前的掃描區間結果。
具體計算時爲了方便起見,利用列表來管理關鍵幀的位置和對應的運動速度(width),爲了防止短期內可能出現的檢測偏差狀況,向後檢測連續兩次光流取均值獲取更穩的結果。 在循環外又額外增長了一下img_length
是由於adaptive_length//adaptive_sensitivity
的整除可能會致使最後有幾幀被遺漏,經過這一行能夠修正img_length
的數值。通過老潘的手動計算以及實際測試,這樣的圖片長度是恰好的。
img_length = 0 width_list = [] width_adjust_position = [] for i in range(adaptive_sensitivity): print(adaptive_start + i * (adaptive_length//adaptive_sensitivity)) vc.set(cv2.CAP_PROP_POS_FRAMES, adaptive_start + i * (adaptive_length//adaptive_sensitivity)) rval, frame_1 = vc.read() rval, frame_2 = vc.read() rval, frame_3 = vc.read() width_list.append((optical_flow(frame_1, frame_2)+optical_flow(frame_2, frame_3))//2) if i == 0: img_length = width_list[0] * (adaptive_start + (adaptive_length//adaptive_sensitivity)) width_adjust_position.append(0) else: img_length += width_list[i] * (adaptive_length//adaptive_sensitivity) width_adjust_position.append(adaptive_start + i * (adaptive_length//adaptive_sensitivity)) img_length += width_list[-1] * (adaptive_length - (adaptive_length//adaptive_sensitivity) * adaptive_sensitivity) img = np.empty((frame_height, abs(img_length), 3), dtype='uint8')
以後還有一個難點是如何在遍歷視頻幀時知道當前處於哪一個速度區間內?老潘想了一個鬼點子:利用當前幀的序號減關鍵幀的列表,統計列表裏面值<=0的數量,這個數量-1,就應該是當前幀所對應的關鍵幀之間的速度區間。並且對於img的操做,pixel_start
的計算邏輯也應該變一下,讓他像指針同樣跟隨進度變化而改變本身的指向位置。
if width_list[0] > 0: pixel_start = img_length for i in range(total_frames): rval, frame = vc.read() if not rval: print('break') break width = width_list[((width_adjust_position - i) <= 0).sum() -1] pixel_start -= width pixel_end = pixel_start + width img[:, pixel_start:pixel_end, :] = frame[:, position:position + width, :] if i % 100 == 0: print('{}/{} - {}'.format(i, total_frames, width))
若是速度爲負則反之,和上述大同小異,只是指針變化狀況稍微變一下:
else: pixel_start = 0 for i in range(total_frames): rval, frame = vc.read() if not rval: print('break') break width = abs(width_list[((width_adjust_position - i) <= 0).sum() -1]) pixel_end = pixel_start + width img[:, pixel_start:pixel_end, :] = frame[:, position:position + width, :] pixel_start += width if i % 100 == 0: print('{}/{} - {}'.format(i, total_frames, width))
上一版本的圖像保存能夠直接用,可是因爲獲取圖像長度的方法不太好,這一版本里改成img.shape[1]
獲取圖片長度,替換本來的什麼幀數乘width的複雜操做。
以後將這些功能整合到本來的UI程序裏面。加一下控件互動(代碼略,可自行閱讀源碼),修改後的佈局界面以下,選擇manual或者adaptive則會使用對應功能的值,另外一部分的值不會起做用。
至於識別的準確率,準起來比老潘手動去數格子都要準,可是偶爾也會有識別失誤的狀況,根據屢次測試,這種狀況是mask沒有很好罩住車體,有一部分干擾前景或背景(例如行人的走動、草木被風吹動)影響了被檢測的運動點。此時調整mask的範圍,便可解決。