Motion Detection script using the power of Python and OpenCV. Due to the problem of false triggering by the PIR motion sensor, I have decided to use OpenCV motion detection feature to trigger the recording and storing process. The setup is even more simple : power, thumb drive and camera module. No PIR sensor. 👍

To install Python3 libraries(eg.pandas), type : sudo pip3 install pandas. However to install OpenCV to Rpi is not that direct, and requirement a few hours to install. I am using RPI Model 3B+ and it took me half a day to install OpenCV. I followed the steps from this website (https://pimylifeup.com/raspberry-pi-opencv/). To me, this is the hardest and most tedious part of this project. Afterwhich we are ready to write the motion detection script.
The script is from Automatic Addison website — https://automaticaddison.com/motion-detection-using-opencv-on-raspberry-pi-4/. This is a fantastic website, please check it out. I made a few changes for triggering the video recording and rotating the camera.
The script for the Python OpenCV motion detection ↓
from picamera.array import PiRGBArray # Generates a 3D RGB array
from picamera import PiCamera # Provides a Python interface for the RPi Camera Module
import time
from time import sleep
import datetime
import cv2
import numpy as np
# Initialize the camera
camera = PiCamera()
camera.rotation = -90
# Set the camera resolution
camera.resolution = (640, 480)
#camera.resolution = (1280, 720)
#camera.resolution = (1640, 922)
#camera.resolution = (3280, 2464)
# Set the number of frames per second
#Resolution and frame rate affect FOV
#camera.framerate = 30
#camera.framerate = 60
camera.framerate = 30
#Generates a 3D RGB array from camera object(source)
#and put into raw_capture
raw_capture = PiRGBArray(camera, size=(640, 480))
#raw_capture = PiRGBArray(camera, size=(1280, 720))
#raw_capture = PiRGBArray(camera, size=(1640, 922))
#raw_capture = PiRGBArray(camera, size=(3280, 2464))
# Create the background object
# Parameters set to default values if leave blank
# MOG2 comes with detectShadows parameter, MOG does not.
#back_sub = cv2.createBackgroundSubtractorMOG2(history=150,
#varThreshold=25, detectShadows=True)
back_sub = cv2.createBackgroundSubtractorMOG2(history=25,
varThreshold=20, detectShadows=True)
# Wait a certain number of seconds to allow the camera time to warmup
time.sleep(0.1)
# Create kernel for morphological operation. You can tweak
# the dimensions of the kernel.
# e.g. instead of 20, 20, you can try 30, 30
# This is like a template running through fg_mask and smoothen the edges
kernel = np.ones((20,20),np.uint8)
# Capture frames continuously from the camera
for frame in camera.capture_continuous(raw_capture, format="bgr", use_video_port=True):
# Grab the raw NumPy array representing the image
image = frame.array
# Get the foreground mask
fg_mask = back_sub.apply(image)
# Perform morphological operation (function cv2.morphologyEx aka Closing),
# which is a combination of Dilation followed by Erosion.
# Closing closes any small holes inside fg_mask, or remove any small black points in
# fg_mask. Closing is opposite of Opening (Erosion followed by Dilation)
fg_mask = cv2.morphologyEx(fg_mask,cv2.MORPH_CLOSE,kernel)
# Remove the unnessary details (salt n pepper noise),we need the outline only
fg_mask = cv2.medianBlur(fg_mask,5)
# If a pixel is less than 127, it is considered black (background)
# and set to 0, otherwise, it is white (foreground). Set to 255.
# Modify the number after fg_mask as you see fit.
#4th parameter is one of the type of threshold which is : if less than
# 127 set to black else set to white
# ret usually in front is not neccessary as we do not require a return.
_, fg_mask = cv2.threshold(fg_mask, 127, 255, cv2.THRESH_BINARY)
# Find the contours of the object inside the binary image
contours, hierarchy = cv2.findContours(fg_mask,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[-2:]
areas = [cv2.contourArea(c) for c in contours]
# If there are no countours
if len(areas) < 1:
# Display the resulting frame
cv2.imshow('Frame',image)
#Record.....
#while w > 500 and h > 500: #doesn't work
while h > 200:
date = datetime.datetime.now().strftime("%d-%m-%Y_%H-%M-%S")
camera.start_recording('/mnt/mydisk/'+date+'.h264')
sleep(5)
camera.stop_recording()
break
# Wait for keyPress for 1 millisecond
key = cv2.waitKey(1) & 0xFF
# Clear the stream in preparation for the next frame
raw_capture.truncate(0)
# If "q" is pressed on the keyboard,
# exit this loop
if key == ord("q"):
break
# Go to the top of the for loop
continue
else:
# Find the largest moving object in the image
max_index = np.argmax(areas)
# Draw the bounding box
cnt = contours[max_index]
x,y,w,h = cv2.boundingRect(cnt)
cv2.rectangle(image,(x,y),(x+w,y+h),(0,255,0),2)
break
# Wait for keyPress for 1 millisecond
key = cv2.waitKey(1) & 0xFF
# Clear the stream in preparation for the next frame
raw_capture.truncate(0)
# If "q" is pressed on the keyboard,
# exit this loop
if key == ord("q"):
break
# Close down windows
cv2.destroyAllWindows()
Video to explain about the workings of the script ↓. The recorded video (2nd video below) will not have the green rectangle box and the x-y coordinates.
Below 2 videos is to show that there is continuity between 2 back to back recorded videos . ↓