Raspberry Pi 教程:150 行 Python 代码实现家庭监控

Raspberry Pi 教程:150 行 Python 代码实现家庭监控

我很久以前就拥有了一个树莓派(Raspberry Pi ),只是坐在我的科技洗衣箱里。在观看 Youtube 上的创意树莓应用程序后,我羡慕地决定自己尝试一下。对我来说,第一个显而易见的想法是在你离开的时候,检查你家的家庭安全系统。

最后一件事是,能够通过相机检测,并粗略地定位任何动作。它会把照片邮寄到你的邮箱。另外,我们可以使用一个简单的网络界面,在我们的本地网络中与它进行交互,这样我们就可以在家门前激活或关闭它。我假设,如果有人能够到达当地的无线网络,最有可能他/她是我们中的一员(够公平吗?)。

Raspberry Pi 家庭监控

对于本文的剩下部分,我尝试总结重要的实现细节。希望你会发现它很有趣。 你也可以直接去 Github 并检查实现。

材料清单

从硬件开始。在这个项目里,你需要什么:

  • Raspberry Pi 2 B+(其他版本可能也没关系)
  • Raspberry Pi 相机和相机外壳(或一个外壳的创造力)
  • Wifi 加密狗(Pi 3 不需要)
  • Raspberry Pi 盒(可选)

我们需要安装相关的Python库。 他们都能使用 pip install 安装:

  • PiCamera:用于从 Raspberry Pi 相机拍摄
  • cv2:所有智能计算机视觉算法
  • smtplib:发送邮件
  • email:格式化邮件数据
  • Flask:简单的网站用于安全的开/关

运动检测

我们开始编码!我们需要做的第一件事就是,像老板一样导入我们的库!然后初始化 Raspberry Pi 相机,使其准备好流式帧。

from picamera.array import PiRGBArray
from picamera import PiCamera
from utils import send_email, TempImage
import argparse
import warnings
import datetime
import json
import time
import cv2

# load some config
conf = json.load(open(args["conf"]))

# initialize the camera 
camera = PiCamera()
camera.resolution = tuple(conf["resolution"])
camera.framerate = conf["fps"]
rawCapture = PiRGBArray(camera, size=tuple(conf["resolution"]))

我们喜欢检测每帧的运动,但我们也喜欢忽略错误的警报。因此,我们计算连续检测的次数,如果它大于给定值,我们真的很警觉; 否则我们只是忽略。另外,我们还有一些时间,让我们的系统在开始运行之前聚集在一起。

# allow the camera to warmup, then initialize the average frame, last
# uploaded timestamp, and frame motion counter
print "[INFO] warming up..."
time.sleep(conf["camera_warmup_time"])
avg = None
lastUploaded = datetime.datetime.now()
motionCounter = 0
print('[INFO] talking raspi started !!')

下一步,我们开始循环遍历帧并运行背景减法算法。 具体来说,我们运用以下操作来检测运动;

  • 将帧转换为灰度
  • 模糊框架以减少框架细节
  • 计算框架和平均背景框架的差异。
  • 用加权运行平均值更新平均背景帧
# capture frames from the camera
for f in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):
    # grab the raw NumPy array representing the image and initialize
    # the timestamp and occupied/unoccupied text
    frame = f.array
    timestamp = datetime.datetime.now()
    text = "Unoccupied"

    ######################################################################
    # COMPUTER VISION
    ######################################################################
    # resize the frame, convert it to grayscale, and blur it
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, tuple(conf['blur_size']), 0)

    # if the average frame is None, initialize it
    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
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))
    cv2.accumulateWeighted(gray, avg, 0.5)

这是一个背景减法的总体方案。 我们只使用平均帧作为背景模型,但有更多更聪明的算法用于更健壮的建模。但是在 Raspberry Pi 中速度快更重要

上面的步骤,给了我们一个像素变化的 mask。但是这还不够。在进一步研究之前,我们需要进行更多的调整来减少噪音,填补空洞并收集指示移动部分的轮廓。 走得更远

  • 阈值降噪的差异帧
  • 扩张阈帧用于填充孔
  • 找到轮廓
  • 找到轮廓周围的边框来定位运动
    # thresholding difference frame for filling holes and noise reduction
    # finding contours of moving regions
    thresh = cv2.threshold(frameDelta, conf["delta_thresh"], 255,
        cv2.THRESH_BINARY)[1]
    thresh = cv2.dilate(thresh, None, iterations=2)
    im2 ,cnts, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE)

    # loop over the contours
    for c in cnts:
        # if the contour is too small, ignore it
        if cv2.contourArea(c) < conf["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"

扩张效应

在所有这些老派算法阶段之后,我们检测运动。现在是时候,实施警报系统的逻辑了。我们从检查连续运动检测的次数开始。如果计数高于阈值,我们会发送邮件,包括在特定时间间隔内拍摄的快照。在这里,我们喜欢用合理数量的帧快照捕捉各种视图。

    ###################################################################################
    # LOGIC
    ###################################################################################

    # check to see if the room is occupied
    if text == "Occupied":
                # save occupied frame
                cv2.imwrite("/tmp/talkingraspi_{}.jpg".format(motionCounter), frame);

                # check to see if enough time has passed between uploads
                if (timestamp - lastUploaded).seconds >= conf["min_upload_seconds"]:

                        # increment the motion counter
                        motionCounter += 1;

                        # check to see if the number of frames with consistent motion is
                        # high enough
                        if motionCounter >= int(conf["min_motion_frames"]):
                                # send an email if enabled
                                if conf["use_email"]:
                                        print("[INFO] Sending an alert email!!!")
                                        send_email(conf)
                                        print("[INFO] waiting {} seconds...".format(conf["camera_warmup_time"]))
                                        time.sleep(conf["camera_warmup_time"])
                                        print("[INFO] running")

                                # update the last uploaded timestamp and reset the motion
                                # counter
                                lastUploaded = timestamp
                                motionCounter = 0

    # otherwise, the room is not occupied
    else:
        motionCounter = 0

发送邮件

因为它是 Python,做事总是很容易,比如发送电子邮件。最初,我们需要雇用一个电子邮件地址(我申请了一个 Gmail 邮箱)。然后,我们只需使用下面的代码发送一个包含我们需要的所有内容的电子邮件。棘手的部分是,在附加到电子邮件之前,将帧转换为正确的数据格式。

import json
import smtplib
import uuid
import os
import glob

from os.path import basename
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate

def send_email(conf):
    """
    Using the defined mail address on gmail, it send a alert mail with attached images
    """"
    fromaddr = "address@gmail.com" 
    for email_address in conf['email_address']:
        toaddrs  = email_address
        print("[INFO] Emailing to {}".format(email_address))
        text = 'Hey Someone in Your House!!!!'
        subject = 'Security Alert!!'
        message = 'Subject: {}\n\n{}'.format(subject, text)

        msg = MIMEMultipart()
        msg['From'] = fromaddr
        msg['To'] = toaddrs
        msg['Date'] = formatdate(localtime=True)
        msg['Subject'] = subject
        msg.attach(MIMEText(text))

        # Taken frames are kept in /tmp folder with concecutive numbering. 
        files = glob.glob("/tmp/talkingraspi*")
        print("[INFO] Number of images attached to email: {}".format(len(files)))
        for f in files:
            with open(f, "rb") as fil:
                part = MIMEApplication(
                    fil.read(),
                    Name=basename(f)
                )
                part['Content-Disposition'] = 'attachment; filename="%s"' % basename(f)
                msg.attach(part)

        # Credentials (if needed)
        username = "gmail_username"
        password = "password"
        # The actual mail send
        server = smtplib.SMTP('smtp.gmail.com:587')
        server.starttls()
        server.login(username,password)
        server.sendmail(fromaddr, toaddrs, msg.as_string())
        server.quit()

网站

项目的最后一部分,是创建一个可从本地网络访问的网站,用于启用和禁用安全系统。为此,我们使用 Flask 框架,它正好是世界上最简单的 Web 服务器框架。

import subprocess
from flask import Flask, render_template
app = Flask(__name__)

# keep runnign process global
proc = None


@app.route("/")
def hello():
    # return the web interface for raspi 
    return render_template("index.html")


@app.route("/start", methods=['GET', 'POST'])
def start_talkingraspi():
    # start watching code on a different process
    global proc
    print(" > Start talkingraspi!")
    proc = subprocess.Popen(["python", "pi_surveillance.py", "-c", "conf.json"])
    print(" > Process id {}".format(proc.pid))
    return "Started!"


@app.route("/stop", methods=['GET', 'POST'])
def stop_talkingraspi():
    # stop the watching code process
    global proc
    print(" > Stop talkingraspi!")
    # subprocess.call(["kill", "-9", "%d" % proc.pid])
    proc.kill()
    print(" > Process {} killed!".format(proc.pid))
    return "Stopped!"


@app.route("/status", methods=['GET', 'POST'])
def status_talkingraspi():
    global proc
    if proc is None:
        print(" > Talkingraspi is resting")
        return "Resting!"
    if proc.poll() is None:
        print(" > Talking raspi is running (Process {})!".format(proc.pid))
        return "Running!"
    else:
        print(" > Talkingraspi is resting")
        return "Stopped!"


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5555, debug=False)

现在,我们需要用 jQuery 支持的基本 HTML 代码创建一个很好的登录页面,以便将 ajax 请求 on/off 到我们的服务器并更新系统状态。我们将其编码,并放置在项目的根文件夹中名为 “templates” 的文件夹中。这是 Flask html 内容的默认位置。

<!DOCTYPE doctype html>

<html lang="en">
<head>
<meta charset="utf-8"/>
<!-- Be Mobile Friendly -->
<meta content="width=device-width,height=device-height initial-scale=1" name="viewport"/>
<title>TalkingRaspi</title>
<meta content="Talkingraspi interface" name="description"/>
<meta content="Eren Golge" name="author"/>
<link href="css/styles.css?v=1.0" rel="stylesheet"/>
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<script>
    // Sending start command to server
    $(document).ready(function(){
      $("#start_button").click(function(e){
          e.preventDefault();
        $.ajax({type: "POST",
                url: "/start",
                data: {},
                success:function(result){
          $("#start_button").html(result);
        }});
      });
    });

    // Sending stop command to server
    $(document).ready(function(){
      $("#stop_button").click(function(e){
          e.preventDefault();
        $.ajax({type: "POST",
                url: "/stop",
                data: {},
                success:function(result){
          $("#stop_button").html(result);
        }});
      });

      // Checking system status
      (function worker() {
         $.ajax({
           url: '/status', 
           success: function(data) {
             $('#status_bar').html(data);
           },
           complete: function() {
             // Schedule the next request when the current one's complete
             setTimeout(worker, 5000);
           }
         });
      })();
    });
    </script>
<!--[if lt IE 9]>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
  <![endif]-->
</head>
<body>
<script src="js/scripts.js"></script>
<h1>Hi! This is Talkingraspi!</h1>
<span id="status_bar"></span>
</body></html>

<button id="start_button">Start Talkingraspi</button>
<button id="stop_button">Stop Talkingraspi</button>
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>

网站截图

可能的改进

有人可能通过更好的算法,获得更强大的检测结果。也许是一个更强大的背景建模,更多的图像处理步骤或更好的算法参数。 你也可以用移动电源调动同一个系统。我尝试了我的10000毫安时 Anker 移动电耗,它使系统保持了15个小时。

如果你喜欢在无插件的地方使用它,一天就够了。

一个简单的增强将使用红外摄像机的黑暗的地方。 我上面分享的设备有一个简单的黑暗盲摄像机,但我最近开始使用红外版本。 这真的很好。

最后的话

我们在这里完成,很好去! 在这里我也分享我的 Github 地址。 欢迎评论和贡献。

希望你永远不需要一个监视系统为你的房子。 但如果你还需要一个便宜的,在这里我给你一个。 如果你懒得实施所有这些联系我(erengolge@gmail.com),让我给你安排一些东西。 祝你好运!

原文链接:https://hackernoon.com/raspberrypi-home-surveillance-with-only-150-lines-of-python-code-2701bd0373c9

尚未评分
您的评分将帮助我们做出更好的玩法

观光\评论区

Copyright © 2017 玩点什么. All Rights Reserved.