哇,上週那篇關於做一個基本運動檢測系統的文章真是贊。寫這篇文章很有樂趣,而且從像您一樣的讀者那裡獲得反饋,使我的努力變得很值得。
對於那些剛看到這篇文章的朋友,上週那篇文章是關於使用計算機視覺來建立一個運動檢測系統,其動機是因為我的朋友James,他罪惡的雙手伸進了我的冰箱,偷走了我最後一罐令人垂涎的啤酒。因為我不能證明是他乾的,所以我想看看我是不是能夠利用計算機視覺和樹莓派,當他再次嘗試偷走我的啤酒的時候當場抓獲他。
您將在本文的最後看到,我們要建造的家用監控和運動檢測系統不僅炫酷又簡約,而且針對我們這個特定的目標還非常的強大。
今天我們將要擴充套件我們的基礎運動檢測方法,並且:
- 讓我們的運動檢測系統變得健壯一些,這樣它就可以連續工作一整天,不那麼容易受光線變化所影響。
- 更新我們的程式碼,讓我們的家用監控系統可以在樹莓派上執行。
- 整合 Dropbox API,使得 Python 指令碼可以自動把安保圖片上傳到我們的 Dropbox 賬戶中。在本文中,我們會看到很多程式碼,請做好準備。但是我們也會學到很多東西。更重要的是,在本文的最後,你將擁有一個你自己的,可以執行的樹莓派家用監控系統。
你可以在下面找到全部的示例視訊以及一些其他的例子。
視訊地址:http://www.youtube.com/embed/BhD1aDEV-kg
OpenCV and Python 版本 為了執行這個例子,你需要 Python 2.7 和 OpenCV 2.4.X.
在開始前,你需要:
動起來,讓我們把必要的東西都搞定。我會假設你已經有了一個樹莓派和 camera board(攝像頭模組)。
你也已經在樹莓派上安裝了 OpenCV 並且可以通過 OpenCV 獲取樹莓派的視訊流。我同樣還會假設你已經閱讀並且熟悉了上週關於建造一個基礎運動監測系統這篇文章。
最後,如果你想要上傳你的家庭安保圖片到個人 Dropbo x賬戶中,你需要到 Dropbox Core API 註冊並獲取你的公有和私有API keys,但接入Dropbox API 並不是本教程所必需的,只是一個錦上添花的東西。
除此之外,我們需要用pip-install安裝一個額外的包。
如果你沒有安裝我的 imutils
包,你需要從 GitHub 獲取或者通過 pip install imutils
安裝
並且如果你有興趣讓你的家用監控系統上傳安保圖片到 Dropbox,你需要 dropbox
包:pip install dropbox
至此所有的東西都已經安裝並且正確配置,我們可以繼續前進使用 Python 和 OpenCV 來打造我們的家用監控及運動檢測系統了。
這裡是我們的安裝過程:
我在上篇文章提到過,我們家用監控系統的目標是抓住任何嘗試溜進我的冰箱並且偷走我的啤酒的人。
為了實現這一目標,我在我的櫥櫃上安裝了樹莓派+攝像頭:
圖1:在櫥櫃頂部安裝的樹莓派
這個系統會俯視冰箱和我公寓的正門:
圖2:樹莓派對準我的冰箱。如果有人嘗試偷啤酒的話,運動檢測程式碼就會被觸發,上傳圖片到我的Dropbox中。
如果有人嘗試開啟冰箱門並取走我的一罐啤酒,運動檢測程式碼會生效,上傳當前幀的截圖到Dropbox,可以抓他個人贓並獲。
DIY:使用樹莓派 + Python + OpenCV 打造家用監控及運動檢測系統
好啦,讓我們開始建造我們的樹莓派家用監控系統吧。首先讓我們看一下這個工程的目錄結構:
1 2 3 4 5 |
|--- pi_surveillance.py |--- conf.json |--- pyimagesearch | |--- __init__.py | |--- tempimage.py |
我們家用監控系統的主要程式碼和邏輯會存放在 pi_surveillance.py
中。我們使用一個JSON配置檔案conf.json
來代替使用命令列引數或是在pi_surveillance.py
中對引數進行硬編碼。
針對這樣一個工程,我發現放棄使用命令列引數並依賴一個JSON配置檔案是很有用的。有時候你有太多的命令列引數,這時利用一個JSON檔案會使其變得容易和更加整潔。
最後,為了更好的組織,我們會定義一個pyimagesearch
包,裡面包含一個單一的類TempImage
,我們會在上傳到Dropbox之前使用它臨時將圖片寫入硬碟。
記住我們專案的目錄結構,開啟一個新的檔案,命名為pi_surveillance.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 25 26 |
# 匯入必須的包 from pyimagesearch.tempimage import TempImage from dropbox.client import DropboxOAuth2FlowNoRedirect from dropbox.client import DropboxClient from picamera.array import PiRGBArray from picamera import PiCamera import argparse import warnings import datetime import imutils import json import time import cv2 # 構建 argument parser 並解析 引數 ap = argparse.ArgumentParser() ap.add_argument("-c", "--conf", required=True, help="path to the JSON configuration file") args = vars(ap.parse_args()) # 過濾警告,載入配置檔案並且初始化Dropbox # 客戶端 warnings.filterwarnings("ignore") conf = json.load(open(args["conf"])) client = None |
哇,真是匯入了好多包啊——比我們平常在PyImageSearch博文中使用的要多得多。第一個匯入語句從 PyImageSearch匯入了我們的 TempImage
類。隨後在3-4行獲取了我們與Dropbox API互動所需的Dropbox函式。5-6行從picamera
匯入了一些類,使我們可以獲取樹莓派攝像頭的原始資料流(你可以在這裡讀到更多相關內容),剩下匯入語句完成了其他我們所需模組的匯入。再說一次,如果你還沒有安裝imutils
,你需要在繼續本教程之前先完成安裝。
16-19行解析我們的命令列引數。我們只需要一個選項 --conf
,它指向我們的JSON配置檔案在磁碟上的路徑。
23行過濾掉了Python和的警告提示資訊,特別是由urllib3和dropbox包產生的那些。最後,我們會在24行從磁碟上載入JSON配置字典並在25行初始化Dropbox客戶端。
JSON配置檔案
在我們深入的太多之前,讓我們先看一眼我們的conf.json
檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "show_video": true, "use_dropbox": true, "dropbox_key": "YOUR_DROPBOX_KEY", "dropbox_secret": "YOUR_DROPBOX_SECRET", "dropbox_base_path": "YOUR_DROPBOX_PATH", "min_upload_seconds": 3.0, "min_motion_frames": 8, "camera_warmup_time": 2.5, "delta_thresh": 5, "resolution": [640, 480], "fps": 16, "min_area": 5000 } |
這個JSON配置檔案存放了一系列重要的變數,讓我們逐個看看它們:
show_video
:一個布林量,表明來自樹莓派的視訊流是否要在螢幕上顯示。use_dropbox
: 布林量,表明是否要整合Dropbox APIdropbox_key
:你的公有Dropbox API keydropbox_secret
:你的私有 Dropbox API keydropbox_base_path
: 用於存放上傳圖片的Dropbox 應用程式目錄的名字。min_upload_seconds
:兩次上傳間需要等待的秒數。比如在我們啟動指令碼後5分33秒有圖片被上傳至Dropbox,第二張圖片只有等到5分36秒時才會被上傳。這個引數簡單的控制了圖片上傳的頻率。min_motion_frames
: 圖片被上傳Dropbox之前,包含運動的連續幀幀數的最小值camera_warmup_time
: 允許樹莓派攝像頭模組“熱身”和校準的時間delta_thresh
: 對於一個給定畫素,當前幀與平均幀之間被“觸發”看做是運動的最小絕對值差。越小的值會導致更多的運動被檢測到,更大的值會導致更少的運動被檢測到。resolution
: 來自樹莓派的視訊,其每一幀的寬和高。fps
: 想要從樹莓派攝像頭每秒獲取的幀數min_area
: 影象中需要考慮是否發生運動的最小區域的最小值(畫素為單位)。越小的值會導致越多的區域被認為發生了運動,而min_area
的值越大的,則會只會標記更大的區域。
至此我們已經定義了我們conf.json
配置檔案中的全部變數,我們可以回頭編碼了。
整合Dropbox
如果你想要整合Dropbox API,我們首先需要設定我們的客戶端:
1 2 3 4 5 6 7 8 9 10 |
if conf["use_dropbox"]: # 連線DropBox並且啟動會話授權過程 flow = DropboxOAuth2FlowNoRedirect(conf["dropbox_key"], conf["dropbox_secret"]) print "[INFO] Authorize this application: {}".format(flow.start()) authCode = raw_input("Enter auth code here: ").strip() # 完成會話授權並獲取客戶端 (accessToken, userID) = flow.finish(authCode) client = DropboxClient(accessToken) print "[SUCCESS] dropbox account linked" |
在第一行我們檢視JSON配置檔案,去看一下是否要使用Dropbox,如果是的話,在3-5行開始進行Dropbox的授權過程。
圖3:授權Dropbox
請注意它是如何通過提供一個URL給我們來進行授權驗證的。把這個URL複製貼上到你的瀏覽器中,我們就可以來到 Dropbox 授權頁面:
圖4:允許我們的指令碼訪問Dropbox API
在Dropbox整合頁面,我們點選“Allow”按鈕,這將為我們產生一個授權程式碼:
圖5:從Dropbox獲取授權程式碼
我們隨後即可把這段程式碼複製貼上回我們的程式:
圖 6:與Dropbox的整合現已完成。我們現在可以通過Python程式碼直接上傳圖片到Dropbox中了。
得到授權程式碼之後,我們就可以在10-11行完成Dropbox的整合工作。
樹莓派家用監控以運動檢測系統
好啦,現在我們終於可以開始執行一些計算機視覺和影象處理工作了。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 初始化攝像頭並且獲取一個指向原始資料的引用 camera = PiCamera() camera.resolution = tuple(conf["resolution"]) camera.framerate = conf["fps"] rawCapture = PiRGBArray(camera, size=tuple(conf["resolution"])) # 等待攝像頭模組啟動, 隨後初始化平均幀, 最後 # 上傳時間戳, 以及運動幀計數器 print "[INFO] warming up..." time.sleep(conf["camera_warmup_time"]) avg = None lastUploaded = datetime.datetime.now() motionCounter = 0 |
在1-3行我們設定從樹莓派攝像頭獲得的資料為捕獲的原始資料(更多關於使用樹莓派攝像頭的內容,你可以看這篇文章。)
我們同時允許樹莓派的攝像頭“熱身”幾秒鐘,確保感測器有足夠的時間進行校準。最後,在11-13行,我們會初始化平均背景幀,以及一些統計用的變數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 從攝像頭逐幀捕獲資料 for f in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True): # 抓取原始NumPy陣列來表示影象並且初始化 # 時間戳以及occupied/unoccupied文字 frame = f.array timestamp = datetime.datetime.now() text = "Unoccupied" # 調整幀尺寸,轉換為灰階影象並進行模糊 frame = imutils.resize(frame, width=500) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (21, 21), 0) # 如果平均幀是None,初始化它 if avg is None: print "[INFO] starting background model..." avg = gray.copy().astype("float") rawCapture.truncate(0) continue # accumulate the weighted average between the current frame and # previous frames, then compute the difference between the current # frame and running average cv2.accumulateWeighted(gray, avg, 0.5) frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg)) |
這裡的程式碼看上去應該和上週的文章中程式碼很類似。
我們對當前幀進行一些預處理,調整尺寸為500畫素寬,隨後將其轉換為灰階影象,並對其使用高斯模糊來移除高頻噪點並且讓我們的能夠專注於這幅影象的“結構”。
在第15行,我們檢查一下平均幀是否已經被初始化,如果沒有初始化,則用當前幀對其進行初始化。
24,25行非常重要,從這裡開始就和上週的實現方式變得不同了。
在我們之前的運動檢測指令碼中,我們假設了視訊資料的第一幀可以很好的代表我們想要建模的背景。對於我們這個特例來說,這個假設可以很好地工作。
但是這個假設同樣容易失效。隨著時間的變化(已經光線的變化),又因為視線中出現了其他物體,我們的系統會錯誤地在沒有發生運動的區域檢測到運動。
為了解決這一問題,我們使用了之前幀的加權平均值配合當前幀工作。這意味著我們的指令碼可以動態的調整背景,即使隨著時間的推移造成了光線的變化。這個方法仍然很基礎,而且不是一個“完美”的背景建模方法,但是和之前相比已經好很多了
基於加權平均的幀資料,我們從當前幀減去加權平均值,得到的結果我們稱之為“幀變化量”
delta = |background_model – current_frame|
圖7:幀變化量的示意圖,平均幀和當前幀的差異
我們隨後可以對這個變化量進行閥值處理來找到我們影象中包含與悲劇模型有顯著差別的區域——這些區域與視訊資料中發生“運動”的區域一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# 對變化影象進行閥值化, 膨脹閥值影象來填補 # 孔洞, 在閥值影象上找到輪廓線 thresh = cv2.threshold(frameDelta, conf["delta_thresh"], 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) < conf["min_area"]: continue # 計算輪廓線的外框, 在當前幀上畫出外框, # 並且更新文字 (x, y, w, h) = cv2.boundingRect(c) cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) text = "Occupied" # 在當前幀上標記文字和時間戳 ts = timestamp.strftime("%A %d %B %Y %I:%M:%S%p") cv2.putText(frame, "Room Status: {}".format(text), (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) cv2.putText(frame, ts, (10, frame.shape[0] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 0, 255), 1) |
為了找到影象中通過閥值測試的區域,我們進行簡單的輪廓檢測。隨後遍歷這些輪廓,看他們是否大於 min_area
。如果該區域足夠大,那麼我們可以表明我們已經在當前幀中找到了發生運動的區域。
16-18行計算了輪廓線的外框,將其畫在在運動區域,並且更新了我們的text
變數。
最後,21-25行獲取了我們當前的時間戳和狀態 變數text
並將它們標記在幀資料上。 現在,讓我們來編寫負責處理Dropbox上傳的程式碼吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# 檢測該房間是否被“佔領” if text == "Occupied": # 判斷上傳時間間隔是否已經達到 if (timestamp - lastUploaded).seconds >= conf["min_upload_seconds"]: # 運動檢測計數器遞增 motionCounter += 1 # 判斷包含連續運動的幀數是否已經 # 足夠多 if motionCounter >= conf["min_motion_frames"]: # 判斷Dropbox是否被使用 if conf["use_dropbox"]: # write the image to temporary file t = TempImage() cv2.imwrite(t.path, frame) # 將影象上傳至Dropbox並刪除臨時圖片 print "[UPLOAD] {}".format(ts) path = "{base_path}/{timestamp}.jpg".format( base_path=conf["dropbox_base_path"], timestamp=ts) client.put_file(path, open(t.path, "rb")) t.cleanup() # 更新最近一次上傳的時間戳並且重置運動 # 計數器 lastUploaded = timestamp motionCounter = 0 #否則, 該房間沒有“被佔領” else: motionCounter = 0 |
我們在第二行判斷是否確實在當前幀中監測到了運動。如果是的話,我們在第四行做另外一個判斷,來確保與上一次上傳到Dropbox的時間相比,已經過去了足夠長的時間——如果確實經過了足夠的時間,我們會將運動計數器遞增。
如果我們的運動計數器達到了一定的連續幀數,我們會把使用TempImage
類把當前影象寫入硬碟,通過Dropbox API將其上傳,並且重置我們的運動計數器和最近一次上傳的時間戳。
如果並沒有在房間中檢測到運動,我們就把運動計時器置為0。
最後,讓我們來完成這個指令碼——判斷我們是否希望將安保視訊顯示在螢幕上:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 判斷安保視訊是否需要顯示在螢幕上 if conf["show_video"]: # 顯示安視訊 cv2.imshow("Security Feed", frame) key = cv2.waitKey(1) & 0xFF # 如果q被按下,跳出迴圈 if key == ord("q"): break # 清理資料流為下一幀做準備 rawCapture.truncate(0) |
這段程式碼同樣是不言自明的。我們檢查一下是否我們想要把視訊顯示在螢幕上(依據我們的JSON配置檔案),如果是的話就顯示,並且監控一個用來終止指令碼的按鍵。
出於完整性考慮,讓我們在pyimagesearch/tempimage.py
中定義TempImage
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 匯入必要的包 import uuid import os class TempImage: def __init__(self, basePath="./", ext=".jpg"): # 建立檔案路徑 self.path = "{base_path}/{rand}{ext}".format(base_path=basePath, rand=str(uuid.uuid4()), ext=ext) def cleanup(self): # 刪除檔案 os.remove(self.path) |
樹莓派家用監控系統
我們已經做了很多工作了。讓我們看一下樹莓派 + Python + OpenCV + Dropbox 家用監控系統的實際表現。 定位到本文的原始碼目錄並且使用下面的命令來執行它:
1 |
$ python pi_surveillance.py --conf conf.json |
根據你的conf.json 的內容,你的輸出(可能)與我的大相近庭。快速回顧一下本文之前的內容,我把樹莓派和攝像頭安裝在櫥櫃的頂部,俯視廚房和冰箱——為了監視並等待任何嘗試偷走我啤酒的人。
這裡有一個例子,視訊從我的樹莓派通過X11 forwarding傳輸至我MacBook,這也是當你設定show_video: true 時會出現的結果:
視訊地址:http://www.youtube.com/embed/_N1YeVL4gjY
在這個視訊中,我已經禁用了視訊流,但通過設定use_dropbox: true
啟動了Dropbox API整合,我們可以看到在圖片中被檢測到的運動結果,以及將結果上傳到我個人Dropbox 賬戶的情況。
視訊地址:http://www.youtube.com/embed/BhD1aDEV-kg
這裡有一些示例幀資料,是家用監控系統工作一整天所記錄的內容:
圖8:樹莓派家用監控系統在視訊中檢測運動並上傳到我個人Dropbox 賬戶的例子
在這個例子中你可以清楚的看到我拿了冰箱中的啤酒
圖9:在這幀被樹莓派攝像頭捕獲的中,你可以清楚的看到我拿了冰箱中的啤酒
鑑於我上週的咆哮,這個家用監控系統應該能夠在James嘗試偷竊我啤酒的時候輕易的抓住他——而這一次,我的 Dropbox賬戶中就有被上傳的真憑實據了。
總結
在本文中我們探索瞭如何使用 Python + OpenCV + Dropbox + 樹莓派和一個攝像頭模組來建立我們自己的家用監控系統。
我們在上週基本運動追蹤例子的基礎上,擴充套件瞭如下幾點,(1) 對於背景環境的變化變得更加健壯, (2) 在樹莓派上工作,(3) 與Dropbox API 整合,這樣我們就可以把家用監控系統的視訊影象直接上傳到我的賬戶中,供我們即時檢視。
本文原始碼:http://pan.baidu.com/s/1hq1XWzE
最後,希望你喜歡這篇檢測,請考慮把它分享給其他人哦!
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式