VMware ESXi and vSphere Cluster Management
Build a Raspberry Pi Camera Motion Detector with Python
Learn how to configure a Raspberry Pi Camera Module, compare frames with Python and Pillow, and save timestamped photos when visual motion is detected.
What this project does
This project uses a Raspberry Pi Camera Module to detect visual motion. The program continuously captures frames, compares each new frame with a previous reference frame, and measures how much the pixels changed. When the change exceeds a configured threshold, the program saves a timestamped still image.
A frame is one individual image captured from the camera. This is different from using a dedicated PIR sensor: a PIR sensor detects changes in infrared radiation, while this project detects changes visible in camera images.
Suitable uses include monitoring a room, observing wildlife at a feeder, and prototyping a camera-based physical-computing project. Use camera monitoring only where recording is permitted. Inform people when notification or consent is required, protect stored images, and do not expose camera services publicly without authentication.
Hardware and software requirements
Connect and configure the camera
Connect the ribbon cable safely
Turn the Raspberry Pi off and disconnect its power before connecting or reseating the cable. Open the camera connector latch, insert the ribbon cable straight and completely, then close the latch. Check the connector orientation for your particular Pi model and camera module. A loose, reversed, or partially inserted cable is a common cause of camera failures.
Update Raspberry Pi OS
sudo apt update
sudo apt full-upgradeRestart if the upgrade installs a new kernel, firmware, or camera-related packages.
Understand the camera software choices
Test the camera before debugging Python:
rpicam-still -o test.jpgSome older releases use an earlier command name instead. A successful test image confirms basic camera connection and software support.
Install Python dependencies
Picamera2 is the Python interface commonly used with the current libcamera stack. Pillow is the maintained Python image-processing library compatible with the older PIL programming model. PIL means Python Imaging Library; the original PIL project is old, while Pillow is its actively maintained replacement.
sudo apt install python3-picamera2 python3-pilUse the package names supplied by your Raspberry Pi OS release. On an older installation that still uses the legacy interface, the historically associated command may be:
sudo apt install python-picamera python-imaging-tkPackage availability varies. python-imaging-tk is not a general replacement for Pillow; it provides Tk integration and may be unavailable or unsuitable. For Python 3 image processing, prefer the available python3-pil package. Install a legacy picamera package only when the operating system and camera stack actually require it.
Check that the interpreter used to run the script is Python 3 and can import the packages:
python3 -c "from picamera2 import Picamera2; from PIL import Image; print('imports OK')"Installing a package for one interpreter does not make it available to a different virtual environment or Python executable. Always run the script with the same interpreter you tested.
How frame-based motion detection works
- Start the camera and allow automatic exposure and white balance to settle.
- Capture an initial reference frame showing the normal scene.
- Wait for a selected interval and capture a subsequent frame.
- Resize or capture a small frame to reduce processing cost.
- Convert both images to grayscale when color is not needed.
- Compute an image difference: each difference pixel represents a change between the two frames.
- Measure the difference intensity and the fraction of pixels that changed significantly.
- Trigger only when the configured motion score and minimum changed area exceed their thresholds.
- Save an image, then wait through a cooldown period before allowing another trigger.
- Use the new frame as the next reference frame.
A false positive is a trigger caused by something other than the intended subject, such as a shadow, changing daylight, automatic exposure, or sensor noise. A sensitivity setting controls how easily the detector triggers; in general, a lower threshold means higher sensitivity.
Complete Picamera2 motion detector
Create a directory for captures:
mkdir -p ~/motion-imagesSave the following source as motion_detector.py. This version uses Picamera2 for the current camera stack and Pillow for comparison and JPEG writing. It saves the captured RGB frame with Pillow, so the JPEG quality setting is applied reliably by Image.save() rather than being passed as an uncertain keyword to Picamera2.capture_file().
#!/usr/bin/env python3
from datetime import datetime
from pathlib import Path
import time
from picamera2 import Picamera2
from PIL import Image, ImageChops, ImageStat
OUTPUT_DIR = Path.home() / "motion-images"
FRAME_SIZE = (640, 480)
FRAME_INTERVAL = 1.0
SETTLE_SECONDS = 3.0
MOTION_THRESHOLD = 12.0
PIXEL_CHANGE_THRESHOLD = 25
MIN_CHANGED_FRACTION = 0.01
COOLDOWN_SECONDS = 10.0
JPEG_QUALITY = 85
def motion_score(previous, current):
"""Return average grayscale difference and changed-pixel fraction."""
old_gray = previous.convert("L")
new_gray = current.convert("L")
difference = ImageChops.difference(old_gray, new_gray)
average_difference = ImageStat.Stat(difference).mean[0]
changed = difference.point(
lambda value: 255 if value >= PIXEL_CHANGE_THRESHOLD else 0
)
changed_pixels = sum(changed.getdata()) // 255
total_pixels = changed.width * changed.height
changed_fraction = changed_pixels / total_pixels
return average_difference, changed_fraction
def capture_frame(camera):
array = camera.capture_array("main")
return Image.fromarray(array).convert("RGB")
def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
camera = Picamera2()
configuration = camera.create_preview_configuration(
main={"size": FRAME_SIZE, "format": "RGB888"}
)
camera.configure(configuration)
camera.start()
try:
print(f"Allowing exposure to settle for {SETTLE_SECONDS} seconds")
time.sleep(SETTLE_SECONDS)
reference = capture_frame(camera)
last_trigger = 0.0
print("Motion detector running. Press Ctrl+C to stop.")
while True:
time.sleep(FRAME_INTERVAL)
current = capture_frame(camera)
average, changed_fraction = motion_score(reference, current)
print(
f"score={average:.2f}, "
f"changed={changed_fraction:.3%}"
)
now = time.monotonic()
triggered = (
average >= MOTION_THRESHOLD
and changed_fraction >= MIN_CHANGED_FRACTION
)
if triggered and now - last_trigger >= COOLDOWN_SECONDS:
filename = datetime.now().strftime("motion-%Y%m%d-%H%M%S-%f.jpg")
path = OUTPUT_DIR / filename
current.save(path, format="JPEG", quality=JPEG_QUALITY)
print(f"Motion detected; saved {path}")
last_trigger = now
reference = current
except KeyboardInterrupt:
print("Stopping motion detector")
finally:
camera.stop()
if __name__ == "__main__":
main()Important parts of the script
Picamera2initializes the camera using the current Raspberry Pi camera stack.FRAME_SIZEkeeps comparisons small enough for a Pi to process efficiently.motion_score()converts frames to grayscale, computes an image difference, and returns both average intensity and changed area.MOTION_THRESHOLDrejects weak overall changes.MIN_CHANGED_FRACTIONrejects tiny regions of noise.- The filename includes the date, time, and microseconds, preventing overwrites when multiple events occur close together.
COOLDOWN_SECONDSis a debounce period: it prevents one person or animal from generating a large burst of duplicate images.- The
finallyblock stops the camera during normal termination or a keyboard interrupt.
Run the detector
Run the source explicitly with Python 3:
python3 motion_detector.pyMove through the monitored scene after the settling period. The terminal prints the calculated score and changed-pixel fraction. Press Ctrl+C to stop safely, then inspect the files:
ls -lh ~/motion-imagesYou can optionally add executable permission and run the file directly:
chmod +x motion_detector.py
./motion_detector.pyDirect execution requires the shebang line at the top of the file. Executable permission is optional when using python3 motion_detector.py.
Saving and managing images
The example creates ~/motion-images and checks that it exists before starting. Saving under the running user's home directory normally avoids permission problems. An absolute path is clearer than relying on the terminal's current working directory.
JPEG quality 85 is a practical starting point. Higher quality produces larger files; lower quality saves storage but loses detail. Storage grows continuously, especially when the detector triggers frequently. Set a retention policy, such as deleting images older than a chosen number of days, rotating files, or moving selected images to protected storage. Make sure stored images have appropriate file permissions and are not published by an unauthenticated web or network service.
Calibration and reliability
Mount the camera securely and keep the scene reasonably fixed. Test under the actual lighting conditions where the detector will run. Shadows, clouds, daylight changes, automatic exposure, foliage, and camera noise can all create false positives. Establish the baseline only after exposure has settled. For calibration, record scores for an unchanged scene and for deliberate movement, then choose a threshold between the typical no-motion and motion values.
For a doorway, establish an empty-room baseline and walk through once. For wildlife observation, aim at a feeder, use a moderate interval, and increase the minimum changed area if plant movement causes constant triggers. To reduce daylight-related triggers, use small grayscale frames, require more changed area, and add a cooldown.
Common errors and fixes
Exam-relevant summary
- Motion detection means identifying meaningful visual changes between frames, not detecting infrared radiation with a PIR sensor.
- A reference frame is the earlier image used for comparison.
- An image difference describes pixel changes between two frames.
- A threshold is the required level of change before the program triggers.
- A false positive is an unwanted trigger caused by noise, light, shadows, or other non-target changes.
- A cooldown prevents repeated captures from one continuing event.
chmodchanges Linux file permissions, including executable permission.- Use the modern libcamera/Picamera2 environment for new Raspberry Pi camera projects unless an older installation specifically requires legacy Picamera.