Introduction: WiFi Security Camera With a Pi Zero 2W

Have you ever left home and felt that something was wrong in your house? Well, worry no more! With this feature-packed yet incredibly simple setup, you can monitor your house whenever you like with just a click on your device.

This project uses just a $15 Pi Zero 2W and a small camera to make a simple, fully functioning, and compact WiFi security camera. The webcam continuously captures frames and sends them to the computer, and when it detects a sudden difference in consecutive frames due to some kind of motion, the camera records a short video. It then saves the video to the Pi Zero, uploads it to Google Drive, and sends a web request to IFTTT and thus, alerts the user in some way. The live camera feed can be viewed from any device with just one click. Pretty cool, right?

In this detailed guide, I will walk you through every step in building this awesome project from scratch. Although this might seem very long, it is a very fun process and the end result is definitely worth the patience.

Supplies

Step 1: Setting Up the Pi Zero 2W

First, we need an SD card for the Raspberry Pi. Since we don't have a lot of code for this project, you can use a relatively small 8GB microSD card. Download the Raspberry Pi Imager to flash the OS onto the SD card. Insert the SD card into your laptop's SD card slot.

In the app, go to Raspberry Pi OS (other) --> Raspbian OS Lite (32-bit for Pi Zero W, 64-bit for Pi Zero 2W). Choose the SD card you just inserted.

In Settings (bottom-right corner), set the hostname to raspberrypi.local, enable SSH, set the username and password to anything you like (this will be used to log into the Pi Zero), and configure the WiFi SSID and password.

Finally, click "SAVE" and then click "WRITE". Be careful not to remove the SD card or turn your laptop off during the flashing process, as this will corrupt the card. After the flash is done, you can remove the SD card from your laptop. Insert the SD card into the Pi with the pins facing down, connect the Pi to power (5V microUSB adapter), and wait for it to boot up (approx. 3 minutes for first boot-up).

Step 2: Finding the Pi's IP Address

To SSH into the Pi and access its terminal, we need its IP address. Since we don't know the address, we use Angry IP Scanner for scanning all IP addresses on our network.

Open Angry IP Scanner and click "Start". Once the scanning is complete, scroll down till you find an IP address with "raspberrypi.lan" or something similar written next to it, and copy the address.

Step 3: Accessing the Pi's Terminal

For this project, we will be using our laptop's terminal to communicate with the Pi Zero.

Open your laptop's terminal (Command Prompt in my case), and type

ssh pi@ip_address

Replace "ip_address" with the IP address you copied from Angry IP Scanner (Example: ssh pi@192.168.1.137). The Pi Zero will ask for a password, and you can type the password which you set when flashing the OS onto the SD card. If the terminal asks you if you want to proceed with the connection, type "yes". Once we are connected to the Pi Zero, your terminal should look something like this.

Step 4: Installing Packages

Packages are used in Python to do various functions within your code. For example, if you want to post a web request to a server, you use the 'requests' package, which allows you to type 'requests.post(.......)' instead of having to type 20 lines of code from scratch.

For this project, the packages we need are:

  • picamera2: for accessing the Raspberry Pi Camera
  • opencv: for processing the video, inserting text, and saving the video
  • numpy: to process image arrays
  • time: to insert delays in the code
  • datetime: to get the current date and time so that the video can be saved with it
  • pydrive: to upload the recorded video to Google Drive
  • os: to join file paths and filenames
  • requests: to post Webhooks requests which will allow us to create applets to notify us.
  • flask: to run a web server, live stream video, and respond to incoming requests.
  • threading: to run two processes at the same time so that the live stream never gets interrupted.

The time, os, and threading libraries are built into the Raspberry Pi OS, so they need not be installed.

To install all these libraries, we use 'pip3' which is the official package installer for Python 3. When installing anything, keep an eye on the screen, as the computer will ask you for y/n permission sometimes. Install pip3 and opencv using the sudo apt-get command:

sudo apt-get install python3-pip
sudo apt-get install python3-opencv

To install all the packages, type this command in the terminal:

sudo pip3 install numpy datetime pydrive requests flask picamera2

[Note] : If you are facing any errors when installing the packages, install them one-by-one.

Step 5: The Wiring and Case

This is the most straightforward wiring on any Raspberry Pi project. First, turn off the Pi Zero using 'sudo halt'. Disconnect power after it shuts down and find the CSI camera connector on the edge of the Pi Zero 2W. Carefully slide the grey latch towards the edge, slide the camera cable (the small one that came with the case) into the connector with the pins facing down, and slide the latch back in. Connect your camera to the other end of the cable with the pins facing the board.

To put the Pi Zero in its case, insert the Pi Zero at an angle into the case, aIign the ports, and then push it down.

To install the camera to the top of the case, get the top panel with the hole in it. You may need to make the hole square depending on your camera model.

Simply line up the holes on the camera board with the standoffs on the top panel and push it into place.

Power the Pi back on, and log into the Pi from your computer's terminal as we did at the beginning. Type:

sudo nano /boot/config.txt

in the terminal. At the end of the file, add:

start_x=1

Hit Ctrl+S and Ctrl+X. Reboot the Pi Zero using sudo reboot.

Step 6: Making IFTTT Applets

For this project, we need to make two IFTTT applets: one for sending a text message when motion is detected, and the other for sending a text message when the system is running.

Go to the IFTTT website and sign in with your Google account. Click on the "Create" button at the top. For "If This", search and select "Webhooks". Select the "Receive a web request" option as the trigger. Click on "Connect" at the bottom to connect your account to Webhooks. Type "motion_detected" as the event name and click "Create trigger".

For "Then That", select "Notifications", and click on "Send a notification from the IFTTT App". Set the message to anything you like (mine gives the current date and time) and finish the applet.

Create another applet in the same manner but with 'pi_active' as the event name. For these applets to work, you need to install the IFTTT app on your phone and sign in with the same account you used to create the applets.

Step 7: Google Drive Credentials

To connect the Pi Zero to our Google Drive and gain access, we need to set up Google Drive API. For this, go to Google Cloud Console and sign in with your Google account. Click on the dropdown and click on "New Project" at the top-right corner of the new dialog box. Give a name for your project and click "Create". In the menu on the left side, go to APIs and Services--> OAuth Consent Screen. Select your project and follow the steps below.

Click on "Configure consent screen", select "External", enter the app name, user support email and developer email, and click "Save and continue". Continue on the "Scopes" page without changing anything. In the "Test users" screen, click "Add users", enter your email address (you will access the Google Drive linked to this account), click "ADD", "Save and Continue", and then "Go back to dashboard".

Go to APIs and Services--> Library. Search for the Google Drive API and enable it. Go to the Credentials tab in the middle, and then click on "Create Credentials" and select "OAuth Client ID". Set the application type as 'desktop app' and click 'Create'. A new window will pop up, where you should copy the Client ID and Client Secret.

Step 8: Creating Files on the Pi

We will use the Nano text editor to create all the files we need. First, type 'nano settings.yaml' to create a YAML file that stores the Google Drive settings. Paste below code into the file, and make sure to replace "YOUR_CLIENT_ID" and "YOUR_CLIENT_SECRET" with the ID and Secret you copied:

client_config_backend: settings
client_config:
  client_id: YOUR_CLIENT_ID
  client_secret: YOUR_CLIENT_SECRET

save_credentials: True
save_credentials_backend: file
save_credentials_file: drive_credentials.json

get_refresh_token: True

oauth_scope:
  - https://www.googleapis.com/auth/drive.file
  - https://www.googleapis.com/auth/drive.install

Click Ctrl+S to save and Ctrl+X to exit.

To get the drive_credentials.json file, we need a browser. Since the Pi Zero does not support Chromium, we have to do this on our computer. For this, download Python on your computer, open a new sketch, and paste this code:

from pydrive.drive import GoogleDrive
from pydrive.auth import GoogleAuth 

gauth = GoogleAuth()
gauth.LocalWebserverAuth()
drive = GoogleDrive(gauth)

Save this code in a folder. In the same folder, create a file named 'settings.yaml' and paste the same code as you did on the Pi Zero. Open a new terminal on your computer, and type 'pip3 install pydrive'. Once the installation is complete, run the Python sketch. Your browser will open and ask you for some permissions. Select a Google account to access its Google Drive, and click "Select All" and "Continue".

Once the process is complete, you should see a message that says "The authentication flow has completed". Close the program and the Python IDLE. In the folder in which you saved the code, there should be a file named 'drive_credentials.json'. Open the file with Notepad and copy the contents. Go back to the terminal, type

nano drive_credentials.json

Paste the copied text, hit Ctrl+S, and then click Ctrl+X to exit. Finally, enter this command to make a folder for the output videos:

sudo mkdir output_vids

Step 9: Webhooks Key

Go to the "Explore" tab on the IFTTT website and search for Webhooks. Go to settings in the top right corner and click on documentation. Copy your key at the top of the page.

Step 10: The Code

Type 'nano motion_camera.py' to create a Python file. In the editor, paste the code below. THE FULL CODE WITHOUT EXPLANATION IS AT THE END OF THE STEP.

#!/usr/bin/env python
from picamera2 import Picamera2
import cv2
import numpy as np
import time
import datetime
from datetime import date
from pydrive.drive import GoogleDrive
from pydrive.auth import GoogleAuth  
import os
import requests
from flask import Flask, render_template
from flask import Response
from threading import Thread

Here, we are importing all the libraries we need for the project.


app = Flask(__name__)

camera = Picamera2()
capture_config = camera.create_preview_configuration()
camera.start(capture_config)

font = cv2.FONT_HERSHEY_SIMPLEX
fourcc = cv2.VideoWriter_fourcc(*'mp4v') 

Creating the Flask server object, starting the Raspberry Pi camera, and setting the font for inserting text into the live stream.


global vid_dir
vid_dir=r"/home/pi/output_vids"
global motiondetection
motiondetection=0

Setting the file path to save the recorded videos. The 'motiondetection' variable is used to turn the motion detection feature on or off. The value of this variable is 0 or 1, and it is controlled through a web request.


gauth = GoogleAuth()
gauth.LocalWebserverAuth()
drive = GoogleDrive(gauth)

Setting up and configuring the Google Drive API to get access to our Google Drive.


def motionvideo():
    global motionvideostart, frame
    motionvideostart=0

Here, we create a function that records a video when motion is detected. The 'motionvideostart' variable is used to know whether this function can be called. When the program enters this function, the 'motionvideostart' variable is set to 0. This means that this function is not to be called until the variable is set to 1 (when this function has finished executing). The 'frame' variable stores the frame that the camera captures continuously. Both of these variables are declared as 'global' because they need to be accessed from other functions in the program.


    now=None
    now=datetime.datetime.now()
    filename=None
    filename="video "+str(date.today())+" "+str(now.hour)+":"+str(now.minute)+".mp4"
    filepath=None
    filepath=os.path.join(vid_dir,filename)
    print(filepath)
    out=None
    out = cv2.VideoWriter(str(filepath),fourcc, 20.0, (640, 480))

Here, we are creating the filename for the video. Since we cannot rewrite variables easily in Python, we are deleting the variables and creating them again. First, we set the 'now' variable to hold the current time. Then, we set the filename to contain the date and current time. We then use 'os.join' to join the 'filename' to the 'filepath' variable, which tells the program which folder to save the video in. Finally, we create a VideoWriter object with the current file path and name to write the video to a file and save it.


    for x in range(500):
        out.write(frame)
        cv2.waitKey(1)
    out.release()

This piece of code gets the frame that the camera is capturing, appends it to a video file, and when the loop exits, releases the VideoWriter object so that we can set the new filename with a new date and time. You can increase the range value if you want a longer video.


    print("Uploading file...")
    f = drive.CreateFile({'title': str(filename)})
    f.SetContentFile(str(filepath))
    f.Upload()
    f=None
    print("Done")

Here, we are uploading the saved video file to Google Drive. We create an empty file with the filename we set earlier as the title. We set the content of the file to be the video that we just saved, and then we upload the file to Google Drive. 


    print("Posting web request...")
    r = requests.post('https://maker.ifttt.com/trigger/motion_detected/json/with/key/YOUR_IFTTT_KEY')
    print("Done")
    motionvideostart=1

We post a web request to Webhooks using requests. Replace 'YOUR_IFTTT_KEY' with your Webhooks integration key.


def mse(frame1,frame2):
   h, w = frame1.shape
   diff = cv2.subtract(frame1, frame2)
   err = np.sum(diff**2)
   mse = err/(float(h*w))
   return mse

This function gets two frames, frame1 and frame2, and calculates their Mean Squared Error. This returns a float value which tells us how different the two frames are. We use this value to determine if there is motion because when something (or someone) moves, there will be a difference in the captured frames.


def livedetection():
    global frame, click, motiondetection, motionvideostart
    motionvideostart=1
    framecount=0
    click=0
    nextframe=0
    while True:
        global frame 
        frame=camera.capture_array("main")
        cv2.waitKey(1)
        framecount+=1
        print(framecount)
        if framecount%5==0:
            frame1mse=frame
            frame1gray=cv2.cvtColor(frame1mse,cv2.COLOR_BGR2GRAY)
            nextframe=framecount+4

        if framecount==50:
            framecount=0

        if framecount==nextframe:
            frame2mse=frame
            frame2gray=cv2.cvtColor(frame2mse,cv2.COLOR_BGR2GRAY)
            error = mse(frame1gray,frame2gray)
            print(error)
            if error>=20.0:
                cv2.putText(frame, "Motion detected", (50,50), font, 1, (0,255,0), 2)
            if error>=20.0 and motiondetection==1 and motionvideostart==1:
                t1 = Thread(target=motionvideo)
                t1.start()
        if click==1:
            break

The 'motionvideostart' variable is set to 1, which tells the program that it can call the function to record video. 'framecount' is used to keep track of the frame number, 'click' is used to stop the video capture when a web request tells the program to do so, and 'nextframe' is used to determine the frame number to be sent to the mse() function as the second frame.

In the loop, the camera continuously captures frames. When the frame number is a multiple of 5, the captured frame is stored into 'frame1mse' and then converted to greyscale. The next frame number to capture is set to be the current frame number+4. So, when the frame number is the same as 'nextframe', the captured frame is stored in 'frame2mse' and converted to greyscale. After the second frame is captured, both the frames are sent to the mse() function to compute their difference.

If the error returned by the mse() function is greater than 20.0, we insert a text that says "Motion detected" into a corner of the frame. You can change the MSE threshold if you want to. If the 'motiondetection' variable is set to 1 (the motion detection feature is turned on) and the 'motionvideostart' variable is set to 1 (the program is allowed to call the video recording function), we start a thread that calls the video recording function in parallel to this function. Finally, if the 'click' variable becomes 1 (a web request was received telling the program to stop monitoring), the function is terminated.


def webframes():
    print("Entering live feed")
    global frame
    while True:
        try:
            ret, buffer = cv2.imencode('.jpg', frame)
            webframe = buffer.tobytes()
            yield (b'--webframe\r\n'
                    b'Content-Type: image/jpeg\r\n\r\n' + webframe + b'\r\n')
        except:
            break

This function live streams the video to a webpage.

@app.route('/')
def index():
    return "The system is running. For live feed, go to /livefeed"

@app.route('/start')
def startlive():
    t = Thread(target=livedetection)
    t.start()
    return "System started"

@app.route('/stop')
def stoplive():
    global click
    click=1
    return "System stopped"


@app.route('/livefeed')
def live_feed():
    return Response(webframes(), mimetype='multipart/x-mixed-replace; boundary=webframe')

@app.route('/disablemotion')
def disable():
    global motiondetection
    motiondetection=0
    return "Motion detection turned off"

@app.route('/enablemotion')
def enable():
    global motiondetection
    motiondetection=1
    return "Motion detection turned on"

#Sends a device activated message to let the user know that the program is running.
requests.post('https://maker.ifttt.com/trigger/pi_active/json/with/key/YOUR_IFTTT_KEY')

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=False) #Runs the web server.

Replace 'YOUR_IFTTT_KEY' with your key.


Here is the complete code for your convenience.

#!/usr/bin/env python
from picamera2 import Picamera2
import cv2
import numpy as np
import time
import datetime
from datetime import date
from pydrive.drive import GoogleDrive
from pydrive.auth import GoogleAuth  
import os
import requests
from flask import Flask, render_template
from flask import Response
from threading import Thread

app = Flask(__name__)

camera = Picamera2()
capture_config = camera.create_preview_configuration()
camera.start(capture_config)

font = cv2.FONT_HERSHEY_SIMPLEX
fourcc = cv2.VideoWriter_fourcc(*'mp4v') 

global vid_dir
vid_dir=r"/home/pi/output_vids"
global motiondetection
motiondetection=0

gauth = GoogleAuth()
gauth.LocalWebserverAuth()
drive = GoogleDrive(gauth)

def motionvideo():
    global motionvideostart, frame
    motionvideostart=0
    now=None
    now=datetime.datetime.now()
    filename=None
    filename="video "+str(date.today())+" "+str(now.hour)+":"+str(now.minute)+".mp4"
    filepath=None
    filepath=os.path.join(vid_dir,filename)
    print(filepath)
    out=None
    out = cv2.VideoWriter(str(filepath),fourcc, 20.0, (640, 480))

    for x in range(500):
        out.write(frame)
        cv2.waitKey(1)
    out.release()

    print("Uploading file...")
    f = drive.CreateFile({'title': str(filename)})
    f.SetContentFile(str(filepath))
    f.Upload()
    f=None
    print("Done")

    print("Posting web request...")
    r = requests.post('https://maker.ifttt.com/trigger/motion_detected/json/with/key/YOUR_IFTTT_KEY')
    print("Done")
    motionvideostart=1

def mse(frame1,frame2):
   h, w = frame1.shape
   diff = cv2.subtract(frame1, frame2)
   err = np.sum(diff**2)
   mse = err/(float(h*w))
   return mse

def livedetection():
    global frame, click, motiondetection, motionvideostart
    motionvideostart=1
    framecount=0
    click=0
    nextframe=0
    while True:
        global frame 
        frame=camera.capture_array("main")
        cv2.waitKey(1)
        framecount+=1
        print(framecount)
        if framecount%5==0:
            frame1mse=frame
            frame1gray=cv2.cvtColor(frame1mse,cv2.COLOR_BGR2GRAY)
            nextframe=framecount+4

        if framecount==50:
            framecount=0

        if framecount==nextframe:
            frame2mse=frame
            frame2gray=cv2.cvtColor(frame2mse,cv2.COLOR_BGR2GRAY)
            error = mse(frame1gray,frame2gray)
            print(error)
            if error>=20.0:
                cv2.putText(frame, "Motion detected", (50,50), font, 1, (0,255,0), 2)
            if error>=20.0 and motiondetection==1 and motionvideostart==1:
                t1 = Thread(target=motionvideo)
                t1.start()
        if click==1:
            break

def webframes():
    print("Entering live feed")
    global frame
    while True:
        try:
            ret, buffer = cv2.imencode('.jpg', frame)
            webframe = buffer.tobytes()
            yield (b'--webframe\r\n'
                    b'Content-Type: image/jpeg\r\n\r\n' + webframe + b'\r\n')
        except:
            break

@app.route('/')
def index():
    return "The system is running. For live feed, go to /livefeed"

@app.route('/start')
def startlive():
    t = Thread(target=livedetection)
    t.start()
    return "System started"

@app.route('/stop')
def stoplive():
    global click
    click=1
    return "System stopped"

@app.route('/livefeed')
def live_feed():
    return Response(webframes(), mimetype='multipart/x-mixed-replace; boundary=webframe')

@app.route('/disablemotion')
def disable():
    global motiondetection
    motiondetection=0
    return "Motion detection turned off"

@app.route('/enablemotion')
def enable():
    global motiondetection
    motiondetection=1
    return "Motion detection turned on"

#Sends a device activated message to let the user know that the program is running.
requests.post('https://maker.ifttt.com/trigger/pi_active/json/with/key/YOUR_IFTTT_KEY')

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=False) #Runs the web server.

Once you have all the code pasted into the Python file, press Ctrl+S to save and then Ctrl+X to exit the editor.

Step 11: First Test

In the terminal, type:

python3 /home/pi/motion_camera.py

to run your script. If there are no errors, you should see something like this on your screen:

If this works, you can close the program using Ctrl+C.

Step 12: Configure the Pi to Run the Code on Startup

We are going to use 'crontab' to run this code on startup. For this, type 'sudo crontab -e' in your terminal. If it asks you for the editor, enter 1. At the end of the text, in a new line, type:

@reboot python3 /home/pi/motion_camera.py

Hit Ctrl+s to save and then Ctrl+X to exit. Type 'crontab -e' in the terminal again and paste the same code at the end of the file. Hit Ctrl+S and Ctrl+X, and we're done!

Step 13: The Best Part: the Completion

If you've made it this far, CONGRATULATIONS!!! It is finally the moment to see the fruit of your hard work in action. Type 'sudo halt' in the terminal to shut down the Pi Zero 2W. After it's completely shut down, turn off the power and turn it back on. Wait for the Pi Zero to boot up.

You will receive a notification saying your Pi Zero is active (this is from the IFTTT applet). In any browser on a device connected to the same Wi-Fi network as the Pi Zero, go to 'http://ip_address_of_your_pi/start' and then go to 'http://ip_address_of_your_pi/livefeed'. You should see your security camera in action.

The lack of smoothness in the video attached below is due to the limited capabilities of the Pi Zero W that I used. This should be rectified if you are using a Pi Zero 2W or something more powerful. I have attached a second video of the same program run on a Raspberry Pi 3 Model B+ so you can see the ideal framerate.

Additionally, if you want to be able to access the camera from anywhere, you can set up port forwarding on your router. You can also create an app or use Shortcuts on your iPhone to access these URLs through buttons.

Let me know in the comments if you have any questions. Thank you so much for checking this out and good luck!