本章我們要學習的是運動物體的跟蹤,現代圖像處理中經典的幾種跟蹤方法主要是:meanshift(均值漂移),Camshift(meanshift的優化版本),KCF,光流法等。
我們本章主要介紹的是前兩種:
meanshift(均值漂移)以及Camshift(meanshift的優化版本)
首先我們需要了解什麼是均值漂移,該算法是一種尋找概率函數離散樣本的最大密度區域的算法,我們可以認為我們圖像中感興趣的區域就是離散樣本密度最大的區域。(這句話看不懂沒關係,先往下看,後面就明白了)
首先我們要有一個均值漂移的基本概念:這個算法會向我們需要跟蹤的區域的方向那慢慢移動過去。
設想在一個有N個樣本點的特徵空間,初始確定一個中心點center(隨便選一個),計算在設置的半徑為D的圓形空間內所有的點(xi)與中心點center的向量。
計算整個圓形空間內所有向量的平均值,得到一個偏移均值
將中心點center移動到偏移均值所指向的位置
重複移動,直到沒有辦法繼續增加圈內的點或移動距離過小後結束
整個過程如下圖所示,points的值為圈內點的數量
使用這種算法,就能讓我們的圓逐步向點多的地方走去,這就是均值漂移的基本概念。
接下來我們需要學習彩色直方圖的概念。
彩色直方圖的x軸表示色彩的值,y軸表示這個色彩的像素有多少個,灰度圖因為只有一個通道所以只有一個直方圖,但如果是HSV或者BGR這種的,就可以對每一個通道進行直方圖的構建,這裡我們只需要對HSV圖像的H通道進行跟蹤即可。(因為我們接下來是根據普通的顏色來進行的跟蹤,一個通道就足夠了)
構建圖像的直方圖需要使用到函數cv2.calcHist,其常用函數語法如下所示:
hist=cv2.calcHist(images, channels, mask, histSize, ranges)
images:輸入的圖像channels:選擇圖像的通道,如果是三通道的話就可以是[0],[1],[2]mask:掩膜,是一個大小和image一樣的np數組,其中把需要處理的部分指定為1,不需要處理的部分指定為0,一般設置為None,如果有mask,會先對輸入圖像進行掩膜操作histSize:使用多少個bin(柱子),一般為256,但如果是H值就是181ranges:像素值的範圍,一般為[0,255]表示0~255,對於H通道而言就是[0,180]需要注意的是,這裡除了mask以外,其餘的幾個參數都要加上[],如下所示:
hist=cv2.calcHist([img],[0],mask,[181],[0,180])這個時候我們還需要使用一種歸一化的方法來對彩色直方圖中的數量值進行規範化。
現有的直方圖中的數值為對應像素的數量,其中圖中出現數量最多的像素的數量值(最高的柱子對應的y軸數值)我們記為max的話,整個直方圖y方向上的取值範圍就是[0,max],我們需要把這個範圍縮減到[0,255],為什麼是255後面會進行解釋。
這裡我們需要使用到cv2.normalize函數,函數主要語法如下所示:
cv2.normalize(src,dst, alpha,beta, norm_type)·src-輸入數組。·dst-與SRC大小相同的輸出數組。·α-範數值在範圍歸一化的情況下歸一化到較低的範圍邊界。·β-上限範圍在範圍歸一化的情況下;它不用於範數歸一化。·范式-規範化類型(見下面詳細介紹)。這裡我們需要注意的是範式-規範化類型,這裡有以下幾種選擇:
NORM_MINMAX:數組的數值被平移或縮放到一個指定的範圍,線性歸一化。NORM_INF:歸一化數組的(切比雪夫距離)L∞範數(絕對值的最大值)NORM_L1:歸一化數組的(曼哈頓距離)L1-範數(絕對值的和)NORM_L2:歸一化數組的(歐幾裡德距離)L2-範數
上面的名詞看起來很高大上,其實是很簡單,我們一一講解下。(不是很感興趣的只要看下第一個NORM_MINMAX即可,剩下的三個可以不看)
首先是NORM_MINMAX,這個是我們最常用的一種歸一化方法。舉個例子,我們上面提到的最高的柱子對應的y軸坐標為max,如果我們使用這種方法,想要縮放到的指定的範圍為[0,255],那麼max就會直接被賦值為255,其餘的柱子也會隨之一樣被壓縮(類似於相似三角形那樣的縮放感覺)。沒錯,很簡單得就介紹完了一種,不是很想了解其他幾個的讀者可以直接跳過本小節剩下來的內容了,因為剩下三種不是很常用。
接下來是NORM_INF,他會對我們每一個柱子的y軸坐標進行如下操作:用當前柱子的y軸坐標,除以所有柱子中y值的絕對值最大的那個作為新的y軸的值,公式如下所示:我們假設上面這張圖就是我們要跟蹤的對象所對應的彩色直方圖(一個很大的跟蹤對象由很多種不同的像素組成,然後我們將他們統計一下,每一種顏色分別對應著幾個像素,有的顏色可能有很多像素,有的顏色可能一個也沒有)。
上圖左邊紅色的那個突出的柱子對應的是最大y軸值,他有100000個點,其他的柱子對應的像素的數目就是在[0,100000]之間,然後我們將彩色直方圖歸一化到[0,255]之間後,紅色突出的柱子對應的最大y軸值就變成了255,其他的顏色對應的y軸的值也一一等比例改變到了0-255之間,這個我們要跟蹤物體所構成的歸一化彩色直方圖我們稱為Hist。
然後我們對我們要處理的圖像(即包含我們要跟蹤的對象的整個圖片)根據我們上面得到的Hist來判斷要處理的圖像中的每一個像素是否屬於我們跟蹤對象或者說屬於我們跟蹤對象的概率有多大。
例如,我們要處理的圖像中有一個點為紅色,那麼他就會去看我們跟蹤對象的直方圖Hist,發現直方圖中的紅色對應的屬於跟蹤對象的可能性(y軸的值)為255,則就會直接賦值為255(純白色);如果要處理的圖像中有一個點為棕色,然後去看直方圖,發現發現直方圖中的棕色對應的屬於跟蹤對象的可能性(y軸的值)為0,就會直接賦值為0(黑色),由此構成我們的直方圖反投影圖。
所以我們最後得到的圖像就是一個與原圖同樣大小的灰度圖像。例如我跟蹤了下面綠色部分的物體,得到綠色部分的彩色直方圖後,其對應的直方圖反投影圖就如下所示:
越暗的地方說明屬於跟蹤部分的可能性越低,越亮的地方屬於跟蹤部分的可能性越高。
這裡使用到的函數為cv2.calcBackProject,函數語法如下所示:dst=cv2.calcBackProject(image,channel,hist,range,scale)
image:輸入圖像channel:用來計算反向投影的通道數,與產生直方圖對應的通道應一致hist:作為輸入的直方圖range:直方圖的取值範圍scale:輸出圖像的縮放比,一般為1,保持與輸入圖像一樣的大小dst:輸出圖像
注意:除了hist和scale外,其他的參數都要加上[]例如:
dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1)
上述講述的都是關於meanshift(均值漂移)的跟蹤方法(其實Camshift也是這個樣子的原理)。現在有了一切的基礎知識後,我們就可以進行物體跟蹤的實現了,這裡我們打算跟蹤一個綠色的物體來看看,首先我們載入我們要處理的視頻文件(或者直接用攝像頭也行):
import cv2import numpy as npcap=cv2.VideoCapture('1.mp4')然後我們設置下我們第一個起始框的位置和長寬(可以理解為上面均值漂移原理中起始圓的起始位置和圓的大小)
r,h,c,w=(400,500,400,500)track_window=(c,r,w,h)然後我們讀取第一幀,先將圖像轉換為HSV圖,方便目標跟蹤,然後通過掩膜操作來得到圖像中的綠色部分的掩膜:
ret,frame=cap.read()hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)mask=cv2.inRange(hsv,np.array((35,43,46)),np.array((77,255,255)))接著我們使用cv2.calcHist函數來得到圖像中的綠色部分,並計算這部分的直方圖,我們只要收集第0通道:H的數據就好了,因為是H通道,其取值範圍為0-180,所以需要181根柱子,H通道像素的取值為0-180(柱子數量不是與像素取值範圍不能一一對應的話,柱與柱之間會有點壓縮,即x軸方向上會產生柱子與柱子之間的融合):
hist=cv2.calcHist([hsv],[0],mask,[181],[0,180])cv2.normalize(hist,hist,0,255,cv2.NORM_MINMAX)然後我們來設置均值漂移meanshift的一次活動的終止條件:
cv2.TERM_CRITERIA_EPS:代表一次均值漂移累計的移動次數,EPS表示epsilon,這裡我們設置為10。
cv2.TERM_CRITERIA_COUNT:表示一次均值漂移移動的最小偏移像素,如果一次漂移的像素值低於這個值,就會終止這次活動,這裡我們設置為1。term_crit=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,10,1)這裡可能有讀者會有點疑惑為什麼要加上這個終止條件,其實這是為了讓視頻處理(攝像頭)變得流暢而設置的,不然如果不設置這個條件,對於每一幀傳入的圖像,meanshift都要找到當前圖像中最好的地方(密度最大的地方),然後才會開始處理下一幀,這樣圖像看起來就會變得異常卡頓。我們程序也沒必要每一幀都完完全全處在跟蹤物體的最佳位置上,容許有些許的偏差,只要能極大部分跟蹤到了物體就可以了。
然後我們在第一幀中得到了要跟蹤的物體的顏色直方圖後,我們開始處理圖像中的後續幀:
while 1: ret,frame=cap.read() if ret== True: hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1) ret,track_window=cv2.meanShift(dst,track_window,term_crit) x,y,w,h=track_window img=cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2) cv2.namedWindow('img',cv2.WINDOW_NORMAL) cv2.imshow('img',img) if cv2.waitKey(1)==ord('q'): break else: breakcap.release()cv2.destroyAllWindows()然後我們就能夠實現綠色物體的跟蹤了,運行結果如下所示:
完整代碼如下所示:
import cv2import numpy as npcap=cv2.VideoCapture('1.mp4')ret,frame=cap.read()r,h,c,w=(400,500,400,500)track_window=(c,r,w,h)hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)mask=cv2.inRange(hsv,np.array((35,43,46)),np.array((77,255,255)))hist=cv2.calcHist([hsv],[0],mask,[181],[0,180])cv2.normalize(hist,hist,0,255,cv2.NORM_MINMAX)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],hist,[0,180],1) ret,track_window=cv2.meanShift(dst,track_window,term_crit) x,y,w,h=track_window img=cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2) cv2.namedWindow('img',cv2.WINDOW_NORMAL) cv2.imshow('img',img) if cv2.waitKey(1)==ord('q'): break else: breakcap.release()cv2.destroyAllWindows()
但我們可以注意到的是,我們矩形框的大小是我們一開始就直接設置好的,在整個跟蹤過程中其大小是不會改變的(不管我們的跟蹤物體是否變小或者變大了)
為了解決這個問題,我們可以在meanshift的基礎上,讓他自適應跟蹤物體的大小來調整矩形框的大小,這就是Camshift。CamShift算法的全稱是」Continuously Adaptive Mean-SHIFT」,稱為連續自適應的meanshift算法,算法部分不變,只是能讓他能夠自我適應跟蹤物體大小而已。
代碼方面也和meanshift差不多,只要在while循環裡改幾行就可以了:while 1 : ret,frame=cap.read() if ret == True: hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) dst=cv2.calcBackProject(hsv,[0],hist,[0,180],1) ret,track_window=cv2.CamShift(dst,track_window,term_crit) boxes=cv2.boxPoints(ret) pts=np.int0(boxes) img2=cv2.polylines(frame,[pts],True,(0,255,0),2) cv2.imshow('img2',img2) if cv2.waitKey(1)==ord('q'): break else: break然後運行結果中的綠色矩形框就能夠根據跟蹤的對象而自適應改變框的大小了(注意:這個Camshift很容易就會檢測出錯)
1.問題,讀者可以根據需要自行修改。
2.我們要跟蹤的對象也可以不是指定的顏色區間,其他特定的東西也沒問題,只要能得到顏色直方圖的都可以。
3.我們這裡的初始矩形框的位置是默認設置到一個位置上的,這其實不是很好,因為如果周邊沒有屬於跟蹤目標的像素或者可能性比較低,導致直方圖反投影圖中那一塊部分全黑的話,那個框就會在那自行鬼畜(因為不知道該往哪走了),這塊是可以加工的,比如直接把矩形框扔到密度比較高的部分也是可以的。(即先檢測一幀,得到第一幀中密度高的區域,然後再設置矩形框)
4.這種檢測中物體移動速度不宜過快過快,不然矩形框可能跟不上。
課程《ROS機器視覺開發入門 · 古月》將帶你入門常用2D/3D視覺傳感器的ROS驅動及數據結構,使用ROS標定功能包完成相機的參數標定,結合人臉識別、物體跟蹤等例程重點講解ROS與OpenCV的結合方法,及基於TensorFlow機器學習平臺實現對日常用品的識別,為後續ROS機器視覺開發夯實基礎。
點擊"閱讀原文"查看課程詳情