Motion Detection Using OpenCV in Python – My journey through to a successful project

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/