Ship your TinyML product faster

The Ultimate Guide to Arduino and ESP32 Camera Motion Detection

Detect motion in realtime from OV7670 and ESP32 camera video streams without a PIR nor external hardware

OV767x and ESP32 cameras are really cheap today (as low as 5$), but they turn out to be of great value in adding simple Computer Vision to your projects.

Many times you need the ability to take a photo capture of a scene when something happens. It can be

  • a person walking in your house garden
  • your pet jumping on the sofa
  • a car passing along the street

In all the above cases, relying on a PIR sensor won't do the job, since your camera will be a few meters away from the target.

What you want is react to motion in your video stream by only analyzing the images.

The action you take once motion is detected depends on your specific needs. For the sake of this project, we'll send the captured photo to your Telegram account.

Thanks to the Eloquent Arduino libraries, you will succeed at this task in no time!

How does motion detection works

There exists many motion detection algorithms in literature, with varying degrees of accuracy, speed and required resources, based on the application field.

The core idea still holds the same for all of them:

  • we model the background of the scene
  • we detect the foreground at each frame
  • if the foreground changed substantially, we call it motion

Not all models are suited to run on resource-contrained devices like Arduino and ESP32 microcontrollers, so we picked only the ones that can guarantee an acceptable speed for realtime execution.

Prepare your environment

There is a single strict requirement to follow this project: an Arduino-compatible camera and a basic knowledge on how to use the Arduino environment (load a sketch, install a library).

You can find several ESP32 boards with an embedded camera on the market: they all work fine and give you good results.

Part List

OV767x
OV767x
x 1
M5Stack camera
M5Stack camera
x 1
AiThinker camera
AiThinker camera
x 1
Esp32 cam
Esp32 cam
x 1
Before continuing, please install the EloquentArduino library version 2.1.1 or above in the Arduino IDE. Install EloquentArduino library version 2.1.1 in Arduino IDE

Test your camera

Before we start implementing Arduino and ESP32 cam motion detection, you need to make sure your camera is working.

Upload the following sketch to your board and open the Serial Monitor: if you see the Camera init OK message and many more lines thereafter, you're good to go.

If not, don't hesitate to drop us a line with the form below and we'll help you.

 /**
 * Camera capture example
 */

#include "eloquent.h"

// uncomment based on your camera and resolution

//#include "eloquent/vision/camera/ov767x/gray/vga.h"
//#include "eloquent/vision/camera/ov767x/gray/qvga.h"
//#include "eloquent/vision/camera/ov767x/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/vga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/vga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/vga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/vga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/vga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/qqvga.h"


void setup() {
    delay(4000);
    Serial.begin(115200);

    // turn on high frequency capturing
    // in the case of OV767x camera, highFreq = 5 fps instead of 1 fps
    // in the case of Esp32 camera, highFreq clock = 20 MHz instead of 10 MHz
    camera.setHighFreq();

    if (!camera.begin())
        eloquent::abort(Serial, "Camera init error");

    Serial.println("Camera init OK");
}

void loop() {
    if (!camera.capture()) {
        Serial.println(camera.getErrorMessage());
        delay(1000);
        return;
    }

    // resize for faster transmission over Serial port
    camera.image.resize<40, 30>();
    camera.image.printAsJsonTo(Serial);

    // release memory
    camera.free();
}
 

Open this page in a desktop environment to run the live demo with the video stream from your own camera.

Realtime Motion Detection without PIR

Now that the camera is capturing frames, we need to detect change from one frame to the next one.

We will start with a naive motion detector that compares the current frame with the previous: if they differ in many pixels, it triggers the motion event.

To perform this operation in the fastest way possible, we can't afford to compare the pixels of the whole image, though.

A common resolution for the cameras we're using is 320x240 (QVGA). This resolution totals to 76800 pixels that need to be compared: it will take too much time and won't even add much accuracy.

This is why we resize the frame to a more manageable size and perform motion detection on the thumbnail version.

 /**
 * Camera motion detection demo
 */

#include "eloquent.h"
#include "eloquent/vision/motion/naive.h"


// uncomment based on your camera and resolution

//#include "eloquent/vision/camera/ov767x/gray/vga.h"
//#include "eloquent/vision/camera/ov767x/gray/qvga.h"
//#include "eloquent/vision/camera/ov767x/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/vga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/vga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/vga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/vga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/vga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/qqvga.h"


#define THUMB_WIDTH 32
#define THUMB_HEIGHT 24


Eloquent::Vision::Motion::Naive<THUMB_WIDTH, THUMB_HEIGHT> detector;


void setup() {
    delay(4000);
    Serial.begin(115200);

    // turn on high freq for fast streaming speed
    camera.setHighFreq();

    if (!camera.begin())
        eloquent::abort(Serial, "Camera init error");

    Serial.println("Camera init OK");

    // wait for at least 10 frames to be processed before starting to detect
    // motion (false triggers at start)
    // then, when motion is detected, don't trigger for the next 10 frames
    detector.startSinceFrameNumber(10);
    detector.debounceMotionTriggerEvery(10);

    // or, in one call
    detector.throttle(10);

    // trigger motion when at least 10% of pixels change intensity by
    // at least 15 out of 255
    detector.setPixelChangesThreshold(0.1);
    detector.setIntensityChangeThreshold(15);
}


void loop() {
    if (!camera.capture()) {
        Serial.println(camera.getErrorMessage());
        delay(1000);
        return;
    }

    // perform motion detection on resized image for fast detection
    camera.image.resize<THUMB_WIDTH, THUMB_HEIGHT>();
    camera.image.printAsJsonTo(Serial);
    detector.update(camera.image);

    // if motion is detected, print coordinates to serial in JSON format
    if (detector.isMotionDetected()) {
        detector.printAsJsonTo(Serial);
    }

    // release memory
    camera.free();
}
 

Open this page in a desktop environment to run the live demo with the video stream from your own camera.

While on mobile, please take a look at the recorded demo.

This is it for the most basic Arduino and ESP32 cam motion detection in realtime.

Pretty easy, right?

Continue reading on to implement photo capture when motion happens.

Capture photo when motion happens

If you're implementing some kind of video surveillance system , you need to save a snapshot when the camera detects that motion is happening.

We'll hook into the previous sketch and add the saving part.

 /**
 * Camera motion detection + JPEG capture demo
 */

#include "eloquent.h"
#include "eloquent/vision/image/gray/custom.h"
#include "eloquent/vision/motion/naive.h"
#include "eloquent/fs/spiffs.h"


// uncomment based on your camera and resolution

//#include "eloquent/vision/camera/ov767x/gray/vga.h"
//#include "eloquent/vision/camera/ov767x/gray/qvga.h"
//#include "eloquent/vision/camera/ov767x/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/vga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/vga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/vga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/vga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/vga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/qqvga.h"


#define THUMB_WIDTH 32
#define THUMB_HEIGHT 24

// allocate memory to store the thumbnail into a new image
Eloquent::Vision::Image::Gray::CustomWithBuffer<THUMB_WIDTH, THUMB_HEIGHT> thumbnail;
Eloquent::Vision::Motion::Naive<THUMB_WIDTH, THUMB_HEIGHT> detector;


void setup() {
    delay(4000);
    Serial.begin(115200);
    filesystem.begin(true);

    // turn on high freq for fast streaming speed
    camera.setHighFreq();

    if (!camera.begin())
        eloquent::abort(Serial, "Camera init error");

    Serial.println("Camera init OK");

    detector.throttle(10);
    detector.setIntensityChangeThreshold(15);
    detector.setPixelChangesThreshold(0.1);
}


void loop() {
    if (!camera.capture()) {
        Serial.println(camera.getErrorMessage());
        delay(1000);
        return;
    }

    // perform motion detection on resized image for fast detection
    // but keep original image for capture at full resolution
    camera.image.resizeTo<THUMB_WIDTH, THUMB_HEIGHT>(thumbnail);
    detector.update(thumbnail);

    // if motion is detected, save original image as jpeg
    if (detector.isMotionDetected()) {
        auto file = filesystem.writeBinary("/capture_best.jpg");
        auto jpeg = camera.image.jpeg().bestQuality();

        if (jpeg.writeTo(file)) {
            Serial.print("Best quality jpeg file size in bytes: ");
            Serial.print(file.size());
            Serial.print(". It took ");
            Serial.print(jpeg.getElapsedTime());
            Serial.println(" micros to encode");
        }
        else {
            Serial.print("Jpeg encoding error: ");
            Serial.println(jpeg.getErrorMessage());
        }
    }

    // release memory
    camera.free();
}
 

As you can see, it only requires a few lines of code to save an image to the given filesystem with on-board Jpeg encoding.

Jpeg encoding benchmark

Well, it depends on the board CPU, the size of the image and the jpeg quality. Next are reported a few figures we obtained on a M5Stack camera.

Image resolution Quality File size Time in millis
VGA 640x480low14 - 15 Kb2326 - 2529
high36 - 40 Kb4795 - 12553
best77 - 79 Kb8732 - 12054
QVGA 320x240low3 - 4 Kb187 - 379
high5 - 7 Kb339 - 477
best12 - 13 Kb677 - 2221
QQVGA 160x120low1 - 2 Kb24 - 153
high2 - 3 Kb163 - 290
best4 - 5 Kb178 - 314

Now that you captured the photo as jpeg, would it be nice if you could receive the captured photo via Telegram ?

Send captured photo to Telegram

It would be great if you could get a notification once motion is detected by the camera, wouldn't it?

Among the many channels you can be notified via, Telegram is one of the most popular because:

  1. you have the app on your smartphone, so you will see it soon
  2. it is easy to integrate into Arduino projects

Sadly, many tutorials on the web make it cumbersome to integrate this part into motion detection sketches. The EloquentArduino library, on the other hand, makes it a breeze .

Before you can send a notification to your Telegram account, you will need to create a Telegram bot (see below for instructions) and get a TELEGRAM_TOKEN and CHAT_ID.

Once you're done, the rest is a piece of cake.

You need to install the ArduinoJson library from the Library Manager first!

 /**
 * Camera motion detection + JPEG capture + Telegram Bot demo
 */

// replace with your own values
#define TELEGRAM_TOKEN "xxxxxxxxxx:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
#define CHAT_ID "0123456789"
#define WIFI_SSID "SSID"
#define WIFI_PASSWORD "PASSWORD"

#include <WiFi.h>
#include <eloquent.h>
#include <eloquent/vision/image/gray/custom.h>
#include <eloquent/vision/motion/naive.h>
#include <eloquent/fs/spiffs.h>
#include <eloquent/networking/wifi.h>
#include <eloquent/apps/telegram/bot/wifi.h>


// uncomment based on your camera and resolution

//#include "eloquent/vision/camera/ov767x/gray/vga.h"
//#include "eloquent/vision/camera/ov767x/gray/qvga.h"
//#include "eloquent/vision/camera/ov767x/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/vga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/aithinker/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/vga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/wrover/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/vga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/eye/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/vga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/m5/gray/qqvga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/vga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/qvga.h"
//#include "eloquent/vision/camera/esp32/m5wide/gray/qqvga.h"


#define THUMB_WIDTH 32
#define THUMB_HEIGHT 24

// allocate memory to store the thumbnail into a new image
Eloquent::Vision::Image::Gray::CustomWithBuffer<THUMB_WIDTH, THUMB_HEIGHT> thumbnail;
Eloquent::Vision::Motion::Naive<THUMB_WIDTH, THUMB_HEIGHT> detector;


void setup() {
    delay(4000);
    Serial.begin(115200);
    filesystem.begin(true);

    // turn on high freq for fast streaming speed
    camera.setHighFreq();

    if (!camera.begin())
        eloquent::abort(Serial, "Camera init error");

    if (!wifi.connectTo(WIFI_SSID, WIFI_PASSWORD))
        eloquent::abort(Serial, "Cannot connect to WiFi");

    if (!telegramBot.connect()) {
        eloquent::abort(Serial, "Cannot connect to Telegram API");
    }

    Serial.println("Camera init OK");
    Serial.println("WiFi connected OK");
    Serial.println("Telegram API connected OK");

    detector.throttle(10);
    detector.setIntensityChangeThreshold(15);
    detector.setPixelChangesThreshold(0.1);
}


void loop() {
    if (!camera.capture()) {
        Serial.println(camera.getErrorMessage());
        delay(1000);
        return;
    }

    // perform motion detection on resized image for fast detection
    // but keep original image for capture at full resolution
    camera.image.resizeTo<THUMB_WIDTH, THUMB_HEIGHT>(thumbnail);
    detector.update(thumbnail);

    // if motion is detected, save original image as jpeg
    if (detector.isMotionDetected()) {
        Serial.println("Motion detected");

        auto file = filesystem.writeBinary("/capture_best.jpg");
        auto jpeg = camera.image.jpeg().bestQuality();

        if (jpeg.writeTo(file)) {
            Serial.println("Jpeg written");

            // send to Telegram
            file.reopenAs("rb");

            bool messageOk = telegramBot.sendMessageTo(CHAT_ID, "Motion detected");
            Serial.println(messageOk ? "Message sent OK" : "Message send error");

            bool jpegOk = telegramBot.sendJpegTo(CHAT_ID, file);
            Serial.println(jpegOk ? "Jpeg sent OK" : "Jpeg send error");

            if (!jpegOk) {
                Serial.print("Telegram error: ");
                Serial.println(telegramBot.getErrorMessage());
            }
        }
        else {
            Serial.print("Jpeg encoding error: ");
            Serial.println(jpeg.getErrorMessage());
        }
    }

    // release memory
    camera.free();
}
 

As you can see from the above code, once you captured the Jpeg image, it only takes 1 line of code to upload that image to Telegram.

And of course you can reuse that same line to send any Jpeg image.

How to Create a Telegram Bot

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
Telegram bot creation process

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

How to Get the Chat ID

In Telegram every account has an ID. To send messages from your bot to your account you will need this ID.

Again, there's a bot for this called myidbot.

Telegram @getmyid    bot
Telegram @getmyid bot

Grap the ID and paste it into the sketch near the CHAT_ID define.

Live demo

Now that we setup everything, let's see how it performs in the real world.

Below is a video recording of me triggering motion events in front of the camera. The video has been edited for speed: while the text notification is almost instantaneous, it takes about 10-15 seconds to upload the image.

Esp32-cam motion detection + Telegram integration demo
Esp32-cam motion detection + Telegram integration demo

The delay between the motion trigger and you receiving the image on your smartphone depends on the resolution and the quality of the jpeg.


That's all for realtime ESP32-cam and OV767x motion detection without PIR sensor nor external hardware.

Were you able to follow the tutorial step by step?

Did you succeded implementing your own camera surveillance system?

Drop me a line if you're having trouble or you think your businees will benefit from such a technology.

Visit the Github project page for the full code.

Capture RGB Image

Notify me via email when the RGB version is out

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.