用树莓派 + Python + OpenCV 实现家庭监控和移动
哇,上周那篇关于做一个基本运动检测系统的文章真是赞。写这篇文章很有乐趣,而且从像您一样的读者那里获得反馈,使我的努力变得很值得。
对于那些刚看到这篇文章的朋友,上周那篇文章是关于使用计算机视觉来建立一个运动检测系统,其动机是因为我的朋友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 打造家用监控及运动检测系统
好啦,让我们开始建造我们的树莓派家用监控系统吧。首先让我们看一下这个工程的目录结构:
Python
|---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,并且开始导入如下的包:
Python
# 导入必须的包
frompyimagesearch.tempimageimportTempImage
fromdropbox.clientimportDropboxOAuth2FlowNoRedirect
fromdropbox.clientimportDropboxClient
frompicamera.arrayimportPiRGBArray
frompicameraimportPiCamera
importargparse
importwarnings
importdatetime
importimutils
importjson
importtime
importcv2
# 构建 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文件:
{
"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 API
dropbox_key:你的公有Dropbox API key
dropbox_secret:你的私有 Dropbox API key
dropbox_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,我们首先需要设置我们的客户端:
Python
ifconf["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的集成工作。
树莓派家用监控以运动检测系统
好啦,现在我们终于可以开始执行一些计算机视觉和图像处理工作了。
Python
# 初始化摄像头并且获取一个指向原始数据的引用
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行,我们会初始化平均背景帧,以及一些统计用的变量。
Python
# 从摄像头逐帧捕获数据
forfincamera.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,初始化它
ifavgisNone:
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:帧变化量的示意图,平均帧和当前帧的差异
我们随后可以对这个变化量进行阀值处理来找到我们图像中包含与悲剧模型有显著差别的区域——这些区域与视频数据中发生“运动”的区域一致:
Python
# 对变化图像进行阀值化, 膨胀阀值图像来填补
# 孔洞, 在阀值图像上找到轮廓线
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)
# 遍历轮廓线
forcincnts:
# if the contour is too small, ignore it
ifcv2.contourArea(c)
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上传的代码吧:
Python
# 检测该房间是否被“占领”
iftext=="Occupied":
# 判断上传时间间隔是否已经达到
if(timestamp-lastUploaded).seconds>=conf["min_upload_seconds"]:
# 运动检测计数器递增
motionCounter+=1
# 判断包含连续运动的帧数是否已经
# 足够多
ifmotionCounter>=conf["min_motion_frames"]:
# 判断Dropbox是否被使用
ifconf["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。
最后,让我们来完成这个脚本——判断我们是否希望将安保视频显示在屏幕上:
Python
# 判断安保视频是否需要显示在屏幕上
ifconf["show_video"]:
# 显示安视频
cv2.imshow("Security Feed",frame)
key=cv2.waitKey(1)&0xFF
# 如果q被按下,跳出循环
ifkey==ord("q"):
break
# 清理数据流为下一帧做准备
rawCapture.truncate(0)
这段代码同样是不言自明的。我们检查一下是否我们想要把视频显示在屏幕上(依据我们的JSON配置文件),如果是的话就显示,并且监控一个用来终止脚本的按键。
出于完整性考虑,让我们在pyimagesearch/tempimage.py中定义TempImage
Python
# 导入必要的包
importuuid
importos
classTempImage:
def__init__(self,basePath="./",ext=".jpg"):
# 创建文件路径
self.path="{base_path}/{rand}{ext}".format(base_path=basePath,
rand=str(uuid.uuid4()),ext=ext)
defcleanup(self):
# 删除文件
os.remove(self.path)
树莓派家用监控系统
我们已经做了很多工作了。让我们看一下树莓派 + Python + OpenCV + Dropbox 家用监控系统的实际表现。 定位到本文的源码目录并且使用下面的命令来执行它:
Shell
$pythonpi_surveillance.py--confconf.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
最后,希望你喜欢这篇检测,请考虑把它分享给其他人哦!