全解析MeanShift傳統(tǒng)目標跟蹤算法
一、均值漂移(MeanShift)
該算法尋找離散樣本的最大密度,并且重新計算下一幀的最大密度,這個算法的特點就是可以給出目標移動的方向。

meanshift算法的原理很簡單。假設你有一堆點集,還有一個小的窗口,這個窗口可能是圓形的,現(xiàn)在你可能要移動這個窗口到點集密度最大的區(qū)域當中。
最開始的窗口是藍色圓環(huán)的區(qū)域,命名為C1。藍色圓環(huán)的圓心用一個藍色的矩形標注,命名為C1_o。
而窗口中所有點的點集構成的質心在藍色圓形點C1_r處,顯然圓環(huán)的形心和質心并不重合。所以,移動藍色的窗口,使得形心與之前得到的質心重合。在新移動后的圓環(huán)的區(qū)域當中再次尋找圓環(huán)當中所包圍點集的質心,然后再次移動,通常情況下,形心和質心是不重合的。不斷執(zhí)行上面的移動過程,直到形心和質心大致重合結束。這樣,最后圓形的窗口會落到像素分布最大的地方,也就是圖中的綠色圈,命名為C2。
meanshift算法除了應用在視頻追蹤當中,在聚類,平滑等等各種涉及到數(shù)據(jù)以及非監(jiān)督學習的場合當中均有重要應用,是一個應用廣泛的算法。
如果不知道預先要跟蹤的目標,就可以采用這種巧妙地辦法,加設定條件,使能動態(tài)的開始跟蹤(和停止跟蹤)視頻的某些區(qū)域,(如可以采用預先訓練好的SVM進行目標的檢測,然后開始使用均值漂移MeanShift跟蹤檢測到的目標)
所以一般是分兩個步驟:1.標記感興趣區(qū)域 2.跟蹤該區(qū)域
二、流程
圖像是一個矩陣信息,如何在一個視頻當中使用meanshift算法來追蹤一個運動的物體呢?大致流程如下:
| meanshift流程 |
| 1.首先在圖像上選定一個目標區(qū)域 |
| 2.計算選定區(qū)域的直方圖分布,一般是HSV色彩空間的直方圖 |
| 3.對下一幀圖像 b 同樣計算直方圖分布 |
| 4.計算圖像 b 當中與選定區(qū)域直方圖分布最為相似的區(qū)域,使用meanshift算法將選定區(qū)域沿著最為相似的部分進行移動,直到找到最相似的區(qū)域,便完成了在圖像b中的目標追蹤。 |
| 5.重復3到4的過程,就完成整個視頻目標追蹤。 |
| 通常情況下我們使用直方圖反向投影得到的圖像和第一幀目標對象的起始位置,當目標對象的移動會反映到直方圖反向投影圖中,meanshift算法就把我們的窗口移動到反向投影圖像中灰度密度最大的區(qū)域了。 |
實現(xiàn)Meanshift的主要流程是︰
- 讀取視頻文件:cv.videoCapture()
- 感興趣區(qū)域設置:獲取第一幀圖像,并設置目標區(qū)域,即感興趣區(qū)域
- 計算直方圖:計算感興趣區(qū)域的HSV直方圖,并進行歸一化
- 目標追蹤︰設置窗口搜索停止條件,直方圖反向投影,進行目標追蹤,并在目標位置繪制矩形框
三、代碼
opencv API
cv2.meanShift(probImage, window, criteria)
參數(shù):
- probImage:ROI區(qū)域,即目標直方圖的反向投影
- window:初始搜索窗口,就是定義ROI的rect
- criteria:確定窗口搜索停止的準則,主要有迭代次數(shù)達到設置的最大值,窗口中心的漂移值大于某個設定的限值等
3.1 meanshift+固定框的代碼
import cv2 as cv
# 創(chuàng)建讀取視頻的對象
cap = cv.VideoCapture("E:\Python-Code/videodataset/enn.mp4")
# 獲取第一幀位置,并指定目標位置
ret, frame = cap.read()
c, r, h, w = 530, 160, 300, 320
track_window = (c, r, h, w)
# 指定感興趣區(qū)域
roi = frame[r:r + h, c:c + w]
# 計算直方圖
# 轉換色彩空間
hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
# 計算直方圖
roi_hist = cv.calcHist([hsv_roi], [0], None, [180], [0, 180])
# 歸一化
cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX)
# 目標追蹤
# 設置窗口搜索終止條件:最大迭代次數(shù),窗口中心漂移最小值
term_crit = (cv.TermCriteria_EPS | cv.TERM_CRITERIA_COUNT, 10, 1)
while True:
ret, frame = cap.read()
if ret:
# 計算直方圖的反向投影
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
# 進行meanshift追蹤
ret, track_window = cv.meanShift(dst, track_window, term_crit)
# 將追蹤的位置繪制在視頻上,并進行顯示
x, y, w, h = track_window
img = cv.rectangle(frame, (x, y), (x + w, y + h), 255, 2)
cv.imshow("frame", img)
if cv.waitKey(20) & 0xFF == ord('q'):
break
else:
break
# 資源釋放
cap.release()
cv.destroyAllWindows()
缺點:這種就是一開始設定了具體框的大小和位置,不能根據(jù)實際情況自己進行更改。
初始框的位置很重要。以Meanshift為例,它的工作原理是根據(jù)概率密度來尋找最大的密度區(qū)域,但如果我們一開始將跟蹤框放置在了一個直方圖反向投影圖中全黑的區(qū)域(密度為0),這會導致其無法正確向物體方向進行移動,從而導致卡死在那里。
我們已追蹤視頻的初始幀(第一幀)為例,我們假設想要跟蹤其中的一個物體,我們就得將跟蹤框放置到跟蹤物體周邊的區(qū)域才能讓程序正常運行,但我們其實很難知道一張圖片中跟蹤物體的具體位置。 舉個簡單的例子,比如我現(xiàn)在有一張圖片,我們要跟蹤圖片右下角的一個物體,但是我不知道這個物體的坐標范圍,所以我只能一次一次的去嘗試(在代碼中修改初始框的位置后,看看程序的運行情況)來保證代碼能夠正常運行,但這樣代碼的普適性很差,因為每當要更改跟蹤對象的時候,都需要反復的修改,才能應對當前的情況,這樣實在是有點麻煩。
3.2 優(yōu)化:meanshift+鼠標選擇
這里介紹一個函數(shù),起名為:cv2.selectROI,使用這個函數(shù),我們就能夠實現(xiàn)手動畫取我們的跟蹤框,其函數(shù)語法如下所示:
track_window=cv2.selectROI('frameName', frame)參數(shù):
- framename:顯示窗口的畫布名
- frame:具體的幀
import cv2
import numpy as np
# 讀取視頻
cap=cv2.VideoCapture('E:\Python-Code/videodataset/enn.mp4')
# 獲取第一幀位置,參數(shù)ret 為True 或者False,代表有沒有讀取到圖片 第二個參數(shù)frame表示截取到一幀的圖片
ret,frame=cap.read()
#我這里畫面太大了所以縮小點——但是縮小后我的就會報錯
# frame=cv2.resize(frame,None,None,fx=1/2,fy=1/2,interpolation=cv2.INTER_CUBIC)
#跟蹤框
track_window=cv2.selectROI('img', frame)
#獲得綠色的直方圖
# 轉換色彩空間
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])
# hist=cv2.calcHist([hsv],[0],[None],[180],[0,180])
# 歸一化
cv2.normalize(hist,hist,0,255,cv2.NORM_MINMAX)
# 目標追蹤
# 設置窗口搜索終止條件:最大迭代次數(shù),窗口中心漂移最小值
term_crit=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,10,1)
while True:
ret,frame=cap.read()
# frame = cv2.resize(frame, None, None, fx=1 / 2, fy=1 / 2, interpolation=cv2.INTER_CUBIC)
if ret== True:
# 計算直方圖的反向投影
hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1)
# 進行meanshift追蹤
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)
(x,y)=img.shape[:2]
cv2.imshow('img',img)
if cv2.waitKey(1)==ord('q'):
break
else:
break
# 資源釋放
cap.release()
cv2.destroyAllWindows()運行上面的代碼,會跳出一個窗口,窗口上顯示的就是我們載入的視頻的第一幀,我們用鼠標拖動,畫出我們要跟蹤的物體的位置
但是這里需要注意的是,這個函數(shù)每次調用只能畫一個矩形(C++版本中的OpenCV可以一次畫好多個),如果想畫好多個矩形的話,可以使用while循環(huán):
bboxes = []
colors = []
while 1:
bbox = cv2.selectROI('MultiTracker', frame)
bboxes.append(bbox)
colors.append((randint(0, 255), randint(0, 255), randint(0, 255)))
print("按下q鍵退出,按下其他鍵繼續(xù)畫下一個框")
if cv2.waitKey(0) & 0xFF==ord('q'):
break
print('選取的邊框為{}'.format(bboxes))但是完整的代碼并沒有跑通
3.3 meanshift+自己實現(xiàn)函數(shù)
這個效果最好,但是不懂為什么會這樣?為什么調用庫函數(shù)反而效果不好呢?
import math
import numpy as np
import cv2
def get_tr(img):
# 定義需要返回的參數(shù)
mouse_params = {'x': None, 'width': None, 'height': None,
'y': None, 'temp': None}
cv2.namedWindow('image')
# 鼠標框選操作函數(shù)
cv2.setMouseCallback('image', on_mouse, mouse_params)
cv2.imshow('image', img)
cv2.waitKey(0)
return [mouse_params['x'], mouse_params['y'], mouse_params['width'],
mouse_params['height']], mouse_params['temp']
def on_mouse(event, x, y, flags, param):
global img, point1
img2 = img.copy()
if event == cv2.EVENT_LBUTTONDOWN: # 左鍵點擊
point1 = (x, y)
cv2.circle(img2, point1, 10, (0, 255, 0), 5)
cv2.imshow('image', img2)
elif event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_LBUTTON): # 按住左鍵拖曳
cv2.rectangle(img2, point1, (x, y), (255, 0, 0), 5)
cv2.imshow('image', img2)
elif event == cv2.EVENT_LBUTTONUP: # 左鍵釋放
point2 = (x, y)
cv2.rectangle(img2, point1, point2, (0, 0, 255), 5)
cv2.imshow('image', img2)
# 返回框選矩形左上角點的坐標、矩形寬度、高度以及矩形包含的圖像
param['x'] = min(point1[0], point2[0])
param['y'] = min(point1[1], point2[1])
param['width'] = abs(point1[0] - point2[0])
param['height'] = abs(point1[1] - point2[1])
param['temp'] = img[param['y']:param['y'] + param['height'],
param['x']:param['x'] + param['width']]
def main():
global img
cap = cv2.VideoCapture("E:\Python-Code/videodataset/enn.mp4")
# 獲取視頻第一幀
ret, frame = cap.read()
img = frame
# 框選目標并返回相應信息:rect為四個信息,temp為框選出來的圖像
rect, temp = get_tr(img)
print(temp)
(a, b, c) = temp.shape
y = [a / 2, b / 2]
# 計算目標圖像的權值矩陣
m_wei = np.zeros((a, b))
for i in range(a):
for j in range(b):
z = (i - y[0]) ** 2 + (j - y[1]) ** 2
m_wei[i, j] = 1 - z / (y[0] ** 2 + y[1] ** 2)
# 計算目標權值直方圖
C = 1 / sum(sum(m_wei))
hist1 = np.zeros(16 ** 3)
for i in range(a):
for j in range(b):
q_b = math.floor(float(temp[i, j, 0]) / 16)
q_g = math.floor(float(temp[i, j, 1]) / 16)
q_r = math.floor(float(temp[i, j, 2]) / 16)
q_temp1 = q_r * 256 + q_g * 16 + q_b
hist1[int(q_temp1)] = hist1[int(q_temp1)] + m_wei[i, j]
hist1 = hist1 * C
# 接著讀取視頻并進行目標跟蹤
while (1):
ret, frame = cap.read()
if ret == True:
Img = frame
num = 0
Y = [1, 1]
# mean shift迭代
while (np.sqrt(Y[0] ** 2 + Y[1] ** 2) > 0.5) & (num < 20):
num = num + 1
# 計算候選區(qū)域直方圖
temp2 = Img[int(rect[1]):int(rect[1] + rect[3]), int(rect[0]):int(rect[0] + rect[2])]
hist2 = np.zeros(16 ** 3)
q_temp2 = np.zeros((a, b))
for i in range(a):
for j in range(b):
q_b = math.floor(float(temp2[i, j, 0]) / 16)
q_g = math.floor(float(temp2[i, j, 1]) / 16)
q_r = math.floor(float(temp2[i, j, 2]) / 16)
q_temp2[i, j] = q_r * 256 + q_g * 16 + q_b
hist2[int(q_temp2[i, j])] = hist2[int(q_temp2[i, j])] + m_wei[i, j]
hist2 = hist2 * C
w = np.zeros(16 ** 3)
for i in range(16 ** 3):
if hist2[i] != 0:
w[i] = math.sqrt(hist1[i] / hist2[i])
else:
w[i] = 0
sum_w = 0
sum_xw = [0, 0]
for i in range(a):
for j in range(b):
sum_w = sum_w + w[int(q_temp2[i, j])]
sum_xw = sum_xw + w[int(q_temp2[i, j])] * np.array([i - y[0], j - y[1]])
Y = sum_xw / sum_w
# 位置更新
rect[0] = rect[0] + Y[1]
rect[1] = rect[1] + Y[0]
v0 = int(rect[0])
v1 = int(rect[1])
v2 = int(rect[2])
v3 = int(rect[3])
pt1 = (v0, v1)
pt2 = (v0 + v2, v1 + v3)
# 畫矩形
IMG = cv2.rectangle(Img, pt1, pt2, (0, 0, 255), 2)
cv2.imshow('IMG', IMG)
k = cv2.waitKey(60) & 0xff
if k == 27:
break
else:
break
if __name__ == '__main__':
main()四、補充知識
4.1 直方圖
構建圖像的直方圖需要使用到函數(shù)cv2.calcHist,其常用函數(shù)語法如下所示:
hist=cv2.calcHist(images, channels, mask, histSize, ranges) images:輸入的圖像 channels:選擇圖像的通道,如果是三通道的話就可以是[0],[1],[2] mask:掩膜,是一個大小和image一樣的np數(shù)組,其中把需要處理的部分指定為1,不需要處理的部分指定為0,一般設置為None,如果有mask,會先對輸入圖像進行掩膜操作 histSize:使用多少個bin(柱子),一般為256,但如果是H值就是181 ranges:像素值的范圍,一般為[0,255]表示0~255,對于H通道而言就是[0,180]
需要注意的是,這里除了mask以外,其余的幾個參數(shù)都要加上[],如下所示:
hist=cv2.calcHist([img],[0],mask,[181],[0,180])
4.2 歸一化
這個時候我們還需要使用一種歸一化的方法來對彩色直方圖中的數(shù)量值進行規(guī)范化。 現(xiàn)有的直方圖中的數(shù)值為對應像素的數(shù)量,其中圖中出現(xiàn)數(shù)量最多的像素的數(shù)量值(最高的柱子對應的y軸數(shù)值)我們記為max的話,整個直方圖y方向上的取值范圍就是[0,max],我們需要把這個范圍縮減到[0,255],
這里我們需要使用到cv2.normalize函數(shù),函數(shù)主要語法如下所示:
cv2.normalize(src,dst, alpha,beta, norm_type) ·src-輸入數(shù)組。 ·dst-與SRC大小相同的輸出數(shù)組。 ·α-范數(shù)值在范圍歸一化的情況下歸一化到較低的范圍邊界。 ·β-上限范圍在范圍歸一化的情況下;它不用于范數(shù)歸一化。 ·范式-規(guī)范化類型(見下面詳細介紹)。
這里我們需要注意的是范式-規(guī)范化類型,這里有以下幾種選擇。
NORM_MINMAX:數(shù)組的數(shù)值被平移或縮放到一個指定的范圍,線性歸一化。 NORM_INF:歸一化數(shù)組的(切比雪夫距離)L∞范數(shù)(絕對值的最大值) NORM_L1: 歸一化數(shù)組的(曼哈頓距離)L1-范數(shù)(絕對值的和) NORM_L2: 歸一化數(shù)組的(歐幾里德距離)L2-范數(shù)
上面的名詞看起來很高大上,其實是很簡單,我們一一講解下。(不是很感興趣的只要看下第一個NORM_MINMAX即可,剩下的三個可以不看)
首先是NORM_MINMAX,這個是我們最常用的一種歸一化方法。舉個例子,我們上面提到的最高的柱子對應的y軸坐標為max,如果我們使用這種方法,想要縮放到的指定的范圍為[0,255],那么max就會直接被賦值為255,其余的柱子也會隨之一樣被壓縮(類似于相似三角形那樣的縮放感覺)。 沒錯,很簡單得就介紹完了一種,不是很想了解其他幾個的讀者可以直接跳過本小節(jié)剩下來的內容了,因為剩下三種不是很常用。
4.3 直方圖反投影
簡單來說,它會輸出與輸入圖像(待搜索)同樣大小的圖像,其中的每一個像素值代表了輸入圖像上對應點屬于目標對象(我們需要跟蹤的目標)的概率。用更簡單的話來解釋,輸出圖像中像素值越高(越白)的點就越可能代表我們要搜索的目標 (在輸入圖像所在的位置)。 而對于灰度圖而言,其只有一個通道,取值范圍為0到255,所以我們之前在歸一化的時候將直方圖的y軸坐標的取值范圍壓縮到了0-255的范圍內,就是為了這里可以直接賦值。
越暗的地方說明屬于跟蹤部分的可能性越低,越亮的地方屬于跟蹤部分的可能性越高。 這里使用到的函數(shù)為cv2.calcBackProject,函數(shù)語法如下所示:
dst=cv2.calcBackProject(image,channel,hist,range,scale) image:輸入圖像 channel:用來計算反向投影的通道數(shù),與產生直方圖對應的通道應一致 hist:作為輸入的直方圖 range:直方圖的取值范圍 scale:輸出圖像的縮放比,一般為1,保持與輸入圖像一樣的大小 dst:輸出圖像 注意:除了hist和scale外,其他的參數(shù)都要加上[]
例如:
dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1)
到此這篇關于傳統(tǒng)目標跟蹤——MeanShift算法的文章就介紹到這了,更多相關MeanShift跟蹤算法內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
用tensorflow實現(xiàn)彈性網(wǎng)絡回歸算法
這篇文章主要介紹了用tensorflow實現(xiàn)彈性網(wǎng)絡回歸算法2018-01-01
刪除DataFrame中值全為NaN或者包含有NaN的列或行方法
今天小編就為大家分享一篇刪除DataFrame中值全為NaN或者包含有NaN的列或行方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-11-11
利用Python腳本生成sitemap.xml的實現(xiàn)方法
最近項目中需要用腳本生成sitemap,中間學習了一下sitemap的格式和lxml庫的用法。把結果記錄一下,方便以后需要直接拿來用。下面這篇文章主要介紹了利用Python腳本生成sitemap.xml的實現(xiàn)方法,需要的朋友可以參考借鑒,一起來看看吧。2017-01-01

