Creating a Raspberry Pi Cat Detector

Have you ever wondered who is coming through your cat flap when you are not around?

Well I hadn’t, until I walked into my utility room one day and found a big fat grey moggy half in and half out of the cat flap looking up at me. This was not my cat. It was a tense moment as we both stared at each other. The offending moggy slowly backed up and stared at me defiantly from outside the cat flap.

This got me thinking about setting up a Raspberry Pi powered cat detector. I wanted to know if this cat was coming in regularly and helping itself to my cat’s food.

I sat down and planned out my basic design goals for my Raspberry-Pi-Cat-Detector (or RPi-CD for short):

1. Detect when a cat enters or exists the cat flap.

2. Take a photo of the cat

3. Email the photo to me

I like to use Python for scripting my projects mainly because it is easy, usually quite simple to understand and there is a lot of support on the internet when I need help (which is often!). There are also lots of modules (e.g. in this project I have used GPIO and picamera) that have been written by the Python community that let you do lots of really cool things with minimal coding.

Step 1. How to detect if something is moving through the cat flap

The first problem to solve was how to know if a cat is coming in through the cat flap. There are lots of ways of doing this. I chose to use a HC-SR501 PIR Infrared Motion Sensor Module (learn about them here) connected via my Raspberry Pi’s GPIO pins. This was mainly because they are cheap and fairly reliable which is presumably why they are used in many home security systems.

So how to connect the PIR to my Raspberry Pi?

All Raspberry Pi’s have what are called GPIO pins. They stick up out of the Pi board and look like this:

IMG_20150806_213529

I am using a Raspberry Pi Model B version 2. If you are using a different version (e.g. Raspberry Pi model B or B+) you might need to change which pins you are using. If you are new to GPIO check out this website: http://pi.gadgetoid.com/pinout

IMG_20150804_131053

PIR sensors can vary as to which order the pins are in. But they will all have 3. And they will be:

  • VCC (positive)
  • GND (negative)
  • OUT (signal – HIGH or LOW)

IMG_20150804_131509 IMG_20150804_131521

Your task is to connect up these 3 PIR pins to your Raspberry Pi via 3 GPIO pins. There are lots to chose from and a word of warning, if you get this bit wrong, you can fry your Raspberry Pi for good. Make sure you check and double check!

I am using physical pin numbers here and not BCM (see here for more on this).

VCC goes to GPIO pin 2

GND goes to GPIO pin 6

OUT goes to GPIO pin 11

I used a bread board as a way of making this easier, and as a way of connecting up an LED to use as a way of showing when a cat has been detected.

IMG_20150804_131131

Python has a module called GPIO (or RPi.GPIO see here) which will let me find out whether the PIR OUT pin, connected to GPIO pin 11 is HIGH or LOW. Sounds complicated right? But it really isn’t so bad when you have wired it all up, I promise.

When the PIR OUT is High (5v in this case), it means that the PIR has detected movement. LOW (0v) means it has not detected movement.

Using the GPIO module we can keep monitoring the status of the PIR OUT pin via the RPi.GPIO module and our Raspberry Pi GPIO pin 11.

The Python module RPi.GPIO is already isntalled if you have an up to date copy of NOOBS (for more on NOOBs see here).

Now we have a way of allowing our Raspberry Pi Cat Detector to be able to detect movement. But we are still blind!

Here is the code I used (cat_io.py):

GPIO.setmode(GPIO.BOARD)
# no warnings
GPIO.setwarnings(False)

# choose pins for reading PIR OUT and lighting up LED
PIR_PIN = 11
LED_PIN = 13

class PIR(object):

    def __init__(self):
        # set PIR_PIN as input - so we can read if HIGH or LOW
        GPIO.setup(PIR_PIN, GPIO.IN)

    def read(self):
        # tiny function to read PIR OUT on pin 11
        return GPIO.input(PIR_PIN)

class LED(object):

    def __init__(self):
        # set LED_PIN as output - so we can set as HIGH (on) or LOW (off)
        GPIO.setup(LED_PIN, GPIO.OUT)

    def on(self):
        # turn LED on
        GPIO.output(LED_PIN, 1)

    def off(self):
        # turn LED off
        GPIO.output(LED_PIN, 0)

    def blink(self, on_seconds, off_seconds, total_blinks):
        # blink LED
        for i in range(total_blinks):
            self.on()
            time.sleep(on_seconds)
            self.off()
            time.sleep(off_seconds)

Step 2. How to capture a photo of the detected cat

So to take a picture, whenever there was movement at the cat flap, I needed a camera. Luckily I had a Pi NOIR (see here) already. This is like a normal camera module but without an infrared filter. You can just use a normal pi camera, I was too cheap to buy another one and was willing to put up with pictures with no infrared filster (the main difference is the photos come out slightly more pinkish than normal).

The Python module picamera (see here) is a great way of taking pictures using Python. I was able to detect motion at the cat flap, now I could take a picture when I had detected that motion.

Here is the code I used (cat_camera.py):

import time
from datetime import datetime as dt
import picamera
from os import walk

class my_camera(object):

    def take_picture(self, save_location):
        '''
            when called, takes a picture and svaes jpg in specified 
            location
            WARNING - does not check if file already exists!
        '''
        with picamera.PiCamera() as camera:
            camera.resolution = (1024, 768)
            file_name = self.create_file_name()
            camera.capture(save_location + '/' + file_name)
            print 'taking picture'

    def create_file_name(self):

        return dt.now().strftime("%Y_%m_%d_%H_%M_%S") + ".jpg"

Step 3. How to send an email with the photos attached

The final hurdle was figuring out how to send an email with an attached photo, when I had detected and photographed a cat.

This turned out to be quite easy using modules that come as standard with Python. All I needed was a gmail account to use for this project and since gmail accounts are free, I just set up a new account.

Here is the code I used (cat_email.py):

import smtplib
from datetime import datetime as dt

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

from os import listdir

class send_mail(object):

    def send(self, file_location):
	''' who you want to send your cat detector emails to '''
        email_to = "this is where you put the to email address"
	''' and who they are from e.g. the new gmail account you just set up '''
        email_from = "this where you put the sender email address"

	# gmail smtp stuff
        smtp_server = 'smtp.gmail.com'
        smtp_port = 587
        smtp_user = email_from

	‘’’ enter your gmail password here - warning this is visible
	    to anyone who can open this file!!’’’
        
        smtp_pass = ‘youremailpasswordhere’

	# create message
        msg = MIMEMultipart('alternative')
        msg['To'] = email_to
        msg['From'] = email_from
        

        # get list of attachments i.e. all photos within subdirectory
	‘’’ I used a directory to store photos in and made it so that 
	photos taken within 10 seconds of each other were grouped into the
        same subdirectory
		’’’
        attachment_list = self.get_attachment_list(file_location)
        if len(attachment_list) > 0:
            count = 0
	    # here I make sure the max number of attached photos is 3
            while count < len(attachment_list) and count < 3:
                pic = attachment_list[count]
                file_name = file_location + '/' + pic
                image_data = open(file_name, 'rb').read()
                image = MIMEImage(image_data, name=pic)
                msg.attach(image)
                count += 1

        time_now = dt.now().strftime('%H:%M:%S')
        
	# create email subject
        msg['Subject'] = 'Cat Detector: cat detected (%s)' % time_now
	
	# email body
        body = '''Hi Tim,\nA cat has just been detected, see attched picture(s).
                \nBest,\nCatDetector
                '''

        body = MIMEText(body, 'plain')

        # attach body
        msg.attach(body)

	# try and send email
        try:   
            s = smtplib.SMTP(smtp_server, smtp_port)
            s.ehlo()
            s.starttls()
            s.login(smtp_user, smtp_pass)
            s.sendmail(email_from, email_to, msg.as_string())
            print 'email sent'
        except:
            print 'error sending mail'

    def get_attachment_list(self, file_location):
       # use listdir to return all files in directory
       return listdir(file_location)

Bringing it all together!

So if you have made it this far – well done!

The next thing I needed was a way to create and delete directories. And to reset the main folder with all the photos in. I added this to make sure the disk space was reclaimed each time I resey my pi. If you want to keep all your photos, you will need to tweak this code.

And yes be careful with this code. You have the power to delete things!

import os
import shutil

class cat_dirs(object):

    def __init__(self):
        # get file path of parent directory
        self.cat_parent_path = os.path.abspath('.') + '/'

    def create_dir(self, episode):
        
        try:
            ''' creates a directory within temp_pic directory '''
            directory = self.create_dir_name(episode)
            os.makedirs(directory)
            
        except:
            directory = None
            print 'error making directory'

        return directory

    def create_dir_name(self, episode):
        # create dir path/name from parent path and episode
        return self.cat_parent_path + 'temp_pic/' + episode
    
    def delete_dir(self, dir_name):
        ''' deletes a directory and contents within temp_pic directory '''
        directory = self.cat_parent_path + 'temp_pic/' + str(dir_name)
        try:
            shutil.rmtree(directory)
        except:
            print 'error deleteing directory'

    def reset(self):

        ''' delete temp_pic and recreate '''
        directory = self.cat_parent_path + 'temp_pic/'
        try:
            shutil.rmtree(directory)
            os.makedirs(directory)
            
        except:
            print 'error reseting directory'


The last part – writing a main loop to run all the code. (if you code a lot you will probably have guessed that as I wrote this code I created all the other classes shown above as I went along – but that is not so easy to follow in a blog article).

[editors note: this code is quite long and might take a bit of time to follow. You might find it just as easy to write your own main loop 😀 ]

#!/usr/bin/env python

import cat_camera
import cat_io
import cat_os
import cat_email
import time

class cat_detector(object):

    def __init__(self):
        '''instantiate cat classes:
        camera, PIR, LED, cat_dirs and send_mail'''
        self.cat_cam = cat_camera.my_camera()
        self.PIR = cat_io.PIR()
        self.LED = cat_io.LED()
        self.cat_dirs = cat_os.cat_dirs()
        self.send_email = cat_email.send_mail()

        #reset directory containing photos
        self.cat_dirs.reset()

    def get_status(self):
        return self.PIR.read()

    def main(self):
        # initiate variables
        cycle = 0
        detection = 0
        time_since_last_detection = 0
        episode = 0
        high_alert = False

        '''turn on LED to show script is running
        useful if running headless on boot (i.e. no monitor)'''
        self.LED.on()
        time.sleep(10)
        self.LED.off()
        
        #main loop
        while True:

            '''check if a cat is detected, if it is, add one to detection count
               and if start of episode, go into HIGH ALERT'''
            if self.get_status():

                high_alert = True

                ''' add one to detection because this is a new
                    cat detection '''
                detection += 1


                ''' if this is the first detection of an episode,
                    add one to episodes '''
                if detection == 1:
                    episode += 1
                    # first detection of new episode so create a new directory
                    if episode < 10:                                                   
                        folder_name = '0' + str(episode)                                          
                    else:                                                   
                        folder_name = str(episode)
                
                current_dir_name = self.cat_dirs.create_dir(folder_name)                 
                
                # take picture of detected cat                 
                self.LED.on()                 
                self.cat_cam.take_picture(current_dir_name)                 
                self.LED.off()                 

               '''because there has been a new detection,                    
                  reset time since last detectionto 0'''                 
               time_since_last_detection = 0                 
               
               # indicate detection with LED blinking                 
               self.LED.blink(0.05,0.05,25)             
          else:                 
              # else if there is no detection                                  
              if high_alert:                     
              ''' if high alert == true                         
                  check time since last detection,                         
                  if too long,                         
                  return alert status to normal '''                     
                  if time_since_last_detection > 9:
                        high_alert = False
                        time_since_last_detection = 0
                        detection = 0
                        self.LED.on() # shows user email is being sent
                        self.send_email.send(current_dir_name)
                        self.LED.off()
                    else:
                        # else add another second to time since last detection
                        time_since_last_detection += 1

            print '\n' *50 + '* ' * 30           
            print '''\nCycle: %s,
                        \nAlert Status: %s,
                        \nEpisode: %s,
                        \nDetection: %s
                        \nTime sinse last detection: %s''' % (cycle, high_alert, episode, detection,  
                                                                time_since_last_detection)       
            print '\n'
            cycle += 1
            time.sleep(1)
            

# run the cat detector!
RPiCD = cat_detector()
RPiCD.main()

 

Being really clever

Now by this point you can run the main loop and see if your cat detector is working. You might have used my code or written your own. But the clever bit is making this script run when you boot up your Raspberry Pi without connecting your monitor, mouse or keyboard. I used the LED in my main loop to show me that the script was running at the start (a long flash at the start) and then when my RPi CD was taking a photo or sending an email. This made life a lot easier than having to guess if everything was working ok.

A good guide for doing this is here:

http://blog.scphillips.com/posts/2013/07/getting-a-python-script-to-run-in-the-background-as-a-service-on-boot/

 

Did I capture the cat?

So did building this cat detector work? Did I find out if the big grey moggy was pilfering my cats food?

I detected my cat:

2015_07_30_16_30_09 2015_08_03_08_39_16 2015_08_03_08_39_21

and a few other things:

2015_07_30_15_56_00

Was this the invading cat coming in by stealth at night?

2015_07_31_03_22_43

 

So what is next for the cat detector?

Night time picture taking needs improving, as the above picture shows. How could this be done?

Options include:

a) using an infra-red LED to light up the picture without alerting the cat (my PiNOIR would be perfect for this as it has not infra red filter) and

b) experimenting with picamera and the exposure time (longer exposure would mean lighter picture but if the cat is moving fast, it could lead to blurred pictures)

[please note – the code given in this article may need reformating in IDLE or similar code editor because Python is sensitive to white space e.g. tabs, which can be dissrupted when posting code into wordpress. No responsibility can be taken by the author for attempts at recreating the RPi-Cat-Camera. The code is given for illustrative purposes only.]

Tim Greenwood

Tim currently works for Living Systems. His background is in psychology, and he has been working as a psychologist within education and the community for the last ten years. He recently gained a Masters of Arts in Management, Learning, and Leadership from Lancaster Business school focusing on the use of Action Research to promote curious and evolving ways of working with individuals, groups and organisations.

More Posts - Website

Published by

Tim Greenwood

Tim currently works for Living Systems. His background is in psychology, and he has been working as a psychologist within education and the community for the last ten years. He recently gained a Masters of Arts in Management, Learning, and Leadership from Lancaster Business school focusing on the use of Action Research to promote curious and evolving ways of working with individuals, groups and organisations.