哇,上周那篇关于做一个基本运动检测系统的文章真是赞。写这篇文章很有乐趣,而且从像您一样的读者那里获得反馈,使我的努力变得很值得。
对于那些刚看到这篇文章的朋友,上周那篇文章是关于使用计算机视觉来建立一个运动检测系统,其动机是因为我的朋友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 |