Saturday 7 March 2015

Catmon

Cat Monitor

Catmon is a simple cat flap monitor application written in python. It takes a picture when a cat enters through the cat flap, uploads the picture to google drive (gdrive) and tweets it on @boosimba.

The original project in 2015 was an opportunity to improve my knowledge of python and physical computing using a raspberry pi. Along the way I also learned to solder (well sort of).

The project was given a major overhaul in 2023 to move it a raspberry pi 3b+, running a 64 bit operating system, to include the catmon image classifier (aka catmonic). Catmonic is a pytorch deep learning module that classifies a catmon image with the cat's name and probability. 

So if you are interested in a 'practical' example of how to a write a python script that runs on a raspberry pi and interacts with a reed switch, camera, gdrive and twitter then read on*...

* The project is described below (as originally published) followed by a description of the main project updates.

The Results

These pictures were captured automatically by catmon and uploaded to gdrive. Just search for @boosimba on twitter to follow the cats' twitter feed.
Simba arrives
And here is Boo with an unfortunate bird

    What Was The Challenging Part?

    The most challenging part of this project for me was working out how to interact with gdrive using OAuth (more of this in the Real-world Learning below) and soldering a delicate reed switch.

    Approach

    I broke the problem down in to smaller independent building blocks and then coded and tested each block. The key building blocks were:
    • Python application to take a picture, using python's picamera module 
    • Python app to send a tweet with a picture, using python's tweepy module to wrapper the twitter API
    • Python app to send a picture to a gdrive folder, using python's pydrive module to wrapper the twitter API
    • Python app to use a config file, using python's configparser module
    • Python app to use a logging framework, using python's logging module
    • Python app to take input from a reed switch, connected to a breadboard, using the RPi.GPIO module
    I obtained a deeper understanding and resolved many problems in developing these building blocks. The learning from each of these was then included in the final project. The building blocks may also form the basis of future projects.

    Solution Overview


    catmon solution overview

    Here is a picture of the the cat flap, pi, camera and breadboard.

    cat flap

    pi, camera and breadboard (front view)

    pi, camera and breadboard (top view)

    Steps To A Working Solution


    Set-up the raspberry pi, with a wi-fi adapter and camera module

    There are lots of great web resources describing how to set-up the pi. For example see the raspberry pi foundation documentation.

    Catmon uses python 2.7.3 and runs on a raspberry pi 1 model B with the latest version of raspbian, as of March 2015.

    Python is included in the pi's raspbian operating system. You should make sure that the pi has the latest build before beginning your project.

    Solder the reed switch and connect to the cat flap with magnet

    A reed switch is a simple switch that closes when in the presence of a magnet. Reed switches are very cheap, I bought 10 for less than £3.

    For my set-up I needed about 2 metres of wire to connect the reed switch on the cat flap to the breadboard, so I reused some old speaker wire containing 2 wires side-by-side in a plastic sheath.
    reed switch, magnet and speaker wire
    After a crash course in soldering (using YouTube as my guide) I soldered one end of the speaker wire to the 2 ends of the reed switch. I then soldered the other end of the speaker wire to 2 breadboard jumper wires. I then taped the reed switch to the cat flap.

    I also reused a small magnet (from the draw of old kids toys) that I taped to the cat flap door.
    cat flap close-up showing reed switch and magnet

    Connect the reed switch and pi to the breadboard

    I connected the raspberry pi's gpio to a small breadboard using a 'cobbler'. The cobbler is a simple way of bringing the pi's gpio pins to the breadboard. Alternatively you could simply connect the appropriate gpio pins to the breadboard using jumper wires.

    The breadboard configuration is described in the circuit diagram and shown in the pictures.

    circuit diagram

    breadboard

    Install key utilities: ssh, vnc and screen

    It is much easier to work on the pi in 'headless mode' - accessing the pi from your laptop (or other computer) over your LAN. Again, there are lots of very good web resources that describe how to set-up the pi for remote access. I used putty and tightvnc to access the pi from a windows 8.1 laptop. I also monitored and tweaked catmon from my ipod, running a light-weight ssh app.

    The screen utility allows you issue a command on the pi secure in the knowledge that should your ssh (or vnc) session terminate unexpectedly (which happens a lot on a wireless LAN) then your command will continue to run. This is particularly useful for long-lived applications, such as catmon, that run for days.

    Download the catmon.py python script and related files

    The catmon project files are stored in the catmon repository on github - see the github bootcamp instructions for an introduction to github.

    Catmon is my first github repo, so I'm still learning... There are several methods for downloading the files. You could 'clone' the repo on to the pi; or click 'download' from the catmon repo on github; or you could 'fork' the repo to your own github area and then clone that repo on the pi. Let's assume the first of these:

    pi@raspberrypi ~ $ git clone https://github.com/terry.dolan/catmon.git project-catmon

    This will create a project-catmon directory with all the key files. You can now cd to the new directory and make it your working directory for this project:
    pi@raspberrypi ~ $ cd project-catmon
    pi@raspberrypi ~/project-catmon $ ls
    <list of files downloaded from the repository>

    By the way, if you don't have git on your pi you can install it as follows:
    pi@raspberrypi ~ $ sudo apt-get install git

    If you are also getting started with git and what to know more then I recommend the The Pro Git book.

    Register a google drive app and install pydrive

    If you don't have one already start by creating a gdrive 'client' account. Rather than use the 'root' folder, I created a 'catmon-pics' folder to hold the catmon images. Note that the name of the target folder is one of the config items in the google section of the catmon.ini file - more about this config file below,

    Before catmon can upload pictures to gdrive it must be authorised to use the gdrive API. Follow the pydrive quickstart instructions to register a gdrive web app using the Google Developers Console. Once you've registered you should download the client_secrets.json file to the project-catmon directory. This file contains the gdrive app credentials. 

    You also need to register a 'service account' so that your app can upload an image to the gdrive folder without the client (that's you) having to give permission each time. Once you've registered you should download the P12 key file as keyfile.p12 to the project-catmon directory.

    The Credentials section of the Google Developers Console should look something like this:

    example Credentials screen
    You must ensure that your accounts have 'edit' (write) access to your gdrive. You do this in the 'permissions' section on the Google Developers Console.

    You also need to copy the key gdrive app data to the catmon.ini file (see below).

    Register a twitter app and install tweepy

    Start by creating a twitter app account for your catmon picture tweets. Before catmon can tweet from this account it must be authorised to use the twitter API. Follow the pi tweepy guidance on how to register your app using the Twitter Application Management. Do play close attention to the section on installing tweepy; I also had to install the latest version of pip to make it work.

    The Keys and Access Tokens section of the Twitter Application Management should look something like this:

    example Keys and Access Tokens screen

    You also need to copy the key twitter app data to the catmon.ini file (see below).

    Add your key gdrive and twitter app data to catmon.ini

    On start-up catmon opens the catmon.ini file and reads the key gdrive and twitter configuration data. Start by creating the catmon.ini from the downloaded template file:
    pi@raspberrypi ~/project-catmon $ cp catmon_template.ini catmon.ini

    Edit the catmon.ini file using nano (or your favourite editor) and replace the items between the angled brackets with the app data from google and twitter. 
    pi@raspberrypi ~/project-catmon $ nano catmon.ini

    It is tedious (and error prone) to copy the long id and credential strings from the web consoles to the config file. Note that you can use vnc to log-on to the pi and then run a web browser session to access the google and twitter developer consoles. From here you can then copy and paste the key data from the console to a nano session. Alternatively you could use a tool such as WinSCP that allows you to copy files  to the pi that you've created on your computer.

    Your updated catmon.ini file should look something like this:

    example catmon.ini

    Run catmon

    Catmon needs root privilege because it accesses the pi's gpio, so you need to run it with sudo:

    pi@raspberrypi ~/project-catmon $ sudo python catmon.py

    Catmon will continue to run until you tell it to stop using CTRL-C. By default catmon logs to catmon.log and stdout. More on logging in the Real-world Learning section below.

    Note: if python highlights any missing packages when you run catmon.py then you can install these using pip. For example:

    pi@raspberrypi ~/project-catmon $ pip install oauth2client
    pi@raspberrypi ~/project-catmon $ pip install configparser


    Catmon code structure explained

    Hopefully the python code in catmon.py is easy to follow. In summary the catmon main() function sets up the camera, gdrive and twitter, and then waits for the reed switch to be activated by the magnet. Once activated the reed_switch_event_handler() is called. The event handler takes a picture, uploads it to gdrive and tweets it.

    The code is tuned to my particular set-up using the values in the constants. You may need to change these constants to suit your own set-up. For example, the CAM_* constants affect the behaviour of the pi's camera; you may need a different CAM_DELAY (say) to ensure you get a good picture of your cat!

    Real-world Learning

    I had to solve a number of real-world problems along the way...

    Managing false alarms

    In theory the circuit diagram shown above should be simpler, without the need for the 'pull down' resistors. I started off with a much simpler circuit, using the gpio pins' built in pull down resistors. However, during testing I received many 'false alarms' where catmon detected an event but there was no cat to be seen! So, in order to minimise 'flapping' I introduced the breadboard resistors and I also coded around the false alarms. For more information on circuits with pull down resistors see here.

    As discussed here I suspect the false alarms are caused by the long, unshielded speaker wire acting like an aerial and picking up electrical interference. This is borne out by the fact that I can cause a false alarm by switching on a near-by light! Note that I did test the pi's power supply using the on-board test points and the voltage is within the expected range.

    I found that I could code around the flapping by checking to see if the REED_SWITCH_PIN is GPIO.LOW immediately after detecting a rising event. It seems that a 'proper' event, tripped by the reed switch, will (as expected) result in a GPIO.HIGH whereas a transient event does not. This method traps up to ~90 false alarms per day.

    The reed switch is triggered when the cat flap door opens (as the magnet swings past) and also when the cat flap door closes. This problem is avoided in the code by including an EVENT_GAP. This ensures that only 1 picture is taken when the cat enters.

    Gdrive, OAuth2.0 and PyDrive

    Google provide a language-specific API to make it easier to for applications to interact to their services - see here. PyDrive makes it even easier to use gdrive; with just a few lines of code you can authenticate and upload a file - see here.

    The most common scenario is that you, the developer, distribute your application (with its secret tokens) to the user. When the user runs your application it authenticates itself with google. If your application  is very popular google will come looking for you for recompense! Your application must also request that the user logs on to their gdrive account so that the application is authorised to upload files. Normally this authorisation is granted (for a limited period) at the beginning of each upload session, just before the first upload takes place. It is usually done through a web-based interactive log on. Note that the application is never aware of the user's username and password.

    This common scenario doesn't work for catmon - I didn't want to authorise catmon each time it started an upload session; I wanted catmon to work 'lights out' without my intervention. That is, I wanted catmon to act on my behalf without the need for a web-based interactive log on. Google provides a special 'service account' to cover this 'lights out' scenario. In this alternative scenario the application needs to prove its own identity to the API, and no user consent is necessary.

    For a description of the different scenarios see Using OAuth 2.0 to Access Google APIs.

    As it says in the documentation:
    "A service account's credentials, which you obtain from the Google Developers Console, include a generated email address that is unique, a client ID, and at least one public/private key pair. You use the client ID and one private key to create a signed JWT [JSON Web Token] and construct an access-token request in the appropriate format. Your application then sends the token request to the Google OAuth 2.0 Authorization Server, which returns an access token. The application uses the token to access a Google API. When the token expires, the application repeats the process."
    So, what started off as a relatively simple PyDrive assisted authentication now started to look much more complicated! I decided to persevere with PyDrive (rather than fall back to the native google python API) and work out what additional code was required to handle the the JWT token. After trial and error (and support from stack overflow e.g. here and here) I worked out how to add the relatively simple additional code. UPDATE: Note that SignedJwtAssertionCredentials was replaced by ServiceAccountCredentials in oauth2client v2.0 (see notes from June 2017 below).

    In addition to PyDrive I imported an additional python module to handle the JWT:

    from oauth2client.client import SignedJwtAssertionCredentials
    from oauth2client.service_account import ServiceAccountCredentials

    Once the gdrive data has been read from config.ini the gdrive credentials can be set and authenticated:

            svc_key = open(svc_key_file, 'rb').read() # p12 key for service
            gcredentials = SignedJwtAssertionCredentials(svc_user_id, svc_key, scope=svc_scope)
            gcredentials = ServiceAccountCredentials.from_p12_keyfile(svc_user_id, svc_key_file, scopes=svc_scope)
            gcredentials.authorize(httplib2.Http())
            gauth = GoogleAuth()
            gauth.credentials = gcredentials

    With this simple extension the gauth authentication object can be used in conjunction with the gdrive_api object to upload the picture in the event handler:

            gdrive_api = GoogleDrive(gauth) # create gdrive api object
            this_file = gdrive_api.CreateFile({'parents': [{'kind': 'drive#fileLink', 'id': gdrive_target_folder_id}]})
            this_file.SetContentFile(image_file) # Read file and set it as the content of this instance
            this_file.Upload()

    The gdrive token expires after a set period, so I needed to check the status of the token and refresh:

            if gcredentials.access_token_expired:
                gcredentials.refresh(httplib2.Http())

    Reducing motion blur

    I found that many of the cat pictures were blurred because the cats (not surprisingly) were moving. To reduce blur I increased the shutter speed (by reducing CAM_SHUTTER_SPEED). The increased shutter speed resulted in pictures that weren't blurred but were dark. Faster shutter speed need more illumination. I increased the CAM_BRIGHTNESS and CAM_CONTRAST to compensate. Further tweaking of these values may improve the quality of the pictures.

    Logging

    I wanted to output key information to a file and to the console. This is particularly important for long-running programs like catmon where lots of things can go wrong when you aren't watching!

    So I decided to improve my knowledge of python logging. I found many good resources that describe logging. For example, see the Intro to LoggingLogging HOWTO, Logging Cookbook, Good Logging Practice and Logging using DictConfig.

    I decided the most flexible approach was to store the catmon logging settings in a python dictionary. The logging configuration is in the catmon_logger_config.py file. You can edit these to suit your own needs. For example, the catmon logs are configured to output to both a file and to the console:

        'loggers': {
            'catmon': {
                'handlers': ['fileHandler', 'consoleHandler'],
                'level': 'DEBUG'
            }
        },

    The fileHandler and consoleHandler define the level of logging and other options. For example, the fileHandler logs to catmon.log and uses a 'rotating' method whereby a fixed number ('backupCount') of additional log files are created when the file reaches the pre-determined 'maxBytes' size. 

    The format of the logs is also configurable. For example the file format is defined as:


            'myFileFormatter': {
                'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
            },

    And results in the following entry in catmon.log:
    2015-03-07 12:24:19,683 - catmon - INFO - >event handler: new event at 2015-03-07 12:24:19.683331


    Component List

    Catmon uses the following software and hardware components:
    • software: python, RPi.GPIO, picamera, tweepy, pydrive, logging, configparser, oath2client, git, github, ...
    • hardware: raspberry pi, wi-fi adapter, cobbler, breadboard, connecting wires, reed switch, small magnet, camera module, camera mount, camera tripod, ...

    Here is an example of where you can buy some of the hardware - to give you an idea of just how cheap the items are. You can of course source these items from any supplier. 

    Variations On A Theme

    There are software alternatives to using a switch to detect motion. For example, I've used the motion software package. The standard motion doesn't work with the pi camera but there is a variant called motion-mmal in development which works with the RPI camera - for more information see this forum page. Motion takes snapshots and then compares the snapshots to determine if there has been motion. You can configure motion to define what sort of output you want. You can also access the live video stream remotely.

    As an aside I've also played with face recognition. I succeeded with basic human face recognition but cat recognition proved to be more difficult than I expected! Along the way I used the powerful (but complex) OpenCV. For guidance on using OpenCV with a pi camera see this article. The python OpenCV tutorials are here.

    To Do / Improvements

    Catmon showed, as I suspected, that many cats other than our family cats (Simba and Boo) were visiting and helping themselves to the cat food. When one stayed for a sleep-over we knew it was time to act. So our family cats were chipped and I installed a new cat flap containing an RFID reader, and now only our cats can enter :-). 

    I've done a little bit of investigation and there are circuit boards available that would support a DIY RFID reader. This rig could then be used to also detect which cat is entering and leaving. I could then provide a definitive view of which cat is in and which cat is out.

    Update at January 2017: Reducing False Alarms

    Catmon has been up and running for nearly 2 years and @boosimba has tweeted over 8000 times! The catmon log file shows a lot of false alarms. I noticed that occasionally catmon misses a cat picture because it is busy handling these false alarms.

    The false alarms are probably caused by the long unshielded wire from the reed switch to the breadboard picking up electromagnetic interference. In order to reduce false alarms I added a couple of capacitors (10nF and 100nF) between GPIO 23 and the GND rail.

    Breadboard with 2 new capacitors

    The capacitors successfully removed most of the false alarms. You can read more about protecting inputs with a 'low pass' filter capacitor here and here.

    Update at January 2017: Avoiding Pictures of Empty Cat Flaps

    For some time catmon has been taking pictures of empty cat flaps. This is very annoying to the thousands of twitter fans. Ok, there aren't so many fans but it's still annoying. 

    This empty cat flap picture actually means that Simba has just left the building! How do I know? Well, the empty picture happens when the cat leaves quickly and the cat flap swings back, passed the reed switch. Boo has a big fluffy tail and this has the effect of slowing the closing cat flap, so it never swings-back passed the reed switch. Simba has a wiry tail and so the cat flap isn't slowed.


    Anyway, I solved this problem by observing that the swing-back cat flap does not open as far as when a cat enters normally. By moving the reed switch and magnet higher up the cat flap I was able to ensure that the catmon picture is only triggered when a cat enters. Ah, experimental science triumphs again ;-). Here is a picture of the improvements.


    Cat flap with higher reed switch and magnet


    Update at June 2017: Reinstall Catmon On Latest Rasbpian

    My raspberry pi has been very reliable. I've experimented with various projects but the pi and catmon have just kept going. Until June 2017 that is when I encountered a corrupt SD card. This presented an opportunity to rebuild the pi from scratch with the latest version of Raspbian, Raspian Jessie with Pixel from February 2017. I ordered myself a new SD card and followed the latest instructions on raspberrypi.org. The installation with NOOBS is very easy. I then installed catmon following my own instructions herein, eating my own cat food ;-). Unfortunately catmon failed to work first time because of a problem importing SignedJwtAssertionCredentials in the oauth2client.client module

    This filled me with dread as dealing with OAuth2 was probably the most difficult part of the original catmon project. I decided to tweak the catmon.py code to work with the latest version of pydrive and oauth2client, rather than try to install old versions of the packages. After a little googling I discovered that SignedJwtAssertionCredentials had been replaced by ServiceAccountCredentials. It was straight-forward to modify the code to work with ServiceAccountCredentials, for example see the guidance here.

    I've updated the blog to reference ServiceAccountCredentials. I've also updated catmon on github with the latest code. For future reference I've uploaded a requirements.txt file with the latest version of the packages that catmon is dependent on. Note that pydrive v1.3.1 uses oauth2client v4.1.2.

    Update at October 2023: Major Change

    Catmon was updated in October 2023 to include the catmon image classifier (aka Catmonic) and a port to a 64-bit raspberry pi 3b+, giving Catmon the ability to identify Boo or Simba. Catmonic is a pytorch deep learning module that classifies a catmon image with the cat's name and a probability. 

    The latest Catmon software is available on catmon github.

    The primary aim of this update was to enhance Catmon to include the Catmonic image classifier. 
    This required a port to raspberrypi 3b+ with 64-bit OS. Several changes were introduced:
    • Use picamera2 instead of picamera, given picamera doesn't work on rpi 64-bit os
    • Introduce CatmonTestModeManager class to support more adavanced testing of catmon
    • Add the image classification using Catmonic, including update to the tweet text
    • Update to use Twitter v1 api to upload media and Twitter v2 api for sending tweets
    • Update to the logging framework, including a child logger for the event handler
    • Refactor catmon as a class to remove use of globals and split out additional methods to make more readable
    • Send images to a specific gdrive folder based on teh catmonic classification
    • Tune lighting and picamera2 parameters to get best results from camera and catmonic
    The Catmonic image classifier module should be downloaded from catmonic github

    The updated working Catmon2 project file structure on the raspberry pi looks like:

    /home/pi/pyprogs/project-catmon2/
    ├── catmonic
    │   ├── catmonic.py
    │   ├── __init__.py
    │   ├── models
    │   │   ├── catmon-img-classifier_mobilenet_v2_state_dict_0.3
    │   │   ├── __init__.py
    ├── catmonic-app
    │   ├── catmonic_cli_app.py
    │   ├── catmonic_cli_app_test_boo.sh
    │   ├── catmonic_cli_app_test_simba.sh
    │   ├── catmonic_cli_app_test_unknown.sh
    │   └── catmonic_logger.py
    ├── catmon.ini
    ├── catmon_logger_config.py
    ├── catmon_logger.py
    ├── catmon.py
    ├── catmon_template.ini
    ├── catmon_test_mode_manager.py
    ├── client_secrets.json
    ├── images
    │   ├── unseen_boo_image_2015-03-08_141742.jpg
    │   ├── unseen_simba_image_2015-05-10_180756.jpg
    │   └── unseen_unknown_image_2015-05-10_044850.jpg
    ├── keyfile.p12
    ├── LICENSE
    ├── README.md
    ├── requirements.txt


    Acknowledgements

    The links in the body of this article (and the code) highlight just some of the excellent resources that have helped me along the way. Thanks to one and all.

    No comments:

    Post a Comment