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
cvusing 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.pycode for core motion detection algorithm - Module 2:
main.pycode for calling, and administration - Module 3:
config.pycode 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 OpenCVStep 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 fileStep 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/
