全解析MeanShift傳統(tǒng)目標(biāo)跟蹤算法
一、均值漂移(MeanShift)
該算法尋找離散樣本的最大密度,并且重新計(jì)算下一幀的最大密度,這個(gè)算法的特點(diǎn)就是可以給出目標(biāo)移動(dòng)的方向。
meanshift算法的原理很簡單。假設(shè)你有一堆點(diǎn)集,還有一個(gè)小的窗口,這個(gè)窗口可能是圓形的,現(xiàn)在你可能要移動(dòng)這個(gè)窗口到點(diǎn)集密度最大的區(qū)域當(dāng)中。
最開始的窗口是藍(lán)色圓環(huán)的區(qū)域,命名為C1。藍(lán)色圓環(huán)的圓心用一個(gè)藍(lán)色的矩形標(biāo)注,命名為C1_o。
而窗口中所有點(diǎn)的點(diǎn)集構(gòu)成的質(zhì)心在藍(lán)色圓形點(diǎn)C1_r處,顯然圓環(huán)的形心和質(zhì)心并不重合。所以,移動(dòng)藍(lán)色的窗口,使得形心與之前得到的質(zhì)心重合。在新移動(dòng)后的圓環(huán)的區(qū)域當(dāng)中再次尋找圓環(huán)當(dāng)中所包圍點(diǎn)集的質(zhì)心,然后再次移動(dòng),通常情況下,形心和質(zhì)心是不重合的。不斷執(zhí)行上面的移動(dòng)過程,直到形心和質(zhì)心大致重合結(jié)束。這樣,最后圓形的窗口會(huì)落到像素分布最大的地方,也就是圖中的綠色圈,命名為C2。
meanshift算法除了應(yīng)用在視頻追蹤當(dāng)中,在聚類,平滑等等各種涉及到數(shù)據(jù)以及非監(jiān)督學(xué)習(xí)的場合當(dāng)中均有重要應(yīng)用,是一個(gè)應(yīng)用廣泛的算法。
如果不知道預(yù)先要跟蹤的目標(biāo),就可以采用這種巧妙地辦法,加設(shè)定條件,使能動(dòng)態(tài)的開始跟蹤(和停止跟蹤)視頻的某些區(qū)域,(如可以采用預(yù)先訓(xùn)練好的SVM進(jìn)行目標(biāo)的檢測,然后開始使用均值漂移MeanShift跟蹤檢測到的目標(biāo))
所以一般是分兩個(gè)步驟:1.標(biāo)記感興趣區(qū)域 2.跟蹤該區(qū)域
二、流程
圖像是一個(gè)矩陣信息,如何在一個(gè)視頻當(dāng)中使用meanshift算法來追蹤一個(gè)運(yùn)動(dòng)的物體呢?大致流程如下:
meanshift流程 |
1.首先在圖像上選定一個(gè)目標(biāo)區(qū)域 |
2.計(jì)算選定區(qū)域的直方圖分布,一般是HSV色彩空間的直方圖 |
3.對(duì)下一幀圖像 b 同樣計(jì)算直方圖分布 |
4.計(jì)算圖像 b 當(dāng)中與選定區(qū)域直方圖分布最為相似的區(qū)域,使用meanshift算法將選定區(qū)域沿著最為相似的部分進(jìn)行移動(dòng),直到找到最相似的區(qū)域,便完成了在圖像b中的目標(biāo)追蹤。 |
5.重復(fù)3到4的過程,就完成整個(gè)視頻目標(biāo)追蹤。 |
通常情況下我們使用直方圖反向投影得到的圖像和第一幀目標(biāo)對(duì)象的起始位置,當(dāng)目標(biāo)對(duì)象的移動(dòng)會(huì)反映到直方圖反向投影圖中,meanshift算法就把我們的窗口移動(dòng)到反向投影圖像中灰度密度最大的區(qū)域了。 |
實(shí)現(xiàn)Meanshift的主要流程是︰
- 讀取視頻文件:cv.videoCapture()
- 感興趣區(qū)域設(shè)置:獲取第一幀圖像,并設(shè)置目標(biāo)區(qū)域,即感興趣區(qū)域
- 計(jì)算直方圖:計(jì)算感興趣區(qū)域的HSV直方圖,并進(jìn)行歸一化
- 目標(biāo)追蹤︰設(shè)置窗口搜索停止條件,直方圖反向投影,進(jìn)行目標(biāo)追蹤,并在目標(biāo)位置繪制矩形框
三、代碼
opencv API
cv2.meanShift(probImage, window, criteria)
參數(shù):
- probImage:ROI區(qū)域,即目標(biāo)直方圖的反向投影
- window:初始搜索窗口,就是定義ROI的rect
- criteria:確定窗口搜索停止的準(zhǔn)則,主要有迭代次數(shù)達(dá)到設(shè)置的最大值,窗口中心的漂移值大于某個(gè)設(shè)定的限值等
3.1 meanshift+固定框的代碼
import cv2 as cv # 創(chuàng)建讀取視頻的對(duì)象 cap = cv.VideoCapture("E:\Python-Code/videodataset/enn.mp4") # 獲取第一幀位置,并指定目標(biāo)位置 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] # 計(jì)算直方圖 # 轉(zhuǎn)換色彩空間 hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV) # 計(jì)算直方圖 roi_hist = cv.calcHist([hsv_roi], [0], None, [180], [0, 180]) # 歸一化 cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX) # 目標(biāo)追蹤 # 設(shè)置窗口搜索終止條件:最大迭代次數(shù),窗口中心漂移最小值 term_crit = (cv.TermCriteria_EPS | cv.TERM_CRITERIA_COUNT, 10, 1) while True: ret, frame = cap.read() if ret: # 計(jì)算直方圖的反向投影 hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV) dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1) # 進(jìn)行meanshift追蹤 ret, track_window = cv.meanShift(dst, track_window, term_crit) # 將追蹤的位置繪制在視頻上,并進(jìn)行顯示 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()
缺點(diǎn):這種就是一開始設(shè)定了具體框的大小和位置,不能根據(jù)實(shí)際情況自己進(jìn)行更改。
初始框的位置很重要。以Meanshift為例,它的工作原理是根據(jù)概率密度來尋找最大的密度區(qū)域,但如果我們一開始將跟蹤框放置在了一個(gè)直方圖反向投影圖中全黑的區(qū)域(密度為0),這會(huì)導(dǎo)致其無法正確向物體方向進(jìn)行移動(dòng),從而導(dǎo)致卡死在那里。
我們已追蹤視頻的初始幀(第一幀)為例,我們假設(shè)想要跟蹤其中的一個(gè)物體,我們就得將跟蹤框放置到跟蹤物體周邊的區(qū)域才能讓程序正常運(yùn)行,但我們其實(shí)很難知道一張圖片中跟蹤物體的具體位置。 舉個(gè)簡單的例子,比如我現(xiàn)在有一張圖片,我們要跟蹤圖片右下角的一個(gè)物體,但是我不知道這個(gè)物體的坐標(biāo)范圍,所以我只能一次一次的去嘗試(在代碼中修改初始框的位置后,看看程序的運(yùn)行情況)來保證代碼能夠正常運(yùn)行,但這樣代碼的普適性很差,因?yàn)槊慨?dāng)要更改跟蹤對(duì)象的時(shí)候,都需要反復(fù)的修改,才能應(yīng)對(duì)當(dāng)前的情況,這樣實(shí)在是有點(diǎn)麻煩。
3.2 優(yōu)化:meanshift+鼠標(biāo)選擇
這里介紹一個(gè)函數(shù),起名為:cv2.selectROI,使用這個(gè)函數(shù),我們就能夠?qū)崿F(xiàn)手動(dòng)畫取我們的跟蹤框,其函數(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,代表有沒有讀取到圖片 第二個(gè)參數(shù)frame表示截取到一幀的圖片 ret,frame=cap.read() #我這里畫面太大了所以縮小點(diǎn)——但是縮小后我的就會(huì)報(bào)錯(cuò) # frame=cv2.resize(frame,None,None,fx=1/2,fy=1/2,interpolation=cv2.INTER_CUBIC) #跟蹤框 track_window=cv2.selectROI('img', frame) #獲得綠色的直方圖 # 轉(zhuǎn)換色彩空間 hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) mask=cv2.inRange(hsv,np.array((35,43,46)),np.array((77,255,255))) # 計(jì)算直方圖 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) # 目標(biāo)追蹤 # 設(shè)置窗口搜索終止條件:最大迭代次數(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: # 計(jì)算直方圖的反向投影 hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1) # 進(jìn)行meanshift追蹤 ret,track_window=cv2.meanShift(dst,track_window,term_crit) # 將追蹤的位置繪制在視頻上,并進(jìn)行顯示 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()
運(yùn)行上面的代碼,會(huì)跳出一個(gè)窗口,窗口上顯示的就是我們載入的視頻的第一幀,我們用鼠標(biāo)拖動(dòng),畫出我們要跟蹤的物體的位置
但是這里需要注意的是,這個(gè)函數(shù)每次調(diào)用只能畫一個(gè)矩形(C++版本中的OpenCV可以一次畫好多個(gè)),如果想畫好多個(gè)矩形的話,可以使用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ù)畫下一個(gè)框") if cv2.waitKey(0) & 0xFF==ord('q'): break print('選取的邊框?yàn)閧}'.format(bboxes))
但是完整的代碼并沒有跑通
3.3 meanshift+自己實(shí)現(xiàn)函數(shù)
這個(gè)效果最好,但是不懂為什么會(huì)這樣?為什么調(diào)用庫函數(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') # 鼠標(biāo)框選操作函數(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: # 左鍵點(diǎn)擊 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) # 返回框選矩形左上角點(diǎn)的坐標(biāo)、矩形寬度、高度以及矩形包含的圖像 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 # 框選目標(biāo)并返回相應(yīng)信息:rect為四個(gè)信息,temp為框選出來的圖像 rect, temp = get_tr(img) print(temp) (a, b, c) = temp.shape y = [a / 2, b / 2] # 計(jì)算目標(biāo)圖像的權(quán)值矩陣 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) # 計(jì)算目標(biāo)權(quán)值直方圖 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 # 接著讀取視頻并進(jìn)行目標(biāo)跟蹤 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 # 計(jì)算候選區(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()
四、補(bǔ)充知識(shí)
4.1 直方圖
構(gòu)建圖像的直方圖需要使用到函數(shù)cv2.calcHist,其常用函數(shù)語法如下所示:
hist=cv2.calcHist(images, channels, mask, histSize, ranges) images:輸入的圖像 channels:選擇圖像的通道,如果是三通道的話就可以是[0],[1],[2] mask:掩膜,是一個(gè)大小和image一樣的np數(shù)組,其中把需要處理的部分指定為1,不需要處理的部分指定為0,一般設(shè)置為None,如果有mask,會(huì)先對(duì)輸入圖像進(jìn)行掩膜操作 histSize:使用多少個(gè)bin(柱子),一般為256,但如果是H值就是181 ranges:像素值的范圍,一般為[0,255]表示0~255,對(duì)于H通道而言就是[0,180]
需要注意的是,這里除了mask以外,其余的幾個(gè)參數(shù)都要加上[],如下所示:
hist=cv2.calcHist([img],[0],mask,[181],[0,180])
4.2 歸一化
這個(gè)時(shí)候我們還需要使用一種歸一化的方法來對(duì)彩色直方圖中的數(shù)量值進(jìn)行規(guī)范化。 現(xiàn)有的直方圖中的數(shù)值為對(duì)應(yīng)像素的數(shù)量,其中圖中出現(xiàn)數(shù)量最多的像素的數(shù)量值(最高的柱子對(duì)應(yīng)的y軸數(shù)值)我們記為max的話,整個(gè)直方圖y方向上的取值范圍就是[0,max],我們需要把這個(gè)范圍縮減到[0,255],
這里我們需要使用到cv2.normalize函數(shù),函數(shù)主要語法如下所示:
cv2.normalize(src,dst, alpha,beta, norm_type) ·src-輸入數(shù)組。 ·dst-與SRC大小相同的輸出數(shù)組。 ·α-范數(shù)值在范圍歸一化的情況下歸一化到較低的范圍邊界。 ·β-上限范圍在范圍歸一化的情況下;它不用于范數(shù)歸一化。 ·范式-規(guī)范化類型(見下面詳細(xì)介紹)。
這里我們需要注意的是范式-規(guī)范化類型,這里有以下幾種選擇。
NORM_MINMAX:數(shù)組的數(shù)值被平移或縮放到一個(gè)指定的范圍,線性歸一化。 NORM_INF:歸一化數(shù)組的(切比雪夫距離)L∞范數(shù)(絕對(duì)值的最大值) NORM_L1: 歸一化數(shù)組的(曼哈頓距離)L1-范數(shù)(絕對(duì)值的和) NORM_L2: 歸一化數(shù)組的(歐幾里德距離)L2-范數(shù)
上面的名詞看起來很高大上,其實(shí)是很簡單,我們一一講解下。(不是很感興趣的只要看下第一個(gè)NORM_MINMAX即可,剩下的三個(gè)可以不看)
首先是NORM_MINMAX,這個(gè)是我們最常用的一種歸一化方法。舉個(gè)例子,我們上面提到的最高的柱子對(duì)應(yīng)的y軸坐標(biāo)為max,如果我們使用這種方法,想要縮放到的指定的范圍為[0,255],那么max就會(huì)直接被賦值為255,其余的柱子也會(huì)隨之一樣被壓縮(類似于相似三角形那樣的縮放感覺)。 沒錯(cuò),很簡單得就介紹完了一種,不是很想了解其他幾個(gè)的讀者可以直接跳過本小節(jié)剩下來的內(nèi)容了,因?yàn)槭O氯N不是很常用。
4.3 直方圖反投影
簡單來說,它會(huì)輸出與輸入圖像(待搜索)同樣大小的圖像,其中的每一個(gè)像素值代表了輸入圖像上對(duì)應(yīng)點(diǎn)屬于目標(biāo)對(duì)象(我們需要跟蹤的目標(biāo))的概率。用更簡單的話來解釋,輸出圖像中像素值越高(越白)的點(diǎn)就越可能代表我們要搜索的目標(biāo) (在輸入圖像所在的位置)。 而對(duì)于灰度圖而言,其只有一個(gè)通道,取值范圍為0到255,所以我們之前在歸一化的時(shí)候?qū)⒅狈綀D的y軸坐標(biāo)的取值范圍壓縮到了0-255的范圍內(nèi),就是為了這里可以直接賦值。
越暗的地方說明屬于跟蹤部分的可能性越低,越亮的地方屬于跟蹤部分的可能性越高。 這里使用到的函數(shù)為cv2.calcBackProject,函數(shù)語法如下所示:
dst=cv2.calcBackProject(image,channel,hist,range,scale) image:輸入圖像 channel:用來計(jì)算反向投影的通道數(shù),與產(chǎn)生直方圖對(duì)應(yīng)的通道應(yīng)一致 hist:作為輸入的直方圖 range:直方圖的取值范圍 scale:輸出圖像的縮放比,一般為1,保持與輸入圖像一樣的大小 dst:輸出圖像 注意:除了hist和scale外,其他的參數(shù)都要加上[]
例如:
dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1)
到此這篇關(guān)于傳統(tǒng)目標(biāo)跟蹤——MeanShift算法的文章就介紹到這了,更多相關(guān)MeanShift跟蹤算法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
用tensorflow實(shí)現(xiàn)彈性網(wǎng)絡(luò)回歸算法
這篇文章主要介紹了用tensorflow實(shí)現(xiàn)彈性網(wǎng)絡(luò)回歸算法2018-01-01Python實(shí)現(xiàn)操作Redis的高級(jí)用法分享
redis-py是Python操作Redis的第三方庫,它提供了與Redis服務(wù)器交互的API,本文為大家介紹了Python利用redis-py操作Redis的高級(jí)用法,需要的可以收藏一下2023-05-05Django Auth應(yīng)用實(shí)現(xiàn)用戶身份認(rèn)證
Django Auth 應(yīng)用一般用在用戶的登錄注冊(cè)上,用于判斷當(dāng)前的用戶是否合法。本文將介紹Auth的另一個(gè)功能,即認(rèn)證用戶身份,感興趣的同學(xué)可以關(guān)注一下2021-12-12python的三種等待方式及優(yōu)缺點(diǎn)小結(jié)
這篇文章主要介紹了python的三種等待方式及優(yōu)缺點(diǎn)的相關(guān)資料,三種等待元素加載的方法分別是強(qiáng)制等待、隱式等待和顯式等待,并詳細(xì)比較了它們的優(yōu)缺點(diǎn),需要的朋友可以參考下2024-12-12刪除DataFrame中值全為NaN或者包含有NaN的列或行方法
今天小編就為大家分享一篇?jiǎng)h除DataFrame中值全為NaN或者包含有NaN的列或行方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-11-11利用Python腳本生成sitemap.xml的實(shí)現(xiàn)方法
最近項(xiàng)目中需要用腳本生成sitemap,中間學(xué)習(xí)了一下sitemap的格式和lxml庫的用法。把結(jié)果記錄一下,方便以后需要直接拿來用。下面這篇文章主要介紹了利用Python腳本生成sitemap.xml的實(shí)現(xiàn)方法,需要的朋友可以參考借鑒,一起來看看吧。2017-01-01