Thursday, August 3, 2017

Raspberry Surveillance System, The Complete Solution


I have shown how to build a basic system based on a Raspberry to capture a picture through a PiCamera and share it on a web page. Now I'd like to share a bit more of my experiments by describing an end-to-end approach to detect movements in the monitored area and alert of a possible intrusion. To the purpose, I'll share what I did setup with basic pieces of code found browsing around the internet. This minimal system is made of three simple elements:
  1. Telegram bot
  2. Motion detection algorithm
  3. Alert system

The Telegram Bot

A Telegram Bot is a nice and free manner to:
  • Send commands to your surveillance system from your personal Telegram account, start/stop services, reconfigure them...Indeed you can still use an SSH connection but Telegram is quick, visual, and immediate. Last but not least, it will allow you to receive alerts on your smartphone.
  • Receive messages coming from your surveillance system. When an alert is generated you will be informed by a message from your bot with the information and format you configure.
So how to create a Telegram bot? That's easier to do than to explain. Just add the BotFather to your contact list and start messaging! Send "/start" for the list of functionalities. To create a bot you'll have to send "/newbot". Check the example:

Mirko Ortensi, [Jul 20, 2017, 9:06 PM]:
/newbot
BotFather, [Jul 20, 2017, 9:06 PM]:
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
Mirko Ortensi, [Jul 20, 2017, 9:06 PM]:
mybotname
BotFather, [Jul 20, 2017, 9:06 PM]:
Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
Mirko Ortensi, [Jul 20, 2017, 9:07 PM]:
mybotnameBot
BotFather, [Jul 20, 2017, 9:07 PM]:
Done! Congratulations on your new bot. You will find it at t.me/mybotnameBot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.
Use this token to access the HTTP API:
447633501:AAH_wZZupcBIGQEC_pPV7[...]
For a description of the Bot API, see this page: https://core.telegram.org/bots/api

Nice! Now you have your bot entity ready to talk to your surveillance system! Write down the API token, which you'll use to authenticate to your bot as bot administrator and send/receive messages.

As the system is written in Python, you'll manage your Telegram bot with python-telegram-bot, so be sure to install it in your Raspberry. You can accomplish that by running:

python -m pip install python-telegram-bot

Motion Detection

This is the core of every surveillance system: the capability to detect any change in the monitored area and take a decision (which is usually inform the administrator). I started from a very basic Google search and hit this lightweight motion detection Python algorithm which is pretty intuitive: few lines, easy to read, quick and, most important, it works pretty well. This piece of code will analyse frames captured with raspistill and detect changes based on previous buffered frame. If a change is detected, the captured frame is written to disk. The algorithm will allow configuration for:
  • Threshold (how much a pixel has to change by to be marked as "changed")
  • Sensitivity (how many changed pixels before capturing an image)
  • ForceCapture (whether to force an image to be captured every forceCaptureTime seconds)
  • Disk space to reserve: once hit, no more frames will be written to disk
I have been trying it for some weeks and it works pretty well, I will share the code with the thresholds I set:

#!/usr/bin/python
import StringIO
import subprocess
import os
import time
from datetime import datetime
from PIL import Image

# Motion detection settings:
# Threshold (how much a pixel has to change by to be marked as "changed")
# Sensitivity (how many changed pixels before capturing an image)
# ForceCapture (whether to force an image to be captured every forceCaptureTime seconds)
threshold = 30
sensitivity = 60
forceCapture = True
forceCaptureTime = 60 * 60 # Once an hour

# File settings
saveWidth = 1280
saveHeight = 960
diskSpaceToReserve = 400 * 1024 * 1024 # Keep 400 mb free on disk

# Capture a small test image (for motion detection)
def captureTestImage():
    print("capture..")
    command = "raspistill -w %s -h %s -t 3000 -e bmp -o -" % (100, 75)
    imageData = StringIO.StringIO()
    imageData.write(subprocess.check_output(command, shell=True))
    imageData.seek(0)
    im = Image.open(imageData)
    buffer = im.load()
    imageData.close()
    return im, buffer

# Save a full size image to disk
def saveImage(width, height, diskSpaceToReserve):
    keepDiskSpaceFree(diskSpaceToReserve)
    time = datetime.now()
    filename = "/tmp/capture-%04d%02d%02d-%02d%02d%02d.jpg" % (time.year, time.month, time.day, time.hour, time.minute, time.second)
    subprocess.call("raspistill -w 1296 -h 972 -t 3000 -e jpg -q 15 -o %s" % filename, shell=True)
    print "Captured %s" % filename

# Keep free space above given level
def keepDiskSpaceFree(bytesToReserve):
    if (getFreeSpace() < bytesToReserve):
        for filename in sorted(os.listdir(".")):
            if filename.startswith("capture") and filename.endswith(".jpg"):
                os.remove(filename)
                print "Deleted %s to avoid filling disk" % filename
                if (getFreeSpace() > bytesToReserve):
                    return

# Get available disk space
def getFreeSpace():
    st = os.statvfs(".")
    du = st.f_bavail * st.f_frsize
    return du
       
# Get first image
image1, buffer1 = captureTestImage()

# Reset last capture time
lastCapture = time.time()

while (True):
    # Get comparison image
    image2, buffer2 = captureTestImage()

    # Count changed pixels
    changedPixels = 0
    for x in xrange(0, 100):
        for y in xrange(0, 75):
            # Just check green channel as it's the highest quality channel
            pixdiff = abs(buffer1[x,y][1] - buffer2[x,y][1])
            if pixdiff > threshold:
                changedPixels += 1

    # Check force capture
    if forceCapture:
        if time.time() - lastCapture > forceCaptureTime:
            changedPixels = sensitivity + 1
               
    # Save an image if pixels changed
    if changedPixels > sensitivity:
        lastCapture = time.time()
        saveImage(saveWidth, saveHeight, diskSpaceToReserve)
   
    # Swap comparison buffers
    image1 = image2
    buffer1 = buffer2

Alert System

Now the final piece of code; we have to inform the user whenever a motion is detected. I wrote two lines of code to send a Telegram message from the bot. This simple algorithm counts frames captured in /tmp folder (every 5 seconds, let's say) Whenever this number is increased, the user receives a message. Something like this:

#!/usr/bin/python

import sys
import time
import random
import datetime
import telegram
import os
import logging
import fnmatch

bot = telegram.Bot(token='447633501:AAH_wZZupcBIGQEC_pPV7[...]')

elems = len(fnmatch.filter(os.listdir("/tmp"), '*.jpg'))
print elems

while (True):
    if (elems != len(fnmatch.filter(os.listdir("/tmp"), '*.jpg'))):
       bot.sendMessage(chat_id=<id_from_userinfobot>, text="Motion!")
       elems = len(fnmatch.filter(os.listdir("/tmp"), '*.jpg'))
    time.sleep(5)

Don't forget to:
  1. Create the bot object by using the token provided by BotFather
  2. Send the message to the user id to be notified. In order to find out your user id, send "/start" command to userinfobot.

Wrap everything up

Now that you have the Telegram bot created, the motion detection loop algorithm and a manner to send alerts, you'll have to wrap it up. This simple architecture is composed of just three daemons:
  1. Telegram bot daemon to receive commands from Telegram user and perform actions
  2. Motion detection daemon to parse frames looking for movements
  3. Alert system daemon which, based on motion detection daemon, send a Telegram notification
These three daemon can be run as start/stop/status services in /etc/init.d as usual, whereas Telegram bot code could be something like:

#!/usr/bin/python

from telegram.ext import Updater, CommandHandler
import subprocess

def status(bot, update):
    command = "sudo ps ax | grep ".py" | grep -v grep | grep -v sudo"
    try:
        p = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
        (output, err) = p.communicate()
        p_status = p.wait()
        update.message.reply_text(output)
    except Exception as e:
        output = str(e)
        update.message.reply_text(output)

def startm(bot, update):
    command = "sudo /etc/init.d/motiond start"
    try:
        p = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
        (output, err) = p.communicate()
        p_status = p.wait()
        update.message.reply_text("Motion Detection Enabled")
    except Exception as e:
        output = str(e)
        update.message.reply_text(output)

def stopm(bot, update):
    command = "sudo /etc/init.d/motiond stop"
    try:
        p = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
        (output, err) = p.communicate()
        p_status = p.wait()
        update.message.reply_text("Motion Detection Disabled")
    except Exception as e:
        output = str(e)
        update.message.reply_text(output)

def hello(bot, update):
    update.message.reply_text(
        'Hello {}'.format(update.message.from_user.first_name))

updater = Updater('447633501:AAH_wZZupcBIGQEC_pPV7[...]')

updater.dispatcher.add_handler(CommandHandler('startm', startm))
updater.dispatcher.add_handler(CommandHandler('stopm', stopm))
updater.dispatcher.add_handler(CommandHandler('status', status))

updater.start_polling()
updater.idle()

Here I implemented three basic commands like:

  • Start motion detection
  • Stop motion detection
  • Check Python processes running, so to be sure all the processes are running
You can add more controls, like starting/stopping alerting system...
This is a demonstrative code, you could think of adding a watchdog to make sure everything is running or join the three daemons into one.