用 Python 和 OpenCV 检测和跟踪运动对象
這個(gè)該死的家伙。我就知道他偷了我最后一罐啤酒!
對(duì)于一個(gè)男人來(lái)講,這些話永遠(yuǎn)都不該說(shuō)。但是當(dāng)我關(guān)上冰箱門的時(shí)候,我憤怒地嘆息,感到厭惡,自言自語(yǔ)地說(shuō)了這些。
你看,我花了12個(gè)小時(shí)寫了這篇將要發(fā)表的文章《PyImageSearch Gurus course》。我的腦子都糊掉了,像個(gè)半熟的攤雞蛋一樣,幾乎要從耳朵里流出來(lái)了。當(dāng)我深夜決定結(jié)束工作的時(shí)候,我只想放松一下,看看我最愛的電影——《侏羅紀(jì)公園》。同時(shí)喝著來(lái)自 Smuttynose 的最好的 IPA 冰啤,Smuttynose 是近來(lái)我非常喜歡的一家酒廠。
但是,昨天晚上來(lái)串門的該死的 James 喝掉了我最后一罐啤酒。
好吧,據(jù)稱。
我并不能證明任何我的猜測(cè)。實(shí)際上,我并沒有親眼看到他喝我的啤酒,因?yàn)槲衣耦^于筆記本電腦中,手指在鍵盤上跳動(dòng),興奮地敲擊出教程和文章。但是我感覺他就是嫌疑犯。他是我唯一會(huì)喝 IPA 的(前)朋友。
所以我做了一件任何男人都會(huì)做的事。
我在櫥柜頂上安裝了一個(gè)樹莓派,來(lái)探測(cè)看他是不是打算再次偷啤酒。
過分了?
也許吧。
但是,我很看重我的啤酒。而且如果 James 再次嘗試偷我的啤酒的話,我會(huì)逮他個(gè)正著。
一篇關(guān)于運(yùn)動(dòng)檢測(cè)的系列文章(分為兩部分)
做一個(gè)用于家庭監(jiān)控的運(yùn)動(dòng)檢測(cè)和追蹤系統(tǒng),分兩部分,本文是第一篇。
本文接下來(lái)的部分,將會(huì)詳細(xì)介紹如何使用計(jì)算機(jī)視覺技術(shù)來(lái)建立一個(gè)用于家庭監(jiān)控的基礎(chǔ)的運(yùn)動(dòng)檢測(cè)和追蹤系統(tǒng)。本例對(duì)預(yù)先錄制的視頻和網(wǎng)絡(luò)攝像頭的實(shí)時(shí)數(shù)據(jù)流都可以工作;然而,我們將會(huì)在我們的筆記本/桌面電腦上進(jìn)行開發(fā)。
在本系列的第二部分中,我會(huì)向你展示如何升級(jí)代碼,使其可以在樹莓派和camera board上工作,以及如何擴(kuò)展家庭監(jiān)控系統(tǒng),來(lái)捕捉任何檢測(cè)到的運(yùn)動(dòng),并且上傳到你的個(gè)人Dropbox中。
也許到了最后,我們可以把 James 抓個(gè)正著。
一點(diǎn)關(guān)于背景移除的內(nèi)容
背景移除是很多計(jì)算機(jī)視覺應(yīng)用的關(guān)鍵內(nèi)容。我們通過它來(lái)計(jì)算經(jīng)過收費(fèi)站的汽車個(gè)數(shù)。我們通過它來(lái)計(jì)算進(jìn)進(jìn)出出一間商店的人的個(gè)數(shù)。
同時(shí)我們使用它來(lái)進(jìn)行運(yùn)動(dòng)檢測(cè)。
在本文開始寫代碼之前,讓我告訴你,OpenCV 里有很多很多方法來(lái)進(jìn)行運(yùn)動(dòng)檢測(cè)、追蹤和分析。有一些非常簡(jiǎn)單,而另外一些非常復(fù)雜。兩個(gè)初級(jí)的方法是某種形式的基于混合高斯模型的前景和背景分割:
在新版本的 OpenCV 中,我們有基于貝葉斯(概率)的前景和背景分割,是 Godbehere 等人在2012年的文章中實(shí)現(xiàn)的,《Visual Tracking of Human Visitors under Variable-Lighting Conditions for a Responsive Audio Art Installation》,我們可以在cv2.createBackgroundSubtractorGMG?中找到它的實(shí)現(xiàn)(然而我們需要等OpenCV 3的到來(lái),才能使用它的全部功能。)
所有這些方法都涉及到從前景中分離背景(它們甚至提供相應(yīng)的機(jī)制來(lái)讓我們辨別實(shí)際運(yùn)動(dòng)和陰影及關(guān)照的細(xì)微改變)!
為什么這一點(diǎn)特別重要?為什么我們這么在意哪個(gè)像素屬于前景哪個(gè)像素屬于背景?
在運(yùn)動(dòng)檢測(cè)中,我們會(huì)做出如下的假設(shè):
我們視頻流中的背景在連續(xù)的視頻幀內(nèi),多數(shù)時(shí)候應(yīng)該是靜止不變的,因此如果我們可以建立背景模型,我們的就可以監(jiān)視到顯著的變化。如果發(fā)生了顯著的變化,我們就可以檢測(cè)到它——通常這些變化和我們視頻中的運(yùn)動(dòng)有關(guān)。
顯然在現(xiàn)實(shí)世界中,我們這個(gè)假設(shè)比較容易失效。因?yàn)殛幱啊⒎瓷⒐庹諚l件以及環(huán)境中可能發(fā)生的其他變化,我們的背景可能會(huì)看上去變得非常不同,這會(huì)讓我們的算法失效。所以為什么最成功的背景移除/前景檢測(cè)系統(tǒng)需要固定安裝的相機(jī)以及控制光照條件。
上面我提到的方法,盡管非常強(qiáng)大,但同時(shí)計(jì)算非常耗時(shí)。而且我們最終的目標(biāo)是在本系列的最后,把該系統(tǒng)部署在樹莓派上,因此我們最好可以堅(jiān)持使用簡(jiǎn)單的方法。我們將在未來(lái)的文章中回到這些強(qiáng)大的方法上,但是目前我們將保持簡(jiǎn)單和高效。
用 Python 和 OpenCV 進(jìn)行基礎(chǔ)的運(yùn)動(dòng)檢測(cè)和追蹤
好了,準(zhǔn)備好幫助我開發(fā)一個(gè)家用監(jiān)視系統(tǒng)來(lái)抓住那個(gè)偷啤酒的混蛋了么? 打開編輯器,新建一個(gè)文件,命名為?motion_detector.py,然后讓我們開始寫代碼吧。
Python| 123456789101112131415161718192021222324 | # 導(dǎo)入必要的軟件包import argparseimport datetimeimport imutilsimport timeimport cv2# 創(chuàng)建參數(shù)解析器并解析參數(shù)ap = argparse.ArgumentParser()ap.add_argument("-v", "--video", help="path to the video file")ap.add_argument("-a", "--min-area", type=int, default=500, help="minimum area size")args = vars(ap.parse_args())# 如果video參數(shù)為None,那么我們從攝像頭讀取數(shù)據(jù)if args.get("video", None) is None:????camera = cv2.VideoCapture(0)????time.sleep(0.25)# 否則我們讀取一個(gè)視頻文件else:????camera = cv2.VideoCapture(args["video"])# 初始化視頻流的第一幀firstFrame = None |
2-6行導(dǎo)入了我們必要的軟件包。這些看上去都很熟悉,除了imutils這個(gè)包,它提供了一組由我編寫的非常方便的函數(shù),來(lái)讓我們更簡(jiǎn)單的進(jìn)行圖像處理。如果你還沒有安裝?imutils到你的系統(tǒng),你可以通過pip來(lái)安裝:pip install imutils
下一步,我們?cè)?span style="border: 0px; margin: 0px; padding: 0px;">9-12行解析了命令行參數(shù)。我們定義了兩個(gè)選項(xiàng)。第一個(gè),--video,是可選的。它會(huì)指定一個(gè)路徑,指向一個(gè)預(yù)先錄制好的視頻文件,我們可以檢測(cè)該視頻中的運(yùn)動(dòng)。如果你不提供視頻的路徑,那么OpenCV會(huì)從你的攝像頭中來(lái)檢測(cè)運(yùn)動(dòng)。
我們同時(shí)還定義了--min-area,它表示一個(gè)圖像區(qū)域被看做實(shí)際運(yùn)動(dòng)的最小尺寸(以像素為單位)。正如我接下來(lái)要講的那樣,我們會(huì)發(fā)現(xiàn)圖像中比較小的區(qū)域變化會(huì)比較顯著,可能是因?yàn)樵朦c(diǎn)或是光線的變化。在實(shí)際中,這些小區(qū)域并不是實(shí)際的運(yùn)動(dòng)——所以我們定義一個(gè)最小的尺寸來(lái)對(duì)付和過濾掉這些假陽(yáng)性(false-positives)結(jié)果。
15-21行獲取一個(gè)我們攝像機(jī)對(duì)象的引用。在這個(gè)例子中,沒有提供視頻路徑(15-17行),我們會(huì)取得一個(gè)攝像頭的引用。如果提供了一個(gè)視頻文件路徑,那么我們會(huì)在20-21行建立一個(gè)指向它的指針。
最后,我們以一個(gè)變量來(lái)結(jié)束這段代碼,這個(gè)變量是firstFrame。 能猜到firstFrame?是什么嗎?
假設(shè):視頻的第一幀不會(huì)包含運(yùn)動(dòng),而僅僅是背景——因此我們可以使用第一幀來(lái)建立背景模型。?顯然我們此處建立的假設(shè)有些太大了。但是再說(shuō)一次,我們的目標(biāo)是要在樹莓派上運(yùn)行這個(gè)系統(tǒng),所以我們不能做的太復(fù)雜。正如你會(huì)在本文的結(jié)果一節(jié)所看到的那樣,當(dāng)有人在屋里走動(dòng)的時(shí)候,我們可以輕易的檢測(cè)到運(yùn)動(dòng)并追蹤他們。
Python| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # 遍歷視頻的每一幀 while True: ????# 獲取當(dāng)前幀并初始化occupied/unoccupied文本 ????(grabbed, frame) = camera.read() ????text = "Unoccupied" ????# 如果不能抓取到一幀,說(shuō)明我們到了視頻的結(jié)尾 ????if not grabbed: ????????break ????# 調(diào)整該幀的大小,轉(zhuǎn)換為灰階圖像并且對(duì)其進(jìn)行高斯模糊 ????frame = imutils.resize(frame, width=500) ????gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) ????gray = cv2.GaussianBlur(gray, (21, 21), 0) ????# 如果第一幀是None,對(duì)其進(jìn)行初始化 ????if firstFrame is None: ????????firstFrame = gray ????????continue |
現(xiàn)在我們已經(jīng)獲取了視頻文件/攝像頭數(shù)據(jù)流的引用,我們可以在第一行(原文第27行)開始遍歷每一幀了。
調(diào)用camera.read()為我們返回一個(gè)2元組。元組的第一個(gè)值是grabbed,表明是否成功從緩沖中讀取了frame。元組的第二個(gè)值就是frame它本身。
我們同時(shí)還定義了一個(gè)叫做?text?的字符串,并對(duì)其進(jìn)行初始化來(lái)表明我們正在監(jiān)控的這個(gè)房間“沒有被占領(lǐng)”(Unoccupied)。如果這個(gè)房間確實(shí)有活動(dòng),我們可以更新這個(gè)字符串。
在這個(gè)例子中,如果沒有成功從視頻文件中讀取一幀,我們會(huì)在10-11行(原文35-36行)跳出循環(huán)。
我們可以開始處理幀數(shù)據(jù)并準(zhǔn)備進(jìn)行運(yùn)動(dòng)分析(15-17行)。我們首先會(huì)調(diào)整它的大小到500像素寬——沒有必要去直接處理視頻流中的大尺寸,原始圖像。我們同樣會(huì)把圖片轉(zhuǎn)換為灰階圖像,因?yàn)椴噬珨?shù)據(jù)對(duì)我們的運(yùn)動(dòng)檢測(cè)算法沒有影響。最后,我們會(huì)使用高斯模糊來(lái)平滑我們的圖像。
認(rèn)識(shí)到即使是相鄰幀,也不是完全相同的這一點(diǎn)很重要!
由于數(shù)碼相機(jī)傳感器的微小變化,沒有100%相同的兩幀數(shù)據(jù)——一些像素肯定會(huì)有不同的強(qiáng)度值。也就是說(shuō),我們需要,并應(yīng)用高斯平滑對(duì)一個(gè)11X11的區(qū)域的像素強(qiáng)度進(jìn)行平均。這能幫我們?yōu)V除可能使我們運(yùn)動(dòng)檢測(cè)算法失效的高頻噪音。
正如我在上面提到的,我們需要通過某種方式對(duì)我們的圖像進(jìn)行背景建模。再一次的,我們會(huì)假設(shè)視頻的第一幀不包含任何運(yùn)動(dòng),它是一個(gè)很好的例子,表明我們的背景是如何的。如果firstFrame沒有初始化,我們會(huì)把它保存然后繼續(xù)處理視頻的下一幀。(20-22行)
這里有一個(gè)關(guān)于示例視頻第一幀的例子:
上面這一幀滿足我們的假設(shè),視頻的第一幀僅僅是一個(gè)靜止的背景——沒有運(yùn)動(dòng)。
有了這個(gè)靜止的背景圖片,我們已經(jīng)準(zhǔn)備好實(shí)時(shí)運(yùn)動(dòng)檢測(cè)和追蹤了:
Python| 123456789101112131415161718192021 | # 計(jì)算當(dāng)前幀和第一幀的不同????frameDelta = cv2.absdiff(firstFrame, gray)????thresh = cv2.threshold(frameDelta, 25, 255, cv2.THRESH_BINARY)[1]????# 擴(kuò)展閥值圖像填充孔洞,然后找到閥值圖像上的輪廓????thresh = cv2.dilate(thresh, None, iterations=2)????(cnts, _) = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,????????cv2.CHAIN_APPROX_SIMPLE)????# 遍歷輪廓????for c in cnts:????????# if the contour is too small, ignore it????????if cv2.contourArea(c) < args["min_area"]:????????????continue????????# compute the bounding box for the contour, draw it on the frame,????????# and update the text????????# 計(jì)算輪廓的邊界框,在當(dāng)前幀中畫出該框????????(x, y, w, h) = cv2.boundingRect(c)????????cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)????????text = "Occupied" |
現(xiàn)在我們已經(jīng)從firstFrame變量對(duì)背景進(jìn)行了建模,我們可以利用它來(lái)計(jì)算起始幀和視頻流數(shù)據(jù)中后續(xù)新幀之間的不同。
計(jì)算兩幀的不同是一個(gè)簡(jiǎn)單的減法,我們使用兩方相應(yīng)的像素強(qiáng)度差的絕對(duì)值。(第二行)
delta = |background_model – current_frame|
兩幀差值圖例如下:
注意到圖片的背景是如何變?yōu)楹谏摹H欢?#xff0c;包含運(yùn)動(dòng)的區(qū)域(比如包含我自己走過房間動(dòng)作的區(qū)域)會(huì)更亮一些。這以為這兩幀差值大的地方是圖片中發(fā)生移動(dòng)的區(qū)域。
我們隨后在第3行對(duì)frameDelta進(jìn)行閥值化來(lái)顯示圖片中像素強(qiáng)度值有顯著變化的區(qū)域。如果差值小于25,我丟棄該像素將其設(shè)置為黑色(例如,背景)。如果差值大于25,我們將其設(shè)定為白色(例如,前景)。閥值化的差值圖片如下:
再一次,注意到圖片的背景是黑色的,而前景(運(yùn)動(dòng)發(fā)生的位置)是白色的。 有了這個(gè)閥值化的圖片,只要簡(jiǎn)單的進(jìn)行實(shí)施輪廓檢測(cè)來(lái)找到白色區(qū)域的外輪廓線(第7行)
我們?cè)诘?4行開始對(duì)輪廓線進(jìn)行遍歷,在15行濾掉小的,不相關(guān)的輪廓。 如果輪廓面積比我們提供的--min-area值大,我們會(huì)在前景和移動(dòng)區(qū)域畫邊框線。(23-25行)。我們同樣會(huì)更新text狀態(tài)字符串來(lái)表示這個(gè)房間”被占領(lǐng)“(Occupied)了
Python| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # draw the text and timestamp on the frame ????# 在當(dāng)前幀上寫文字以及時(shí)間戳 ????cv2.putText(frame, "Room Status: {}".format(text), (10, 20), ????????cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) ????cv2.putText(frame, datetime.datetime.now().strftime("%A %d %B %Y %I:%M:%S%p"), ????????(10, frame.shape[0] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 0, 255), 1) ????顯示當(dāng)前幀并記錄用戶是否按下按鍵 ????cv2.imshow("Security Feed", frame) ????cv2.imshow("Thresh", thresh) ????cv2.imshow("Frame Delta", frameDelta) ????key = cv2.waitKey(1) & 0xFF ????# 如果q鍵被按下,跳出循環(huán) ????if key == ord("q"): ????????break # 清理攝像機(jī)資源并關(guān)閉打開的窗口 camera.release() cv2.destroyAllWindows() |
11-13行顯示了我的工作成果,運(yùn)行我們可以在視頻中看到是否檢測(cè)到了運(yùn)動(dòng),使用幀差值和閥值圖像我們可以調(diào)試我們的腳本。
注意:如果你下載了本文的源代碼并打算應(yīng)用到你自己的視頻文件上,你可能需要改變cv2.threshold?的值和--min-area?參數(shù)來(lái)獲得你所在光照環(huán)境下的最佳效果。
最后,22行和23行清理并釋放了視頻流的指針。
結(jié)果
顯然,我要確定我們的運(yùn)動(dòng)監(jiān)測(cè)系統(tǒng)可以在James那個(gè)偷酒賊再次造訪的之前能夠正常工作——我們將在本系列第二篇文章中談到他。為了測(cè)試我們使用Python和OpenCV搭建的運(yùn)動(dòng)監(jiān)測(cè)系統(tǒng),我錄制了兩個(gè)視頻文件。
第一個(gè)文件是example_01.mp4?,監(jiān)視了我公寓的正門,當(dāng)門被打開時(shí)完成檢測(cè)。第二個(gè)文件是example_02.mp4?使用安裝在櫥柜上的樹莓派錄制的。它監(jiān)控廚房和客廳,當(dāng)有人在其中走動(dòng)的時(shí)候完成檢測(cè)。
讓我們給我們簡(jiǎn)單的探測(cè)器一次嘗試的機(jī)會(huì),打開終端并執(zhí)行下面指令:
Python| 1 | $ python motion_detector.py --video videos/example_01.mp4 |
下圖是一個(gè) gif 圖,顯示來(lái)自探測(cè)器的一些靜止幀數(shù)據(jù)。
注意到在門被打開前沒有進(jìn)行運(yùn)動(dòng)檢測(cè)——然后我們可以檢測(cè)到我自己從門中走過。你可以在這里看到全部視頻:
http://www.youtube.com/embed/fi4LORwk8Fc?feature=oembed
現(xiàn)在,我安裝在用于監(jiān)視廚房和客廳的攝像機(jī)表現(xiàn)如何呢?然我們一探究竟。輸入下面命令:
Python| 1 | $ python motion_detector.py --video videos/example_02.mp4 |
來(lái)自第二個(gè)視頻文件的結(jié)果樣本如下:
同樣,這里是我們運(yùn)動(dòng)檢測(cè)結(jié)果的完整視頻:
http://www.youtube.com/embed/36j238XtcIE?feature=oembed
正如你看到的,我們的運(yùn)動(dòng)檢測(cè)系統(tǒng)盡管非常簡(jiǎn)單,但表現(xiàn)還不錯(cuò)!我們可以正常檢測(cè)到我進(jìn)入客廳和離開房間。
然而,現(xiàn)實(shí)來(lái)講,結(jié)果還遠(yuǎn)遠(yuǎn)談不上完美。盡管只有一個(gè)人在屋內(nèi)走動(dòng),我們卻得到了多個(gè)外框——這和理想狀態(tài)相差甚遠(yuǎn)。而且我可以看到,微小的光線變化,比如陰影和墻面反射,都觸發(fā)了假陽(yáng)性的運(yùn)動(dòng)檢測(cè)結(jié)果。
為了解決這些問題,我們依靠OpenCV中更加強(qiáng)大的背景移除方法,這些方法對(duì)陰影和少量的反射進(jìn)行了處理。(我將在未來(lái)的文章中談到這些更為先進(jìn)的背景移除/前景檢測(cè)方法)
但是于此同時(shí),請(qǐng)考慮一下我們的最終目標(biāo)
這個(gè)系統(tǒng),盡管是在我們的筆記本/臺(tái)式機(jī)系統(tǒng)上開發(fā)的,卻是為了要部署在樹莓派上,樹莓派的計(jì)算資源非常有限。因此,我們需要讓我們的運(yùn)動(dòng)檢測(cè)方法保持簡(jiǎn)單和快速。我們的運(yùn)動(dòng)檢測(cè)系統(tǒng)并不完美,很不幸這是一個(gè)不利的方面,但是對(duì)于我們特定的項(xiàng)目,它仍然能夠很好的完成工作。
最后,如果你想要利用你的攝像頭的原始視頻流來(lái)進(jìn)行運(yùn)動(dòng)檢測(cè),空著--video選項(xiàng)即可。
Python| 1 | $ python motion_detector.py |
小結(jié)
通過本文,我們已經(jīng)認(rèn)識(shí)到我的朋友James是一個(gè)偷酒賊。真是個(gè)混蛋啊!
為了能抓他個(gè)人贓并獲,我們決定使用Python和OpenCV建立一個(gè)運(yùn)動(dòng)檢測(cè)和追蹤系統(tǒng)。這個(gè)系統(tǒng)可以獲取視頻流并分析它們獲取運(yùn)動(dòng)。考慮到我們所使用的方法,能夠得到可以接受的監(jiān)測(cè)結(jié)果。
最終目標(biāo)是要把本系統(tǒng)部署在樹莓派上,因此我們沒有依賴OpenCV中一些比較先進(jìn)的背景移除方法。相反,我們依賴一個(gè)簡(jiǎn)單,但合理高效的假設(shè)——視頻的第一幀僅僅包含我們想要建模的背景,而不包括其他任何東西。
在這個(gè)假設(shè)下,我們可以實(shí)施背景移除,檢測(cè)圖片中的運(yùn)動(dòng),在檢測(cè)到運(yùn)動(dòng)的區(qū)域畫出輪廓框。
在這個(gè)關(guān)于運(yùn)動(dòng)檢測(cè)系列文章的第二部分,我們會(huì)更新代碼使其在樹莓派上運(yùn)行。
我們同樣會(huì)集成Dropbox API,允許我們監(jiān)控家用監(jiān)控系統(tǒng)并且當(dāng)我們的系統(tǒng)檢測(cè)到運(yùn)動(dòng)時(shí),獲取實(shí)時(shí)更新數(shù)據(jù)。
by??伯樂在線?-?艾凌風(fēng)
from:?http://python.jobbole.com/81593/
總結(jié)
以上是生活随笔為你收集整理的用 Python 和 OpenCV 检测和跟踪运动对象的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 不知道被谁删了微信好友?用 Python
- 下一篇: 用Python Pandas处理亿级数据