這個該死的傢伙。我就知道他偷了我最後一罐啤酒!
對於一個男人來講,這些話永遠都不該說。但是當我關上冰箱門的時候,我憤怒地嘆息,感到厭惡,自言自語地說了這些。
你看,我花了12個小時寫了這篇將要發表的文章《PyImageSearch Gurus course》。我的腦子都糊掉了,像個半熟的攤雞蛋一樣,幾乎要從耳朵裡流出來了。當我深夜決定結束工作的時候,我只想放鬆一下,看看我最愛的電影——《侏羅紀公園》。同時喝著來自 Smuttynose 的最好的 IPA 冰啤,Smuttynose 是近來我非常喜歡的一家酒廠。
但是,昨天晚上來串門的該死的 James 喝掉了我最後一罐啤酒。
好吧,據稱。
我並不能證明任何我的猜測。實際上,我並沒有親眼看到他喝我的啤酒,因為我埋頭於膝上型電腦中,手指在鍵盤上跳動,興奮地敲擊出教程和文章。但是我感覺他就是嫌疑犯。他是我唯一會喝 IPA 的(前)朋友。
所以我做了一件任何男人都會做的事。
我在櫥櫃頂上安裝了一個樹莓派,來探測看他是不是打算再次偷啤酒。
過分了?
也許吧。
但是,我很看重我的啤酒。而且如果 James 再次嘗試偷我的啤酒的話,我會逮他個正著。
一篇關於運動檢測的系列文章(分為兩部分)
做一個用於家庭監控的運動檢測和追蹤系統,分兩部分,本文是第一篇。
本文接下來的部分,將會詳細介紹如何使用計算機視覺技術來建立一個用於家庭監控的基礎的運動檢測和追蹤系統。本例對預先錄製的視訊和網路攝像頭的實時資料流都可以工作;然而,我們將會在我們的筆記本/桌面電腦上進行開發。
在本系列的第二部分中,我會向你展示如何升級程式碼,使其可以在樹莓派和camera board上工作,以及如何擴充套件家庭監控系統,來捕捉任何檢測到的運動,並且上傳到你的個人Dropbox中。
也許到了最後,我們可以把 James 抓個正著。
一點關於背景移除的內容
背景移除是很多計算機視覺應用的關鍵內容。我們通過它來計算經過收費站的汽車個數。我們通過它來計算進進出出一間商店的人的個數。
同時我們使用它來進行運動檢測。
在本文開始寫程式碼之前,讓我告訴你,OpenCV 裡有很多很多方法來進行運動檢測、追蹤和分析。有一些非常簡單,而另外一些非常複雜。兩個初級的方法是某種形式的基於混合高斯模型的前景和背景分割:
- KaewTraKulPong 等人發表的《An improved adaptive background mixture model for real-time tracking with shadow detection>。這個方法可以通過
cv2.BackgroundSubtractorMOG
來使用。 - Zivkovic 提出的《Improved adaptive Gaussian mixture model for background subtraction》和《Efficient Adaptive Density Estimation per Image Pixel for the Task of Background Subtraction》。可以通過
cv2.BackgroundSubtractorMOG2
來使用。
在新版本的 OpenCV 中,我們有基於貝葉斯(概率)的前景和背景分割,是 Godbehere 等人在2012年的文章中實現的,《Visual Tracking of Human Visitors under Variable-Lighting Conditions for a Responsive Audio Art Installation》,我們可以在cv2.createBackgroundSubtractorGMG
中找到它的實現(然而我們需要等OpenCV 3的到來,才能使用它的全部功能。)
所有這些方法都涉及到從前景中分離背景(它們甚至提供相應的機制來讓我們辨別實際運動和陰影及關照的細微改變)!
為什麼這一點特別重要?為什麼我們這麼在意哪個畫素屬於前景哪個畫素屬於背景?
在運動檢測中,我們會做出如下的假設:
我們視訊流中的背景在連續的視訊幀內,多數時候應該是靜止不變的,因此如果我們可以建立背景模型,我們的就可以監視到顯著的變化。如果發生了顯著的變化,我們就可以檢測到它——通常這些變化和我們視訊中的運動有關。
顯然在現實世界中,我們這個假設比較容易失效。因為陰影、反色、光照條件以及環境中可能發生的其他變化,我們的背景可能會看上去變得非常不同,這會讓我們的演算法失效。所以為什麼最成功的背景移除/前景檢測系統需要固定安裝的相機以及控制光照條件。
上面我提到的方法,儘管非常強大,但同時計算非常耗時。而且我們最終的目標是在本系列的最後,把該系統部署在樹莓派上,因此我們最好可以堅持使用簡單的方法。我們將在未來的文章中回到這些強大的方法上,但是目前我們將保持簡單和高效。
用 Python 和 OpenCV 進行基礎的運動檢測和追蹤
好了,準備好幫助我開發一個家用監視系統來抓住那個偷啤酒的混蛋了麼? 開啟編輯器,新建一個檔案,命名為 motion_detector.py,然後讓我們開始寫程式碼吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 匯入必要的軟體包 import argparse import datetime import imutils import time import cv2 # 建立引數解析器並解析引數 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引數為None,那麼我們從攝像頭讀取資料 if args.get("video", None) is None: camera = cv2.VideoCapture(0) time.sleep(0.25) # 否則我們讀取一個視訊檔案 else: camera = cv2.VideoCapture(args["video"]) # 初始化視訊流的第一幀 firstFrame = None |
2-6行匯入了我們必要的軟體包。這些看上去都很熟悉,除了imutils
這個包,它提供了一組由我編寫的非常方便的函式,來讓我們更簡單的進行影象處理。如果你還沒有安裝 imutils 到你的系統,你可以通過pip來安裝:pip install imutils
下一步,我們在9-12行解析了命令列引數。我們定義了兩個選項。第一個,--video
,是可選的。它會指定一個路徑,指向一個預先錄製好的視訊檔案,我們可以檢測該視訊中的運動。如果你不提供視訊的路徑,那麼OpenCV會從你的攝像頭中來檢測運動。
我們同時還定義了--min-area
,它表示一個影象區域被看做實際運動的最小尺寸(以畫素為單位)。正如我接下來要講的那樣,我們會發現影象中比較小的區域變化會比較顯著,可能是因為噪點或是光線的變化。在實際中,這些小區域並不是實際的運動——所以我們定義一個最小的尺寸來對付和過濾掉這些假陽性(false-positives)結果。
15-21行獲取一個我們攝像機物件的引用。在這個例子中,沒有提供視訊路徑(15-17行),我們會取得一個攝像頭的引用。如果提供了一個視訊檔案路徑,那麼我們會在20-21行建立一個指向它的指標。
最後,我們以一個變數來結束這段程式碼,這個變數是firstFrame
。 能猜到firstFrame
是什麼嗎?
假設:視訊的第一幀不會包含運動,而僅僅是背景——因此我們可以使用第一幀來建立背景模型。 顯然我們此處建立的假設有些太大了。但是再說一次,我們的目標是要在樹莓派上執行這個系統,所以我們不能做的太複雜。正如你會在本文的結果一節所看到的那樣,當有人在屋裡走動的時候,我們可以輕易的檢測到運動並追蹤他們。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 遍歷視訊的每一幀 while True: # 獲取當前幀並初始化occupied/unoccupied文字 (grabbed, frame) = camera.read() text = "Unoccupied" # 如果不能抓取到一幀,說明我們到了視訊的結尾 if not grabbed: break # 調整該幀的大小,轉換為灰階影象並且對其進行高斯模糊 frame = imutils.resize(frame, width=500) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (21, 21), 0) # 如果第一幀是None,對其進行初始化 if firstFrame is None: firstFrame = gray continue |
現在我們已經獲取了視訊檔案/攝像頭資料流的引用,我們可以在第一行(原文第27行)開始遍歷每一幀了。
呼叫camera.read()
為我們返回一個2元組。元組的第一個值是grabbed
,表明是否成功從緩衝中讀取了frame
。元組的第二個值就是frame
它本身。
我們同時還定義了一個叫做 text
的字串,並對其進行初始化來表明我們正在監控的這個房間“沒有被佔領”(Unoccupied)。如果這個房間確實有活動,我們可以更新這個字串。
在這個例子中,如果沒有成功從視訊檔案中讀取一幀,我們會在10-11行(原文35-36行)跳出迴圈。
我們可以開始處理幀資料並準備進行運動分析(15-17行)。我們首先會調整它的大小到500畫素寬——沒有必要去直接處理視訊流中的大尺寸,原始影象。我們同樣會把圖片轉換為灰階影象,因為彩色資料對我們的運動檢測演算法沒有影響。最後,我們會使用高斯模糊來平滑我們的影象。
認識到即使是相鄰幀,也不是完全相同的這一點很重要!
由於數碼相機感測器的微小變化,沒有100%相同的兩幀資料——一些畫素肯定會有不同的強度值。也就是說,我們需要,並應用高斯平滑對一個11X11的區域的畫素強度進行平均。這能幫我們濾除可能使我們運動檢測演算法失效的高頻噪音。
正如我在上面提到的,我們需要通過某種方式對我們的影象進行背景建模。再一次的,我們會假設視訊的第一幀不包含任何運動,它是一個很好的例子,表明我們的背景是如何的。如果firstFrame
沒有初始化,我們會把它儲存然後繼續處理視訊的下一幀。(20-22行)
這裡有一個關於示例視訊第一幀的例子:
上面這一幀滿足我們的假設,視訊的第一幀僅僅是一個靜止的背景——沒有運動。
有了這個靜止的背景圖片,我們已經準備好實時運動檢測和追蹤了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# 計算當前幀和第一幀的不同 frameDelta = cv2.absdiff(firstFrame, gray) thresh = cv2.threshold(frameDelta, 25, 255, cv2.THRESH_BINARY)[1] # 擴充套件閥值影象填充孔洞,然後找到閥值影象上的輪廓 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 # 計算輪廓的邊界框,在當前幀中畫出該框 (x, y, w, h) = cv2.boundingRect(c) cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) text = "Occupied" |
現在我們已經從firstFrame
變數對背景進行了建模,我們可以利用它來計算起始幀和視訊流資料中後續新幀之間的不同。
計算兩幀的不同是一個簡單的減法,我們使用兩方相應的畫素強度差的絕對值。(第二行)
delta = |background_model – current_frame|
兩幀差值圖例如下:
注意到圖片的背景是如何變為黑色的。然而,包含運動的區域(比如包含我自己走過房間動作的區域)會更亮一些。這以為這兩幀差值大的地方是圖片中發生移動的區域。
我們隨後在第3行對frameDelta
進行閥值化來顯示圖片中畫素強度值有顯著變化的區域。如果差值小於25,我丟棄該畫素將其設定為黑色(例如,背景)。如果差值大於25,我們將其設定為白色(例如,前景)。閥值化的差值圖片如下:
再一次,注意到圖片的背景是黑色的,而前景(運動發生的位置)是白色的。 有了這個閥值化的圖片,只要簡單的進行實施輪廓檢測來找到白色區域的外輪廓線(第7行)
我們在第14行開始對輪廓線進行遍歷,在15行濾掉小的,不相關的輪廓。 如果輪廓面積比我們提供的--min-area
值大,我們會在前景和移動區域畫邊框線。(23-25行)。我們同樣會更新text
狀態字串來表示這個房間”被佔領“(Occupied)了
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 # 在當前幀上寫文字以及時間戳 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) 顯示當前幀並記錄使用者是否按下按鍵 cv2.imshow("Security Feed", frame) cv2.imshow("Thresh", thresh) cv2.imshow("Frame Delta", frameDelta) key = cv2.waitKey(1) & 0xFF # 如果q鍵被按下,跳出迴圈 if key == ord("q"): break # 清理攝像機資源並關閉開啟的視窗 camera.release() cv2.destroyAllWindows() |
11-13行顯示了我的工作成果,執行我們可以在視訊中看到是否檢測到了運動,使用幀差值和閥值影象我們可以除錯我們的指令碼。
注意:如果你下載了本文的原始碼並打算應用到你自己的視訊檔案上,你可能需要改變cv2.threshold
的值和--min-area
引數來獲得你所在光照環境下的最佳效果。
最後,22行和23行清理並釋放了視訊流的指標。
結果
顯然,我要確定我們的運動監測系統可以在James那個偷酒賊再次造訪的之前能夠正常工作——我們將在本系列第二篇文章中談到他。為了測試我們使用Python和OpenCV搭建的運動監測系統,我錄製了兩個視訊檔案。
第一個檔案是example_01.mp4
,監視了我公寓的正門,當門被開啟時完成檢測。第二個檔案是example_02.mp4
使用安裝在櫥櫃上的樹莓派錄製的。它監控廚房和客廳,當有人在其中走動的時候完成檢測。
讓我們給我們簡單的探測器一次嘗試的機會,開啟終端並執行下面指令:
1 |
$ python motion_detector.py --video videos/example_01.mp4 |
下圖是一個 gif 圖,顯示來自探測器的一些靜止幀資料。
注意到在門被開啟前沒有進行運動檢測——然後我們可以檢測到我自己從門中走過。你可以在這裡看到全部視訊:
http://www.youtube.com/embed/fi4LORwk8Fc?feature=oembed
現在,我安裝在用於監視廚房和客廳的攝像機表現如何呢?然我們一探究竟。輸入下面命令:
1 |
$ python motion_detector.py --video videos/example_02.mp4 |
來自第二個視訊檔案的結果樣本如下:
同樣,這裡是我們運動檢測結果的完整視訊:
http://www.youtube.com/embed/36j238XtcIE?feature=oembed
正如你看到的,我們的運動檢測系統儘管非常簡單,但表現還不錯!我們可以正常檢測到我進入客廳和離開房間。
然而,現實來講,結果還遠遠談不上完美。儘管只有一個人在屋內走動,我們卻得到了多個外框——這和理想狀態相差甚遠。而且我可以看到,微小的光線變化,比如陰影和牆面反射,都觸發了假陽性的運動檢測結果。
為了解決這些問題,我們依靠OpenCV中更加強大的背景移除方法,這些方法對陰影和少量的反射進行了處理。(我將在未來的文章中談到這些更為先進的背景移除/前景檢測方法)
但是於此同時,請考慮一下我們的最終目標
這個系統,儘管是在我們的筆記本/桌上型電腦系統上開發的,卻是為了要部署在樹莓派上,樹莓派的計算資源非常有限。因此,我們需要讓我們的運動檢測方法保持簡單和快速。我們的運動檢測系統並不完美,很不幸這是一個不利的方面,但是對於我們特定的專案,它仍然能夠很好的完成工作。
最後,如果你想要利用你的攝像頭的原始視訊流來進行運動檢測,空著--video
選項即可。
1 |
$ python motion_detector.py |
小結
通過本文,我們已經認識到我的朋友James是一個偷酒賊。真是個混蛋啊!
為了能抓他個人贓並獲,我們決定使用Python和OpenCV建立一個運動檢測和追蹤系統。這個系統可以獲取視訊流並分析它們獲取運動。考慮到我們所使用的方法,能夠得到可以接受的監測結果。
最終目標是要把本系統部署在樹莓派上,因此我們沒有依賴OpenCV中一些比較先進的背景移除方法。相反,我們依賴一個簡單,但合理高效的假設——視訊的第一幀僅僅包含我們想要建模的背景,而不包括其他任何東西。
在這個假設下,我們可以實施背景移除,檢測圖片中的運動,在檢測到運動的區域畫出輪廓框。
在這個關於運動檢測系列文章的第二部分,我們會更新程式碼使其在樹莓派上執行。
我們同樣會整合Dropbox API,允許我們監控家用監控系統並且當我們的系統檢測到運動時,獲取實時更新資料。
敬請期待!
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式