Motion detection algorithms form an important part of a video surveillance system. It can also be coupled with many AI-based human presence and profiling systems.
Many applications of motion detection algorithms may be thought of with substantive advantages in saving man-hours, efforts, and storage requirements, that is:
- During live video surveillance, the motion detection algorithm can be used to trigger an alarm or, through a bounding box, alert the security personnel of intrusion.
- The Disk space requirement can greatly be reduced by discarding all static video frames and saving only frames that contain perceivable motion.
- From recorded surveillance videos, an event can be quickly sought by playing only the frames with motion detection.
- The algorithm may be enhanced to leverage motion tracking, which could be used to develop AI models for football and other game players’ movement analysis.
Out of my sheer personal experience, which I hereby share to contribute my little penny towards collective knowledge of humanity.
I would limit my discussion to OpenCV using Python, the best practices, and a few practical coding tips, which I have experienced through working on real-world projects on surveillance systems.
For coding purposes, I used the Ubuntu 22.04LTS, Anaconda environment, Python 3.8, and Visual Studio Code.
💡 Before I delve into coding, it is pertinent to mention here that motion detection in OpenCV is based on finding the change in pixel values of a previous video frame with respect to the current frame.
Preliminary Steps performed:
- Install Anaconda
- Install Visual Code
- Create an environment preferably named
cv
using the following commands at the conda prompt.
$ conda create -n cv python==3.8.15 $ conda activate cv $ python -m ipykernel install –user –name cv $ pip install opencv ipykernel matplotlib imutils numpy easydict
Now, create a project folder and open visual code there, and select cv
as Python kernel environment inside the IDE.
I have adopted a modular approach by distributing the code into three modules:
- Module 1:
motionDetection.py
code for core motion detection algorithm - Module 2:
main.py
code for calling, and administration - Module 3:
config.py
code for setting configurable options
The ‘video_file.mp4/avi
’ file to stream from of camera not used
Module 1

Let’s dive into how I created a file ‘motion.py
’ in the project directory inside VS Code and open it.
Step 1: Importing required packages.
import cv2 import imutils import numpy as np from config import cfg
To facilitate the generalization and reusability of the code, I wrapped the motion algorithm inside a function, accepting the two video frames to realize motion.
Step 2: Define a function header
def motionDetection(currFrame, exFrame):
Step 3: Motion detection algorithm begins
# making copy of the current frame frame = np.array(currFrame.copy()) # convert the frame to grayscale, ad colour may introduce additional edges gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # blur the image it to overcome sharp edges gray = cv2.GaussianBlur(gray, (21, 21), 0)
Step 4
At the very beginning of the execution, there is no previous frame and it will generate errors. The following code would avoid such errors.
# if the average frame is None, initialize it with the current frame if exFrame is None: exFrame = gray.copy().astype("float")
Step 5
The cameras may capture noise due to some momentarily light flashes at night. This code will make the current frame mixed with exFrame
a defined weight/ratio. A value of 0.4 to 0.6 worked best for me to overcome the noise.
# accumulate the weighted average between the current & previous frames, cv2.accumulateWeighted(gray, exFrame, 0.4)
Step 6
Findling the difference in pixels between the frames according to a configurable threshold, named cfg.motionsenseThreshold
, configurable inside config.py
.
A value less than 10 works best for the night or low light conditions,
while for daytime, a value above 25 would be better.
# compute the difference between the current frame and running average frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(exFrame)) thresh = cv2.threshold(frameDelta, cfg.motion.senseThreshold, 255, cv2.THRESH_BINARY)[1] # dilation function make to contour lines wider for easy discrimination thresh = cv2.dilate(thresh, None, iterations=3)
Step 7
The motion contours detection code. In the 3rd line, the 10 most prominent contours are selected. This can be changed as per requirement, i.e., for game fields, it should be more.
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:10]
Step 8
Some initializations for usage in the loop:
img = frame motion = 0
Step 9
Looping over the contours. cfg.motion.senseArea
is an adjustable parameter through config.py
and will set a minimum area below which motion would not be detected.
for c in cnts: # if the contour is too small, ignore it if (cv2.contourArea(c) < cfg.motionsenseArea): continue # a mask can be added here to exclude certain areas from motion detection # compute the bounding box for the contour, draw it on the frame motion += 1 (x, y, w, h) = cv2.boundingRect(c) # draw a red thick bounding box around motion area img = cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2) rect = cv2.minAreaRect(c) # get the min area rect box = cv2.boxPoints(rect) box = np.int0(box) #convert coordinates floating point values to int # draw a blue thin 'nghien' rectangle around the motion area. img = cv2.drawContours(img, [box], 0, (255, 0, 0),1)
Step 10
The motionDetection
function returns the motion-marked-image, the previous frame (used for refeeding to the next frame, and a motion parameter signifying the number of detected areas of motion and can be used to play sound with value as intensity when motion is detected for alert purposes.
Module 2

Creating a file main.py
in the project directory inside VS Code and open it for coding. This will host the code for loading/opening the motion stream and calling the motionDetection
function and other support tasks.
Step 1
Importing packages
import cv2 import time from datetime import datetime from motion import motionDetection from config import cfg
Step 2
To make the code more versatile, I added two important elements to control video size, the choice of the encoder and the required FPS, both configurable in ‘config.py’.
vidEncoder = cv2.VideoWriter_fourcc(*cfg.videoEncoder) ReqFPS = cfg.videoFps # desired frame per second
Step 3
Defining the capStream()
function responsible for looping over the video frames:
def captureStream(cap):
Step 4
Pre-Loop initializations
frameCount = 0 exFrame = None frw = cap.get(cv2.CAP_PROP_FRAME_WIDTH) frh = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) dimensions = (int(frw * cfg.videoScale), int(frh * cfg.videoScale)) fps = round(cap.get(cv2.CAP_PROP_FPS)) skip = round(fps / reqFPS) # used later for target fps adjustment start_time = time.time() fileName = "Output_" + str(datetime.now().strftime("%Y-%m-%d-%H%M%S")) + ".avi" # file name with date,time stamping out = cv2.VideoWriter(fileName, vidEncoder, cfg.videoFps, dimensions) recordDuration = int(cfg.videoDuration * 60) print("Record Duration : " + str(recordDuration)) print("Started @ : " + str(datetime.now().strftime("%Y-%m-%d-%H%M%S")))
Step 5
Start of while loop for executing the code frame-by-frame
while cap.isOpened(): # as long as/if stream handle is open now = time.time() success, frame = cap.read() # read the camera frame elapsed = round(now - start_time)
Step 6
Go ahead if the frame reading is successful.
if success: frameCount += 1 the_frame = frame.copy()
Step 7
If rescaling of the output code is desired, videoScale
is adjustable through config.py
if cfg.videoScale < 1.0: # downscale by configurable factor the_frame = cv2.resize(frame, dimensions, \ interpolation=cv2.INTER_AREA) # rescaling using OpenCV
Step 8
Motion Detection and output saving loop
try: if elapsed < recordDuration: if frameCount % 4 == 0: # gap to avoid inter-frame noise the_frame, exFrame, motion = \ motionDetection(the_frame, exFrame) if (cfg.videoShow): print(f"Frame: {frameCount}, Motion: {motion}") # display the motion detection feed cv2.imshow("Motion Detection Feed", the_frame) key = cv2.waitKey(1) & 0xFF if key == ord("q"): # press `q` to break loop break if frameCount % skip == 0: # skip ‘skip’ frames from save out.write(the_frame) #write frame to file
Step 9
The else
portion of the if
block when recordDuration
exceeds the set duration. It writes the file and starts a new file with the current date, time stamping
else: out.release() print("Record Saved : " + fileName, elapsed) # starting new file with current time print("Starting new ...") fileName = "Output_" + \ str(datetime.now().strftime("%Y-%m-%d-%H%M%S")) + ".avi" # initialising file handle out = cv2.VideoWriter(fileName, vidEncoder, cfg.videoFps, \ dimensions) # also re-initializing timer and frame counter start_time = time.time() frameCount = 0 except Exception as e: # exception handling print("Error found : "+ str(e))
Step 10
The ending lines of the capture loop
else: # if frame read not successful cap.release() print("Stopped @: " + \ str(datetime.now().strftime("%Y-%m-%d-%H%M%S"))) # the function ‘captureStream’ return message of cap.isOpened() fails’ return "Video Capture Object not open..."
Step 11
Calling the body of the main.py
module, depending upon the input file/camera, code may be modified.
if __name__ == '__main__': # if video file is the input stream vidFile = r'3.avi' cap_stream = cv2.VideoCapture(vidFile) # if the camera is the input stream, open it with the cam id. #cap_stream = cv2.VideoCapture(0) captureStream(cap_stream)
Module 3

Creating a file config.py
in the project directory inside VS Code and opening it for coding. This file holds the configurable parameters, which are used in the main.py
and motion.py
modules.
Step 1
Import the package ‘easydict
’. This package proved quite handy for managing the global configurable parameters.
from easydict import EasyDict as edict
Step 2
Instantiate the dictionary object and assign values. The comments against each value are self-descriptive:
cfg = edict() # used to set video-related parameters cfg.videoFps = 10 # default = 10, video saving FPS cfg.videoEncoder = 'WMV1' # encoder options are ‘MJPG’ ‘XVID’ ‘WVM2’ cfg.videoScale = 1 # down-scales video frames size, recommended 0.5 to 1.0 cfg.videoShow = True # to show/hide output video cfg.videoDuration = 100 # video save file duration in # used to set motion sensitivity parameters cfg.motionSenseThreshold = 6 # for night cameras with low light, reduce this parameter below 10, This is the threshold pixel value for motion perception. cfg.motionSenseArea = 150 # default = 900, The min Area threshold for detection
This finishes off the code.
Conclusion

Reviewers’ feedback is welcome, and I hope this will provide a reasonable baseline for the motion detection projects using OpenCV, which can run on minimal hardware.
Also, please think of it separately from object detection. It will detect the object or part of it in motion.
The algorithm can be enhanced to include object detection, but I limited it to combine with surveillance system recording only when some motion is detected.
I have tried to include the Yolo object detection combined with it, but in multi-cam implementation, with basic hardware, it proved to be computationally expensive and produced substantially lower FPS.
My LinkedIn: https://www.linkedin.com/in/tj-yazman/