这个该死的家伙。我就知道他偷了我最后一罐啤酒!
对于一个男人来讲,这些话永远都不该说。但是当我关上冰箱门的时候,我愤怒地叹息,感到厌恶,自言自语地说了这些。
你看,我花了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,允许我们监控家用监控系统并且当我们的系统检测到运动时,获取实时更新数据。
哇,上周那篇关于做一个基本运动检测系统的文章真是赞。写这篇文章很有乐趣,而且从像您一样的读者那里获得反馈,使我的努力变得很值得。
对于那些刚看到这篇文章的朋友,上周那篇文章是关于使用计算机视觉来建立一个运动检测系统,其动机是因为我的朋友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 集成,这样我们就可以把家用监控系统的视频图像直接上传到我的账户中,供我们即时查看。