目標
• 本節咱們要學習使用 Meanshift 和 Camshift 算法在視頻中找到並跟蹤
目標對象
39.1 Meanshift
Meanshift 算法的基本原理是和很簡單的。假設咱們有一堆點(好比直方圖反向投影獲得的點),和一個小的圓形窗口,咱們要完成的任務就是將這個窗口移動到最大灰度密度處(或者是點最多的地方)。以下圖所示:python
初始窗口是藍色的「C1」,它的圓心爲藍色方框「C1_o」,而窗口中全部點質心倒是「C1_r」(小的藍色圓圈),很明顯圓心和點的質心沒有重合。因此移動圓心 C1_o 到質心 C1_r,這樣咱們就獲得了一個新的窗口。這時又能夠找到新窗口內全部點的質心,大多數狀況下仍是不重合的,因此重複上面的操做:將新窗口的中心移動到新的質心。就這樣不停的迭代操做直到窗口的中心和其所包含點的質心重合爲止(或者有一點小偏差)。按照這樣的操做咱們的窗口最終會落在像素值(和)最大的地方。如上圖所示「C2」是窗口的最後位址,咱們能夠看出來這個窗口中的像素點最多。整個過程以下圖所示:
一般狀況下咱們要使用直方圖方向投影獲得的圖像和目標對象的起始位置。
當目標對象的移動會反映到直方圖反向投影圖中。就這樣,meanshift 算法就把咱們的窗口移動到圖像中灰度密度最大的區域了。linux
39.2 OpenCV 中的 Meanshift
要在 OpenCV 中使用 Meanshift 算法首先咱們要對目標對象進行設置,計算目標對象的直方圖,這樣在執行 meanshift 算法時咱們就能夠將目標對象反向投影到每一幀中去了。另外咱們還須要提供窗口的起始位置。在這裏咱們值計算 H(Hue)通道的直方圖,一樣爲了不低亮度形成的影響,咱們使用函數 cv2.inRange() 將低亮度的值忽略掉。算法
import numpy as np import cv2 cap = cv2.VideoCapture('slow.flv') # take first frame of the video ret,frame = cap.read() # setup initial location of window r,h,c,w = 250,90,400,125 # simply hardcoded the values track_window = (c,r,w,h) # set up the ROI for tracking roi = frame[r:r+h, c:c+w] hsv_roi = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.))) roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180]) cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX) # Setup the termination criteria, either 10 iteration or move by atleast 1 pt term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 ) while(1): ret ,frame = cap.read() if ret == True: hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1) # apply meanshift to get the new location ret, track_window = cv2.meanShift(dst, track_window, term_crit) # Draw it on image x,y,w,h = track_window img2 = cv2.rectangle(frame, (x,y), (x+w,y+h), 255,2) cv2.imshow('img2',img2) k = cv2.waitKey(60) & 0xff if k == 27: break else: cv2.imwrite(chr(k)+".jpg",img2) else: break cv2.destroyAllWindows() cap.release()
下面是我使用 meanshift 算法對一個視頻前三幀分析的結果:小程序
39.3 Camshift
你認真看上面的結果了嗎?這裏面還有一個問題。咱們的窗口的大小是固定的,而汽車由遠及近(在視覺上)是一個逐漸變大的過程,固定的窗口是不合適的。因此咱們須要根據目標的大小和角度來對窗口的大小和角度進行修訂。數組
OpenCVLabs 爲咱們帶來的解決方案(1988 年):一個被叫作 CAMshift 的算法。
這個算法首先要使用 meanshift,meanshift 找到(並覆蓋)目標以後,再去調整窗口的大小, 。它還會計算目標對象的最佳外接橢圓的角度,並以此調節窗口角度。而後使用更新後的窗口大小和角度來在原來的位置繼續進行 meanshift。重複這個過程知道達到須要的精度。app
39.4 OpenCV 中的 Camshift
與 Meanshift 基本同樣,可是返回的結果是一個帶旋轉角度的矩形(這是咱們的結果),以及這個矩形的參數(被用到下一次迭代過程當中)。下面是代碼:
dom
import numpy as np import cv2 cap = cv2.VideoCapture('slow.flv') # take first frame of the video ret,frame = cap.read() # setup initial location of window r,h,c,w = 250,90,400,125 # simply hardcoded the values track_window = (c,r,w,h) # set up the ROI for tracking roi = frame[r:r+h, c:c+w] hsv_roi = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.))) roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180]) cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX) # Setup the termination criteria, either 10 iteration or move by atleast 1 pt term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 ) while(1): ret ,frame = cap.read() if ret == True: hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1) # apply meanshift to get the new location ret, track_window = cv2.CamShift(dst, track_window, term_crit) # Draw it on image pts = cv2.boxPoints(ret) pts = np.int0(pts) img2 = cv2.polylines(frame,[pts],True, 255,2) cv2.imshow('img2',img2) k = cv2.waitKey(60) & 0xff if k == 27: break else: cv2.imwrite(chr(k)+".jpg",img2) else: break cv2.destroyAllWindows() cap.release()
對三幀圖像分析的結果以下:ide
練習
1. OpenCV 的官方示例中有一個 camshift 的交互式演示,搞定它吧!函數
# 待排版 #!/usr/bin/env python ''' Camshift tracker ================ This is a demo that shows mean-shift based tracking You select a color objects such as your face and it tracks it. This reads from video camera (0 by default, or the camera number the user enters) http://www.robinhewitt.com/research/track/camshift.html Usage: ------ camshift.py [<video source>] To initialize tracking, select the object with mouse Keys: ----- ESC - exit b - toggle back-projected probability visualization ''' import numpy as np import cv2 # local module import video class App(object): def __init__(self, video_src): self.cam = video.create_capture(video_src) ret, self.frame = self.cam.read() cv2.namedWindow('camshift') cv2.setMouseCallback('camshift', self.onmouse) self.selection = None self.drag_start = None self.tracking_state = 0 self.show_backproj = False def onmouse(self, event, x, y, flags, param): x, y = np.int16([x, y]) if event == cv2.EVENT_LBUTTONDOWN: self.drag_start = (x, y) self.tracking_state = 0 # 官方示例中下面一行判斷有問題,做以下修改就能夠了 if self.drag_start and event == cv2.EVENT_MOUSEMOVE: # print x,y if flags==cv2.EVENT_FLAG_LBUTTON: # print 'ok' h, w = self.frame.shape[:2] xo, yo = self.drag_start x0, y0 = np.maximum(0, np.minimum([xo, yo], [x, y])) x1, y1 = np.minimum([w, h], np.maximum([xo, yo], [x, y])) self.selection = None if x1-x0 > 0 and y1-y0 > 0: self.selection = (x0, y0, x1, y1) print self.selection else: self.drag_start = None if self.selection is not None: self.tracking_state = 1 def show_hist(self): bin_count = self.hist.shape[0] bin_w = 24
目標
本節咱們將要學習:
• 光流的概念以及 Lucas-Kanade 光流法
• 使用函數 cv2.calcOpticalFlowPyrLK() 對圖像中的特徵點進行跟蹤
40.1 光流
因爲目標對象或者攝像機的移動形成的圖像對象在連續兩幀圖像中的移動被稱爲光流。它是一個 2D 向量場,能夠用來顯示一個點從第一幀圖像到第二幀圖像之間的移動。以下圖所示(Image Courtesy: Wikipedia article onOptical Flow):
上圖顯示了一個點在連續的五幀圖像間的移動。箭頭表示光流場向量。光流在不少領域中都頗有用:
• 由運動重建結構
• 視頻壓縮
• Video Stabilization 等
光流是基於一下假設的:
1. 在連續的兩幀圖像之間(目標對象的)像素的灰度值不改變。
2. 相鄰的像素具備相同的運動
第一幀圖像中的像素 I (x,y,t) 在時間 dt 後移動到第二幀圖像的(x+dx,y+dy)處。根據第一條假設:灰度值不變。因此咱們能夠獲得:
對等號右側進行泰勒級數展開,消去相同項,兩邊都除以 dt,獲得以下方程:
其中:
上邊的等式叫作光流方程。其中 f x 和 f y 是圖像梯度,一樣 f t 是時間方向的梯度。但(u,v)是不知道的。咱們不能在一個等式中求解兩個未知數。有幾個方法能夠幫咱們解決這個問題,其中的一個是 Lucas-Kanade 法
40.2 Lucas-Kanade 法
如今咱們要使用第二條假設,鄰域內的全部點都有類似的運動。LucasKanade 法就是利用一個 3x3 鄰域中的 9 個點具備相同運動的這一點。這樣咱們就能夠找到這 9 個點的光流方程,用它們組成一個具備兩個未知數 9 個等式的方程組,這是一個約束條件過多的方程組。一個好的解決方法就是使用最小二乘擬合。下面就是求解結果:
(有沒有發現上邊的逆矩陣與 Harris 角點檢測器很是類似,這說明角點很適合被用來作跟蹤)
從使用者的角度來看,想法很簡單,咱們取跟蹤一些點,而後咱們就會得到這些點的光流向量。可是還有一些問題。直到如今咱們處理的都是很小的運動。
若是有大的運動怎麼辦呢?圖像金字塔。咱們可使用圖像金字塔的頂層,此時小的運動被移除,大的運動裝換成了小的運動,如今再使用 Lucas-Kanade算法,咱們就會獲得尺度空間上的光流。
40.3 OpenCV 中的 Lucas-Kanade 光流
上述全部過程都被 OpenCV 打包成了一個函數:cv2.calcOpticalFlowPyrLK()。
如今咱們使用這個函數建立一個小程序來跟蹤視頻中的一些點。要跟蹤那些點呢?咱們使用函數 cv2.goodFeatureToTrack() 來肯定要跟蹤的點。咱們首先在視頻的第一幀圖像中檢測一些 Shi-Tomasi 角點,而後咱們使用 LucasKanade 算法迭代跟蹤這些角點。咱們要給函數 cv2.calcOpticlaFlowPyrLK()傳入前一幀圖像和其中的點,以及下一幀圖像。函數將返回帶有狀態數的點,若是狀態數是 1,那說明在下一幀圖像中找到了這個點(上一幀中角點),若是狀態數是 0,就說明沒有在下一幀圖像中找到這個點。咱們再把這些點做爲參數傳給函數,如此迭代下去實現跟蹤。代碼以下:
(上面的代碼沒有對返回角點的正確性進行檢查。圖像中的一些特徵點甚至在丟失之後,光流還會找到一個預期類似的點。因此爲了實現穩定的跟蹤,咱們應該每一個必定間隔就要進行一次角點檢測。OpenCV 的官方示例中帶有這樣一個例子,它是每 5 幀進行一個特徵點檢測。它還對光流點使用反向檢測來選取好的點進行跟蹤。示例爲/samples/python2/lk_track.py)
import numpy as np import cv2 cap = cv2.VideoCapture('slow.flv') # params for ShiTomasi corner detection feature_params = dict( maxCorners = 100, qualityLevel = 0.3, minDistance = 7, blockSize = 7 ) # Parameters for lucas kanade optical flow lk_params = dict( winSize = (15,15), maxLevel = 2, criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)) # Create some random colors color = np.random.randint(0,255,(100,3)) # Take first frame and find corners in it ret, old_frame = cap.read() old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY) p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **feature_params) # Create a mask image for drawing purposes mask = np.zeros_like(old_frame) while(1): ret,frame = cap.read() frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # calculate optical flow p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params) # Select good points good_new = p1[st==1] good_old = p0[st==1] # draw the tracks for i,(new,old) in enumerate(zip(good_new,good_old)): a,b = new.ravel() c,d = old.ravel() mask = cv2.line(mask, (a,b),(c,d), color[i].tolist(), 2) frame = cv2.circle(frame,(a,b),5,color[i].tolist(),-1) img = cv2.add(frame,mask) cv2.imshow('frame',img) k = cv2.waitKey(30) & 0xff if k == 27: break # Now update the previous frame and previous points old_gray = frame_gray.copy() p0 = good_new.reshape(-1,1,2) cv2.destroyAllWindows() cap.release()
下面是個人到的結果:
40.4 OpenCV 中的稠密光流
Lucas-Kanade 法是計算一些特徵點的光流(咱們上面的例子使用的是Shi-Tomasi 算法檢測到的角點)。OpenCV 還提供了一種計算稠密光流的方法。它會圖像中的全部點的光流。這是基於 Gunner_Farneback 的算法(2003 年)。
下面的例子就是使用上面的算法計算稠密光流。結果是一個帶有光流向量(u,v)的雙通道數組。經過計算咱們能獲得光流的大小和方向。咱們使用顏色對結果進行編碼以便於更好的觀察。方向對應於 H(Hue)通道,大小對應於 V(Value)通道。代碼以下:
import cv2 import numpy as np cap = cv2.VideoCapture("vtest.avi") ret, frame1 = cap.read() prvs = cv2.cvtColor(frame1,cv2.COLOR_BGR2GRAY) hsv = np.zeros_like(frame1) hsv[...,1] = 255 while(1): ret, frame2 = cap.read() next = cv2.cvtColor(frame2,cv2.COLOR_BGR2GRAY) flow = cv2.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1]) hsv[...,0] = ang*180/np.pi/2 hsv[...,2] = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX) rgb = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR) cv2.imshow('frame2',rgb) k = cv2.waitKey(30) & 0xff if k == 27: break elif k == ord('s'): cv2.imwrite('opticalfb.png',frame2) cv2.imwrite('opticalhsv.png',rgb) prvs = next cap.release() cv2.destroyAllWindows()
結果以下:
OpenCV 的官方示例中有一個更高級的稠密光流/samples/python2/
opt_flow.py,去搞定它吧!
目標
• 本節咱們將要學習 OpenCV 中的背景減除方法
41.1 基礎
在不少基礎應用中背景檢出都是一個很是重要的步驟。例如顧客統計,使用一個靜態攝像頭來記錄進入和離開房間的人數,或者是交通攝像頭,須要提取交通工具的信息等。在全部的這些例子中,首先要將人或車單獨提取出來。技術上來講,咱們須要從靜止的背景中提取移動的前景。
若是你有一張背景(僅有背景不含前景)圖像,好比沒有顧客的房間,沒有交通工具的道路等,那就好辦了。咱們只須要在新的圖像中減去背景就能夠獲得前景對象了。可是在大多數狀況下,咱們沒有這樣的(背景)圖像,因此咱們須要從咱們有的圖像中提取背景。若是圖像中的交通工具還有影子的話,那這個工做就更難了,由於影子也在移動,僅僅使用減法會把影子也當成前景。真是一件很複雜的事情。
爲了實現這個目的科學家們已經提出了幾種算法。OpenCV 中已經包含了其中三種比較容易使用的方法。咱們一個一個學習一下吧。
41.2 BackgroundSubtractorMOG
這是一個以混合高斯模型爲基礎的前景/背景分割算法。它是 P.KadewTraKuPong和 R.Bowden 在 2001 年提出的。它使用 K(K=3 或 5)個高斯分佈混合對背景像素進行建模。使用這些顏色(在整個視頻中)存在時間的長短做爲混合的權重。背景的顏色通常持續的時間最長,並且更加靜止。一個像素怎麼會有分佈呢?在 x,y 平面上一個像素就是一個像素沒有分佈,可是咱們如今講的背景建模是基於時間序列的,所以每個像素點所在的位置在整個時間序列中就會有不少值,從而構成一個分佈。
在編寫代碼時,咱們須要使用函數:cv2.createBackgroundSubtractorMOG()建立一個背景對象。這個函數有些可選參數,好比要進行建模場景的時間長度,高斯混合成分的數量,閾值等。將他們所有設置爲默認值。而後在整個視頻中咱們是須要使用 backgroundsubtractor.apply() 就能夠獲得前景的掩模了。
下面是一個簡單的例子:
import numpy as np import cv2 cap = cv2.VideoCapture('vtest.avi') fgbg = cv2.createBackgroundSubtractorMOG() while(1): ret, frame = cap.read() fgmask = fgbg.apply(frame) cv2.imshow('frame',fgmask) k = cv2.waitKey(30) & 0xff if k == 27: break cap.release() cv2.destroyAllWindows()
41.3 BackgroundSubtractorMOG2
這個也是以高斯混合模型爲基礎的背景/前景分割算法。它是以 2004 年和 2006 年 Z.Zivkovic 的兩篇文章爲基礎的。這個算法的一個特色是它爲每個像素選擇一個合適數目的高斯分佈。(上一個方法中咱們使用是 K 高斯分佈)。這樣就會對因爲亮度等發生變化引發的場景變化產生更好的適應。
和前面同樣咱們須要建立一個背景對象。但在這裏咱們咱們能夠選擇是否檢測陰影。若是 detectShadows = True(默認值),它就會檢測並將影子標記出來,可是這樣作會下降處理速度。影子會被標記爲灰色。
239
www.linuxidc.com
# -*- coding: utf-8 -*-
"""
Created on Mon Jan 27 18:16:29 2014
@author: duan
"""
import numpy as np
import cv2
cap = cv2.VideoCapture('vtest.avi')
fgbg = cv2.createBackgroundSubtractorMOG2()
while(1):
ret, frame = cap.read()
fgmask = fgbg.apply(frame)
cv2.imshow('frame',fgmask)
k = cv2.waitKey(30) & 0xff
if k == 27:
break
cap.release()
cv2.destroyAllWindows()
41.4 BackgroundSubtractorGMG
此算法結合了靜態背景圖像估計和每一個像素的貝葉斯分割。這是 2012 年Andrew_B.Godbehere,Akihiro_Matsukawa 和 Ken_Goldberg 在文章中提出的。
它使用前面不多的圖像(默認爲前 120 幀)進行背景建模。使用了機率前景估計算法(使用貝葉斯估計鑑定前景)。這是一種自適應的估計,新觀察到的對象比舊的對象具備更高的權重,從而對光照變化產生適應。一些形態學操做如開運算閉運算等被用來除去不須要的噪音。在前幾幀圖像中你會獲得一個黑色窗口。
對結果進行形態學開運算對與去除噪聲頗有幫助。
import numpy as np import cv2 cap = cv2.VideoCapture('vtest.avi') fgbg = cv2.createBackgroundSubtractorMOG2() while(1): ret, frame = cap.read() fgmask = fgbg.apply(frame) cv2.imshow('frame',fgmask) k = cv2.waitKey(30) & 0xff if k == 27: break cap.release() cv2.destroyAllWindows()
41.5 結果
原始圖像
下圖顯示了一段視頻中的第 200 幀圖像
BackgroundSubtractorMOG 的結果
2 BackgroundSubtractorMOG2 的結果灰色區域表明陰影
BackgroundSubtractorGMG 的結果使用形態學開運算將噪音去除。
更多資源練習