Level up your TinyML skills

The Definitive Guide To Esp32-cam Motion Detection

The world-best guide on motion detection for Esp32-cam without external PIR sensor. Everything you need to know, from A to Z

Esp32-cam Motion Detection

Motion detection on Esp32-cam without external PIR sensor has always been the most popular topic on this website.

I put much efforts over the months to refine this guide, but it never felt good enough.

So I kept working. And now I feel satisfied.

Now I feel I wrote the best motion detection software for Esp32-cam of the world.

It may seem a bold statement, but I'm 100% sure you will agree by the end of this guide. If not, I invite you to share the following shame tweet.

This guide is pretty long and detailed. Take the time it deserves to read it all: don't skim through.

What is motion detection without PIR

Motion detection refers to the task of detecting when something in your region of interest is moving. Many times you are interested in human movement, but you can actually want to detect:

  • a car passing in from of your window
  • a bird reaching your bird-house
  • detect if your cat jumps over the sofa while you're off

Most tutorials on the web focus on human detection, so they equip the Esp32 with an external infrared sensor (a.k.a PIR, the one you find in home alarm systems) and take a photo when the PIR detects something.

If this setup works fine for you, go with it. Really. It's easy, fast and pretty accurate.

But the PIR approach has a few drawbacks:

  1. you can only detect living beings: since it is based on infrared sensing, it can only detect when something "hot" is in its field of view (humans and animals, basically). If you want to detect a car passing, it won't work
  2. it has a limited range: PIR sensors reach at most 15-20 meters. If you need to detect people walking on the street in front of your house at 30 meters, it won't work
  3. it needs a clear line-of-sight: to detect infrared light, the PIR sensor needs no obstacle in-between itself and the moving object. If you put it behind a window to detect people outside your home, it won't work
  4. it triggers even when no motion happened: the PIR tecnique is actually a proxy for motion detection. The PIR sensor doesn't actually detects motion: it detects the presence of warm objects. For example, if a person comes into a room and lies down on the sofa, the PIR sensor will trigger for as long as the person doesn't leave the room

On the other hand, video motion detection can fulfill all the above cases because it performs motion detection on the camera frames, comparing each one with the previous looking for differences.

If a large portion of the image changed, it triggers.

Video motion detection has its drawbacks, nonetheless:

  1. power-hungry: comparing each frame with the previous means the camera must be always on. While with the PIR sensor you can put the camera to sleep, now you have to continuosly check each frame
  2. insensitive to slow changes: to avoid false triggers, you will set a lower threshold on the image portion that need to change to detect motion (e.g. 10% of the frame). If something is moving slowly in your field of view such that it changes less than 10% of the frame, the algorithm will not pick it up.

Take some time to review the pros and cons of video motion detection now that you have a little more details.

If you're convinced it will work for you, let's start looking at the implementation code.

Harware requirements

To implement motion detection on the Esp32-cam, you will only need, well, an Esp32-cam.

There are many models available on the market that will work fine:

Software requirements

This guide uses the Arduino IDE and complies with Arduino programming style. If you're using ESP-IDF, you may need to tweak a few lines of code (not covered in this guide).

Once you have the Arduino IDE installed, you need to install the EloquentSurveillance library from the Library Manager.

Install the EloquentSurveillance library from the Arduino Library Manager

Now that everything is setup, let's start the fun part.

Use Case 1: Capture frames

Even though the EloquentSurveillance library primary focus is on motion detection, you can use it at the most basic level to just grab pictures from your Esp32-cam with ease.

The project below does exactly this: it takes pictures and prints how much space they take in memory.

 // turn on debug messages
#define VERBOSE
#include "EloquentSurveillance.h"


/**
 *
 */
void setup() {
    Serial.begin(115200);
    delay(3000);

    // debug(LEVEl, message)
    debug("INFO", "Init");

    /**
     * Configure camera model
     * You have access to the global variable `camera`
     * Allowed values are:
     *  - aithinker()
     *  - m5()
     *  - m5wide()
     *  - eye()
     *  - wrover()
     */
    camera.m5wide();

    /**
     * Configure camera resolution
     * Allowed values are:
     *  - qqvga()
     *  - qvga()
     *  - vga()
     */
    camera.qvga();

    /**
     * Configure JPEG quality
     * Allowed values are:
     *  - lowQuality()
     *  - highQuality()
     *  - bestQuality()
     *  - setQuality(quality), ranging from 10 (best) to 64 (lowest)
     */
    camera.highQuality();

    /**
     * Initialize the camera
     * If something goes wrong, print the error message
     */
    while (!camera.begin())
        debug("ERROR", camera.getErrorMessage());

    debug("SUCCESS", "Camera OK");
}

/**
 *
 */
void loop() {
    /**
     * Try to capture a frame
     * If something goes wrong, print the error message
     */
    if (!camera.capture()) {
        debug("ERROR", camera.getErrorMessage());
        return;
    }

    debug("SUCCCESS", camera.getFileSize());

    /**
     * Do whatever you want with the captured frame.
     * Access it with `camera.getBuffer()` (it contains the JPEG frame as uint8_t*)
     */
}

 

If you read other tutorials on the web about how to capture a frame from an Esp32-cam, you know they're 10x longer and very much harder to follow.

With the EloquentSurveillance library, you can unleash the power of Esp32-cam with only a few lines of code instead.

Use Case 2: Streaming Web Server

Taking photo is a meaningless if you can't see them.

The Esp32-cam is able to start a streaming web server that gives you access to the realtime video feed from the camera.

It needs to connect to your home WiFi network, so be sure you have one.

 // turn on debug messages
#define VERBOSE
#include "EloquentSurveillance.h"

/**
 * Replace with your WiFi credentials
 */
#define WIFI_SSID "Abc"
#define WIFI_PASS "12345678"


/**
 * 80 is the port to listen to
 * You can change it to whatever you want, 80 is the default for HTTP
 */
EloquentSurveillance::StreamServer streamServer(80);


void setup() {
    Serial.begin(115200);
    delay(3000);
    debug("INFO", "Init");

    /**
     * See CameraCaptureExample for more details
     */
    camera.m5wide();
    camera.highQuality();
    camera.qvga();

    /**
     * Initialize the camera
     * If something goes wrong, print the error message
     */
    while (!camera.begin())
        debug("ERROR", camera.getErrorMessage());

    /**
     * Connect to WiFi
     * If something goes wrong, print the error message
     */
    while (!wifi.connect(WIFI_SSID, WIFI_PASS))
        debug("ERROR", wifi.getErrorMessage());

    /**
     * Initialize stream web server
     * If something goes wrong, print the error message
     */
    while (!streamServer.begin())
        debug("ERROR", streamServer.getErrorMessage());

    /**
     * Display address of stream server
     */
    debug("SUCCESS", streamServer.getWelcomeMessage());
}


void loop() {
}

 

Flash the above sketch to your board and open the Serial Monitor. You will read a message like the following:

StreamServer listening at http://192.168.1.100.
MJPEG stream is available at http://192.168.1.100/stream

The actual IP address will be different.

The important part is that you can copy the MJPEG stream address and open it in your browser. You will be able to see the realtime video feed.

Use Case 3: Motion Detection

Well, motion detection is the reason you're reading this post, right?

 // turn on debug messages
#define VERBOSE
#include "EloquentSurveillance.h"


/**
 * Instantiate motion detector
 */
EloquentSurveillance::Motion motion;


/**
 *
 */
void setup() {
    Serial.begin(115200);
    delay(3000);
    debug("INFO", "Init");

    /**
     * See CameraCaptureExample for more details
     */
    camera.m5wide();
    camera.qvga();
    camera.highQuality();

    /**
     * Configure motion detection
     *
     * > setMinChanges() accepts a number from 0 to 1 (percent) or an integer
     *   At least the given number of pixels must change from one frame to the next
     *   to trigger the motion.
     *   The following line translates to "Trigger motion if at least 10% of the pixels
     *   in the image changed value"
     */
    motion.setMinChanges(0.1);

    /**
     * > setMinPixelDiff() accepts an integer
     *   Each pixel value must differ at least of the given amount from one frame to the next
     *   to be considered as different.
     *   The following line translates to "Consider a pixel as changed if its value increased
     *   or decreased by 10 (out of 255)"
     */
    motion.setMinPixelDiff(10);

    /**
     * > setMinSizeDiff() accepts a number from 0 to 1 (percent) or an integer
     *   To speed up the detection, you can exit early if the image size is almost the same.
     *   This is an heuristic that says: "If two consecutive frames have a similar size, they
     *   probably have the same contents". This is by no means guaranteed, but can dramatically
     *   reduce the computation cost.
     *   The following line translates to "Check for motion if the filesize of the current image
     *   differs by more than 5% from the previous".
     *
     *   If you don't feel like this heuristic works for you, delete this line.
     */
    motion.setMinSizeDiff(0.05);

    /**
     * Initialize the camera
     * If something goes wrong, print the error message
     */
    while (!camera.begin())
        debug("ERROR", camera.getErrorMessage());

    debug("SUCCESS", "Camera OK");
}

/**
 *
 */
void loop() {
    /**
     * Try to capture a frame
     * If something goes wrong, print the error message
     */
    if (!camera.capture()) {
        debug("ERROR", camera.getErrorMessage());
        return;
    }

    /**
     * Look for motion.
     * In the `true` branch, you can handle a motion event.
     * For the moment, just print the processing time for motion detection.
     */
    if (motion.update()) {
        debug("INFO", String("Motion detected in ") + motion.getExecutionTimeInMicros() + " us");
        delay(5000);
    }
    else if (!motion.isOk()) {
        /**
         * Something went wrong.
         * This part requires proper handling if you want to integrate it in your project
         * because you can reach this point for a number of reason.
         * For the moment, just print the error message
         */
        debug("ERROR", motion.getErrorMessage());
    }
}

 

Upload the sketch and open the Serial Monitor.

If you didn't deleted the motion.setMinSizeDiff(0.05) line, you will see many lines like the following:

old size = 4371, new size = 4294, thresh = 214, diff = 77

The actual values depend on your camera configuration (size and quality), but they report:

  • the size of the previous jpeg frame
  • the size of the current jpeg frame
  • the minimum difference needed to perform motion detection on the pixels
  • the actual difference

As long as diff is lower than thresh, the motion detection algorithm will not even run and always report false.

If you move in front of the camera, you will read something like the following:

old size = 4350, new size = 5120, thresh = 256, diff = 770
num changes = 898, threshold = 120
[INFO] Motion detected in 26231 us

This time, diff is greater than thresh, so the algorithm runs. The next line tells you how many pixels changed their value and the minimum threshold required to trigger motion.

Since num changes is greater than threshold, the motion detection returns true and you see the debug message.

Use case 3.5: Debounced Motion Detection

If you look carefully at the Serial Monitor, you may sometimes see a log like the following:

old size = 4350, new size = 5120, thresh = 256, diff = 770
num changes = 898, threshold = 120
[INFO] Motion detected in 26231 us
old size = 4350, new size = 5120, thresh = 256, diff = 770
num changes = 898, threshold = 120
[INFO] Motion detected in 26231 us

That is, two consecutive triggers in a very short time.

It happens when the motion is longer than the delay we introduced in code.

Most often, you want to await a few seconds before a new motion event is detected, otherwise you may get many triggers from a single event.

In this case, you only need a single line of code.

motion.setMinChanges(0.1);
motion.setMinPixelDiff(10);
motion.setMinSizeDiff(0.05);

/**
 * Turn on debouncing.
 * It accepts the number of milliseconds between two consecutive events.
 * The following line translates to "Don't trigger a new motion event if
 * 10 seconds didn't elapsed from the previous"
 */
motion.debounce(10000L);

Use case 4: Save Picture of Motion

Two of the most common operations that users want to perform when motion is detected are sending a notification and saving the frame to the disk or SD card.

In this paragraph we'll look at the latter. In the next one, we'll look at the notification part.

The next example saves the captures to the internal SPIFFS filesystem, so you don't need any external hardware. If you want to save to SD card, replace the references to SPIFFS with the SD class.

 // turn on debug messages
#define VERBOSE
#include <SPIFFS.h>
#include "EloquentSurveillance.h"


/**
 * Instantiate motion detector
 */
EloquentSurveillance::Motion motion;


/**
 *
 */
void setup() {
    Serial.begin(115200);
    delay(3000);
    debug("INFO", "Init");

    /**
     * See CameraCaptureExample for more details
     */
    camera.m5wide();
    camera.qvga();
    camera.highQuality();

    /**
     * See MotionDetectionExample for more details
     */
    motion.setMinChanges(0.1);
    motion.setMinPixelDiff(10);
    motion.setMinSizeDiff(0.05);
    motion.debounce(10000L);

    /**
     * Initialize the camera
     * If something goes wrong, print the error message
     */
    while (!camera.begin())
        debug("ERROR", camera.getErrorMessage());

    /**
     * Initialize the filesystem
     * If something goes wrong, print an error message
     */
    while (!SPIFFS.begin(true))
        debug("ERROR", "Cannot init SPIFFS");

    debug("SUCCESS", "Camera OK");
}

/**
 *
 */
void loop() {
    /**
     * Try to capture a frame
     * If something goes wrong, print the error message
     */
    if (!camera.capture()) {
        debug("ERROR", camera.getErrorMessage());
        return;
    }

    /**
     * Look for motion.
     * In the `true` branch, you can handle a motion event.
     * In this case, we save the frame to disk
     */
    if (motion.update()) {
        /**
         * You can construct the filename as you prefer
         *  - motion.getCount() will return an incremental number *for the current run*
         *  - motion.getPersistenCount() will return an incremental number *that persists reboots*
         *  - motion.getNextFilename(String prefix) will construct a filename using the given prefix
         */
        //String filename = String("/capture_") + motion.getCount() + ".jpg";
        //String filename = String("/capture_") + motion.getPersistentCount() + ".jpg";
        String filename = motion.getNextFilename("/capture_");

        if (camera.saveTo(SPIFFS, filename)) {
            debug("SUCCESS", "Frame saved to disk");
            debug("SUCCESS", filename);
        }
        else {
            debug("ERROR", camera.getErrorMessage());
            debug("ERROR", filename);
        }

        delay(5000);
    }
}

 

This is pretty much the same as the MotionDetectionExample, with a few lines added.

This will save the frames to the SPIFFS filesystem with names like capture_1.jpg, capture_2.jpg and so on. The counter will survive even across reboots!

You can get fancy here and add an RTC (Real Time Clock) to the project to keep the correct timing. Or, if you have WiFi enabled, you can connect to an NTP (Network Time Protocol) server to keep the time synced.

If you don't know how to do this on your own, you can subscribe to the "Become a Motion Detection Pro" newsletter below and I'll show you how to do it.

Use Case 5: Motion File Server

Once you collect a few pictures (or while you're still collecting them, depending on your use case), you will eventually want to see those pictures.

If you're saving them to an external SD card, you would take it and view the files on your PC.

If using the SPIFFS filesystem (or if you don't want to remove the SD card each time), it would be much better if you had a nice GUI (Graphical User Interface) for file browsing.

EloquentSurveillance provides such a GUI by means of a Web File Server.

A File Server, similar to the Streaming Server of Use Case 2, is accessible over the network at the Esp32-cam IP address and displays you a webpage. In this case, instead of the live video feed, you'll find a table listing all the images available on the filesystem, with a link to open them.

Here is the sketch.

 #include <SPIFFS.h>
#include "EloquentSurveillance.h"


#define WIFI_SSID "Abc"
#define WIFI_PASS "12345678"


EloquentSurveillance::Motion motion;
/**
 * 80 is the port to listen to
 * You can change it to whatever you want, 80 is the default for HTTP
 */
EloquentSurveillance::FileServer fileServer(80);


/**
 *
 */
void setup() {
    Serial.begin(115200);
    delay(3000);
    debug("INFO", "Init");

    /**
     * See CameraCaptureExample for more details
     */
    camera.m5wide();
    camera.vga();
    camera.highQuality();

    /**
     * See MotionDetectionExample for more details
     */
    motion.setMinChanges(0.1);
    motion.setMinPixelDiff(8);
    motion.setMinSizeDiff(0.03);
    motion.debounce(10000L);

    /**
     * Initialize the camera
     * If something goes wrong, print the error message
     */
    while (!camera.begin())
        debug("ERROR", camera.getErrorMessage());

    /**
     * Connect to WiFi
     * If something goes wrong, print the error message
     */
    while (!wifi.connect(WIFI_SSID, WIFI_PASS))
        debug("ERROR", wifi.getErrorMessage());

    /**
     * Initialize the filesystem
     * If something goes wrong, print an error message
     */
    while (!SPIFFS.begin(true))
        debug("ERROR", "Cannot init SPIFFS");

    /**
     * Initialize the file server
     * If something goes wrong, print an error message
     */
    while (!fileServer.begin())
        debug("ERROR", fileServer.getErrorMessage());

    /**
     * Display address of file server
     */
    debug("SUCCESS", fileServer.getWelcomeMessage());
}

/**
 *
 */
void loop() {
    /**
     * Handle file server requests, if any
     */
    fileServer.handle();

    /**
     * Try to capture a frame
     * If something goes wrong, print the error message
     */
    if (!camera.capture()) {
        debug("ERROR", camera.getErrorMessage());
        return;
    }

    /**
     * Look for motion.
     * In the `true` branch, you can handle a motion event.
     * For this example, save the image to SPIFFS and print a debug message
     */
    if (motion.update()) {
        String filename = motion.getNextFilename();

        if (camera.saveTo(SPIFFS, filename)) {
            debug("INFO", String("Frame saved to SPIFFS: ") + filename);
            debug("INFO", "Refresh the file server webpage to see the new file");
        }
    }
}

 

Again, open the Serial Monitor to read the File Server address.

FileServer listening at http://192.168.1.100

And this is a screenshot of the GUI.

Esp32-cam File Server example

Now you don't have to disconnect the board from power to extract the SD card or load a dedicated sketch to see the captures: you can watch them in realtime as soon as they get captured!

Use Case 6: Telegram Notifications

Once your Esp32-cam detects motion, you may want to get some kind of notification.

Notifications come in many shapes:

  • a sound alert
  • a Telegram message
  • an Email
  • an SMS
  • a Webhook

One of the most popular is the Telegram notification, since it delivers directly to your phone in realtime.

EloquentSurveillance makes integrating Telegram into your project a breeze.

 #define VERBOSE

#include "EloquentSurveillance.h"
#include "TelegramChat.h"

#define WIFI_SSID "Abc"
#define WIFI_PASS "12345678"
#define BOT_TOKEN "XXXXXXXXXX:yyyyyyyyyyyyyyyyyyyyyyyy-zzzzzzzzzz"
#define CHAT_ID 1234567890


EloquentSurveillance::Motion motion;
/**
 * Instantiate a Telegram chat to send messages and photos
 */
EloquentSurveillance::TelegramChat chat(BOT_TOKEN, CHAT_ID);


/**
 *
 */
void setup() {
    Serial.begin(115200);
    delay(3000);
    debug("INFO", "Init");

    /**
     * See CameraCaptureExample for more details
     */
    camera.m5wide();
    camera.vga();
    camera.highQuality();

    /**
     * See MotionDetectionExample for more details
     */
    motion.setMinChanges(0.05);
    motion.setMinPixelDiff(7);
    motion.setMinSizeDiff(100);
    motion.debounce(10000L);

    /**
     * Initialize the camera
     * If something goes wrong, print the error message
     */
    while (!camera.begin())
        debug("ERROR", camera.getErrorMessage());

    /**
     * Connect to WiFi
     * If something goes wrong, print the error message
     */
    while (!wifi.connect(WIFI_SSID, WIFI_PASS))
        debug("ERROR", wifi.getErrorMessage());

    debug("SUCCESS", "Camera OK, WiFi connected");
}

/**
 *
 */
void loop() {
    /**
     * Try to capture a frame
     * If something goes wrong, print the error message
     */
    if (!camera.capture()) {
        debug("ERROR", camera.getErrorMessage());
        return;
    }

    /**
     * Look for motion.
     * In the `true` branch, you can handle a motion event.
     * In this case, we send the photo to Telegram
     */
    if (motion.update()) {
        debug("INFO", String("Motion detected in ") + motion.getExecutionTimeInMicros() + " us");

        bool messageResponse = chat.sendMessage("Motion detected");
        debug("TELEGRAM MSG", messageResponse ? "OK" : "ERR");

        /**
         * @bug: this always returns false, even on success
         */
        bool photoResponse = chat.sendPhoto();
        debug("TELEGRAM PHOTO", photoResponse ? "OK" : "ERR");
    }
}
 

You have to first create your own Telegram bot (see later).

Then you need your own CHAT_ID. You can get your very own chat id messaging the myidbot bot from your Telegram.

Telegram @myidbot    screen

Open up your Serial Monitor, move your hand in front of the camera, and await a couple seconds to get the notification on your phone!


Wow, this has been pretty detailed, right?

If you read so far, you have all the tools you need to create your own motion detection project with a lot of basic stuff.

Now the hard decision.

If this post was a waste of your time, please go to the top of the page and tweet the shame tweet.

But...

If this was the best post you've ever read about motion detection on Esp32-cam, please tweet the following.

Advanced Use Cases

Beyond the basics I highlighted in this post, there are advanced topics related to motion detection that may interest you:

  • region-based motion detection: what if you want to perform motion detection only on a portion of the image, not the entire frame? No one on the web ever talked about it. This is possible with the EloquentSurveillance package (and pretty easy too!). You can even check for motion in many different regions.
  • create an animated GIF from motion frames: capturing a frame is a thing, getting an animation is a completely different one. It will supercharge your projects!
  • email notifications: Telegram is only one notification channel among the many available. What if you would like to send an email when motion is detected? We got you covered!
  • Zapier integration: Zapier is an automation platform to connect many web services without writing code. You can tweet, write to Google Sheets, send an email, save to Dropbox, add to Google Calendar and much more when motion is detected. Without writing a single line of code!

If this sounds appeaaling to you, leave your email below and I'll send you the detailed instructions on how to implement all of the above.

Become a Motion Detection Pro

Level up your motion detection skills and toolset with a lot of advanced features. You will receive all the code examples in your inobx. No spam.

We use Mailchimp as our marketing platform. By submitting this form, you acknowledge that the information you provided will be transferred to Mailchimp for processing in accordance with their terms of use. We will use your email to send you updates relevant to this website.

Appendix: How to create a Telegram Bot

If you want to use your own Telegram Bot to get motion notifications, you need to create one.

Creating a Telegram bot is a straightforward process: there's a bot to do it!

Search for "BotFather" in your Telegram account and type /newbot, then follow the instructions. You will have something similar to the below screenshot.

Telegram bot creation process

Grab the token and paste it into the sketch near the TELEGRAM_TOKEN define.

Get monthly updates

Do not miss the next posts on TinyML and Esp32 camera. No spam, I promise

We use Mailchimp as our marketing platform. By submitting this form, you acknowledge that the information you provided will be transferred to Mailchimp for processing in accordance with their terms of use. We will use your email to send you updates relevant to this website.

Having troubles? Ask a question

Need a one to one call? Book now!

© Copyright 2022 Eloquent Arduino. All Rights Reserved.