Raspberry Pi Camera Project

How to Build a Residential Intrusion Detection System #raspiSeries — Episode 3

J3
Jungletronics
8 min readAug 25, 2024

--

This continues from the previous episode.

Now, let’s add logging functionality to support future use with artificial intelligence algorithms.

Then we will send photos taken by Raspberry Pi via email.

Let’s get started!

1 Logging Functionality:

The script cam12.py uses a Raspberry Pi with a PIR sensor, LED, and camera to detect motion and capture photos. When motion is detected, the camera takes a photo with a timestamp overlay using OpenCV, and the image is saved to a specified directory. The script ensures necessary directories exist for image and log storage, and controls the LED based on motion detection. It continuously monitors the PIR sensor, taking a new photo only if a specified duration has passed since the last capture.

# cam12.py

import RPi.GPIO as GPIO
import time, cv2
from picamera2 import Picamera2, MappedArray
from libcamera import Transform
import os

PIR_PIN = 4
LED_PIN = 17
resolution = (800, 600)
LOG_FILE_NAME = "/home/pi/camera/log/photo_logs.txt"

def apply_text(request):
# Text options
colour = (255, 255, 255)
origin = (0, 60)
font = cv2.FONT_HERSHEY_SIMPLEX
scale = 1
thickness = 1
# text = "17082024 09:07"
# Get the current time in the format "DDMMYYYY HH:MM"
text = time.strftime("%d%m%Y %H:%M")
# Calculate the text size
text_size, _ = cv2.getTextSize(text, font, scale, thickness)

# Calculate the bottom-right origin
x = resolution[0] - text_size[0] - 10 # 10 pixels padding from the right
y = resolution[1] - 10 # 10 pixels padding from the bottom

origin = (x, y)
with MappedArray(request, "main") as m:
cv2.putText(m.array, text, origin, font, scale, colour, thickness)

def take_photo(picam2):
# Ensure the directory exists
if not os.path.exists("/home/pi/Camera"):
os.makedirs("/home/pi/Camera")

file_name = "/home/pi/Camera/img_" + str(time.time()) + ".jpg"
# picam2.capture_file(file_name)
picam2.switch_mode_and_capture_file(capture_config, file_name)
print(f"Photo saved: {file_name}")
return file_name

# Ensure that the directory exists before attempting to write to the log file
def update_photo_log_file(photo_file_name):
# Ensure the directory exists
log_directory = os.path.dirname(LOG_FILE_NAME)
if not os.path.exists(log_directory):
os.makedirs(log_directory)

with open(LOG_FILE_NAME, "a") as f:
f.write(photo_file_name)
f.write("\n")

# Setup camera
picam2 = Picamera2()
# picam2.configure(picam2.create_still_configuration(transform=Transform(rotation=180)))
# Create two separate configs - one for preview and one for capture.
# Make sure the preview is the same resolution as the capture, to make
# sure the overlay stays the same size
capture_config = picam2.create_still_configuration({"size": resolution}, transform=Transform(hflip=True, vflip=True))
preview_config = picam2.create_preview_configuration({"size": resolution}, transform=Transform(hflip=True, vflip=True))

# Set the current config as the preview config
picam2.configure(preview_config)

# Add the timestamp
picam2.pre_callback = apply_text
# Start the camera
picam2.start(show_preview=False)
picam2.start() # Start the camera

# Pause for 2 seconds to allow the camera to stabilize
time.sleep(2)
print("Camera setup ok.")

# Setup GPIOs
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIR_PIN, GPIO.IN)
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.output(LED_PIN, GPIO.LOW)
print("GPIOs setup ok.")

MOV_DETECT_THRESHOLD = 3.0 # Time threshold for sustained motion
MIN_DURATION_BETWEEN_PHOTOS = 60.0 # Minimum time between two photos (in seconds)

last_pir_state = GPIO.input(PIR_PIN)
movement_timer = time.time()
last_time_photo_taken = 0 # Initialize last photo time to 0

print("Everything has been set up.")

try:
while True:
time.sleep(0.01)
pir_state = GPIO.input(PIR_PIN)

# Activate LED when movement is detected.
GPIO.output(LED_PIN, GPIO.HIGH if pir_state == GPIO.HIGH else GPIO.LOW)

# Detecting the start of motion
if last_pir_state == GPIO.LOW and pir_state == GPIO.HIGH:
movement_timer = time.time()

# Sustained motion detection
if last_pir_state == GPIO.HIGH and pir_state == GPIO.HIGH:
if time.time() - movement_timer > MOV_DETECT_THRESHOLD:
# Check if enough time has passed since the last photo
if time.time() - last_time_photo_taken > MIN_DURATION_BETWEEN_PHOTOS:
print("Take Photo and Send it by Email")
photo_file_name = take_photo(picam2)
update_photo_log_file(photo_file_name)
last_time_photo_taken = time.time() # Update the last photo taken time

last_pir_state = pir_state

except KeyboardInterrupt:
GPIO.cleanup()
picam2.stop()

The logging functionality in the script involves recording the filenames of the photos taken by the Raspberry Pi camera when motion is detected. This is done in the update_photo_log_file function. The function first checks if the directory for the log file (/home/pi/camera/log/) exists, and if not, creates it to ensure the log file can be written. It then appends the name of each photo taken, including its full path, to the photo_logs.txt file, each entry on a new line, allowing the user to keep a record of all captured photos.

Here is the content of the camera/log/photo_logs.txt file.

2 Sending Email:

to prevent this error:

ModuleNotFoundError: No module named 'yagmail'

Open Raspberry Pi Terminal and type:

pip3 install yagmail

If it doesn’t work (I’m currently running into issues on bookworm), you’ll need to manually download and install the yagmail package.

This is my OS version:

cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

To install Yagmail manually, first visit the Yagmail PyPI page to find the latest version (mine is 0.15.293).

Then, run these commands:

sudo apt-get update

[1-Steps to Install yagmail Using Updated Methods]
[Find the Latest Version of yagmail:]


[2-Download the Latest Source Code:]

wget https://files.pythonhosted.org/packages/source/y/yagmail/yagmail-0.15.293.tar.gz
tar -xzvf yagmail-0.15.293.tar.gz

[3-Extract and Install the Package:]

tar -xzvf yagmail-0.15.293.tar.gz
cd yagmail-0.15.293
sudo python3 setup.py install

[4-Install Dependencies:]

sudo apt-get install python3-keyring python3-requests

[5-Verify the Installation:]

python3 -c "import yagmail; print(yagmail.__version__)"

0.15.293

Once you install Yagmail sucessfully now let’s Securely Save Google Email Credentials in Python.

First, you’ll need to learn how to create app passwords. Follow this instructions. Create one secure token and proceed.

Second, Open your Raspberry Pi terminal.

  1. Edit the profile file (e.g., ~/.bashrc or ~/.bash_profile)
nano ~/.bashrc

Add the following line at the end of the file:

export FROM_EMAIL='your_email_account_here'
export TO_EMAIL='other_email_destination_here'
export EMAIL_TOKEN='your_secure_token_here'
  • Save the file and close the editor.
  • Reload the file to apply the changes:
source ~/.bashrc

3 Now let’s code!

# cam13.py

import RPi.GPIO as GPIO
import time, cv2
from picamera2 import Picamera2, MappedArray
from libcamera import Transform
import os
import yagmail


email_token = os.getenv('EMAIL_TOKEN')
if not email_token:
raise ValueError("No EMAIL TOKEN found. Set the EMAIL_TOKEN environment variable.")

from_email = os.getenv('FROM_EMAIL')
if not from_email:
raise ValueError("No FROM EMAIL ACCOUNT found. Set the FROM EMAIL environment variable.")

to_email = os.getenv('TO_EMAIL')
if not to_email:
raise ValueError("No TO EMAIL ACCOUNT found. Set the TO_EMAIL environment variable.")

PIR_PIN = 4
LED_PIN = 17
resolution = (800, 600)
LOG_FILE_NAME = "/home/pi/Camera/log/photo_logs.txt"

def apply_text(request):
# Text options
colour = (255, 255, 255)
origin = (0, 60)
font = cv2.FONT_HERSHEY_SIMPLEX
scale = 1
thickness = 1
# text = "17082024 09:07"
# Get the current time in the format "DDMMYYYY HH:MM"
text = time.strftime("%d%m%Y %H:%M")
# Calculate the text size
text_size, _ = cv2.getTextSize(text, font, scale, thickness)

# Calculate the bottom-right origin
x = resolution[0] - text_size[0] - 10 # 10 pixels padding from the right
y = resolution[1] - 10 # 10 pixels padding from the bottom

origin = (x, y)
with MappedArray(request, "main") as m:
cv2.putText(m.array, text, origin, font, scale, colour, thickness)

def take_photo(_picam2):
# Ensure the directory exists
if not os.path.exists("/home/pi/Camera"):
os.makedirs("/home/pi/Camera")

file_name = "/home/pi/Camera/img_" + str(time.time()) + ".jpg"
# picam2.capture_file(file_name)
_picam2.switch_mode_and_capture_file(capture_config, file_name)
print(f"Photo saved: {file_name}")
return file_name

# Ensure that the directory exists before attempting to write to the log file
def update_photo_log_file(_photo_file_name):
# Ensure the directory exists
log_directory = os.path.dirname(LOG_FILE_NAME)
if not os.path.exists(log_directory):
os.makedirs(log_directory)

with open(LOG_FILE_NAME, "a", encoding="utf-8") as f:
f.write(_photo_file_name)
f.write("\n")

def send_email_with_photo(yagmail_client, file_name):
yagmail_client.send(to= to_email,
subject="Movement detected!",
contents="Here's a photo taken by your Raspberry Pi",
attachments=file_name)

# Setup camera
picam2 = Picamera2()
# picam2.configure(picam2.create_still_configuration(transform=Transform(rotation=180)))
# Create two separate configs - one for preview and one for capture.
# Make sure the preview is the same resolution as the capture, to make
# sure the overlay stays the same size
capture_config = picam2.create_still_configuration({"size": resolution}, transform=Transform(hflip=True, vflip=True))
preview_config = picam2.create_preview_configuration({"size": resolution}, transform=Transform(hflip=True, vflip=True))

# Set the current config as the preview config
picam2.configure(preview_config)

# Add the timestamp
picam2.pre_callback = apply_text
# Start the camera
picam2.start(show_preview=False)
picam2.start() # Start the camera

# Pause for 2 seconds to allow the camera to stabilize
time.sleep(2)
print("Camera setup ok.")

# Remove log file
if os.path.exists(LOG_FILE_NAME):
os.remove(LOG_FILE_NAME)
print("Log file removed.")

# Setup yagmail
password = email_token
yag = yagmail.SMTP(from_email, password)
print("Email sender setup OK.")

# Setup GPIOs
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIR_PIN, GPIO.IN)
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.output(LED_PIN, GPIO.LOW)
print("GPIOs setup ok.")

MOV_DETECT_THRESHOLD = 3.0 # Time threshold for sustained motion
MIN_DURATION_BETWEEN_PHOTOS = 60.0 # Minimum time between two photos (in seconds)

last_pir_state = GPIO.input(PIR_PIN)
movement_timer = time.time()
last_time_photo_taken = 0 # Initialize last photo time to 0

print("Everything has been set up.")

try:
while True:
time.sleep(0.01)
pir_state = GPIO.input(PIR_PIN)

# Activate LED when movement is detected.
GPIO.output(LED_PIN, GPIO.HIGH if pir_state == GPIO.HIGH else GPIO.LOW)

# Detecting the start of motion
if last_pir_state == GPIO.LOW and pir_state == GPIO.HIGH:
movement_timer = time.time()

# Sustained motion detection
if last_pir_state == GPIO.HIGH and pir_state == GPIO.HIGH:
if time.time() - movement_timer > MOV_DETECT_THRESHOLD:
# Check if enough time has passed since the last photo
if time.time() - last_time_photo_taken > MIN_DURATION_BETWEEN_PHOTOS:
print("Take Photo and Send it by Email")
photo_file_name = take_photo(picam2)
update_photo_log_file(photo_file_name)
send_email_with_photo(yag, photo_file_name)
last_time_photo_taken = int(time.time()) # Update the last photo taken time

last_pir_state = pir_state

except KeyboardInterrupt:
GPIO.cleanup()
picam2.stop()

In the script, the send_email_with_photo function uses the yagmail library to send an email with an attached photo. It sends an email to to_emal with the subject Movement detected! and includes the photo as an attachment. The yagmail_client parameter is an instance of yagmail.SMTP configured with the sender's email and password. This function is called after a photo is taken, ensuring that each captured image is emailed to the specified address.

Run the script in the terminal instead of using the Thonny app.

Running on terminal: python /home/pi/Desktop/cam13.py. The issue you’re encountering in Thonny is due to the fact that Thonny, unlike the terminal, does not automatically inherit environment variables set in the shell session or a .env file. When you run your script in Thonny, it starts a new Python process that doesn't have access to the environment variables defined in your shell or terminal. For production environments and to keep credentials secure, continue running your script from the terminal where environment variables are properly set.

The result:

The email inbox successfully received the email. Great!
The timestamp feature is a fantastic addition to this app, don’t you think?

In the next installment, we’ll implement the web interface.

For now, thank you!

Goodbye!

👉GitHub Repo Episode 3

👌 Review all additions.

Related Posts

Raspberry PI

0#Episode — #raspiSeries — Raspberry Pi Intro — Quick And Easy Way To Install Raspberry Pi OS

1#Episode — #raspiSeries — Raspberry Pi Camera Module — How To Connect the Rpicam, Take Pictures, Record Video, and Apply image effects

2#Episode — #raspiSeries — Raspberry Pi Camera Project — How to Build a Residential Intrusion Detection System

3#Episode — #raspiSeries — Raspberry Pi Camera Project — How to Build a Residential Intrusion Detection System (this one)

[Ubuntu Terminal - Host]

sudo scp pi@192.168.0.114:/home/pi/Desktop/*.py /home/j3/Documents/raspi_projects/Episode_3

[Change the Owner for your User Name]

sudo chown -R j3:j3 /home/j3/Documents/raspi_projects/Episode_3

--

--

J3
Jungletronics

😎 Gilberto Oliveira Jr | 🖥️ Computer Engineer | 🐍 Python | 🧩 C | 💎 Rails | 🤖 AI & IoT | ✍️