Video based Motion Detection in Python with OpenCV

Demo

I added 30 seconds buffer before the scipt start recording so we can see the green color indicates the detected movements.

What you need

  • A Webcam
  • Python and pip

Requirements.txt

1
2
opencv-python
numpy

Goal

To implement a security camera auto record videos when some thing moves in the view port.

Source code

The original implementation was in Python 2.x with OpenCV 3.x

I fork it to Python 3.x and make it compatible with OpenCV 4.x

The MotionDetectorContours.py and MotionDetector.py results the same in the original implementation. But they give different result in my implementation, I guess it is because I skiped cv.Erode() in MotionDetectorContours.py.

The original implementation

There are two implementation according to the developer.

Simple way

  1. Receive frames and send to process in run()
  2. processImage() calculate difference of pixels in frames
  3. If number of different pixels exceed the threshold, somethingHasMoved() return True
  4. run() enable recording if somethingHasMoved() return True

Smart way

  1. Receive frames and send to process in run()
  2. processImage() calculate difference of areas in frames with cv2.findContours()
  3. If change of area in comparing to total area exceed the threshold, somethingHasMoved() return True
  4. run() enable recording if somethingHasMoved() return True

Our implementation

Our implementation will be based on MotionDetectorContours.py. It did better job for me and it can catch my eye blinking.

Import OpenCV for image processing, Numpy for replacing cv2.CreateImage(), datetime and tmie for showing video recording time.

1
2
3
4
import cv2 as cv
import numpy as np
from datetime import datetime
import time

Define a class for image processing and maintain the loop of reading frames.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MotionDetectorAdaptative():
def onChange(self, val): #callback when the user change the detection threshold
pass

def __init__(self,threshold=25, doRecord=True, showWindows=True):
pass

def initRecorder(self): #Create the recorder
pass

def run(self):
pass

def processImage(self, curframe):
pass

def somethingHasMoved(self):
pass

The initializatoin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def __init__(self,threshold=25, doRecord=True, showWindows=True):
self.writer = None
self.font = None
self.doRecord=doRecord #Either or not record the moving object
self.show = showWindows #Either or not show the 2 windows
self.frame = None

self.capture=cv.VideoCapture(0)
self.frame = self.capture.read()[1] #Take a frame to init recorder
if doRecord:
self.initRecorder()

self.absdiff_frame = None
self.previous_frame = None

self.surface = self.frame.shape[0] * self.frame.shape[1]
self.currentsurface = 0
self.currentcontours = None
self.threshold = threshold
self.isRecording = False
self.trigger_time = 0 #Hold timestamp of the last detection
self.es = cv.getStructuringElement(cv.MORPH_ELLIPSE, (9,4))

if showWindows:
cv.namedWindow("Image")
# for user to change threshold in runtime
cv.createTrackbar("Detection treshold: ", "Image", self.threshold, 100, self.onChange)

run() will maintain the loop to:

  1. read frame
  2. pass to processImage()
  3. check if anything moved in somethingHasMoved(), isRecording = True if things moved
  4. if isRecording == True a video recorder will be activated
  5. draw area of moved/changed to frame and display it on screen
  6. repeat the steps if Esc was not pressed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def run(self):
started = time.time()
while True:

currentframe = self.capture.read()[1]
instant = time.time() #Get timestamp o the frame

self.processImage(currentframe) #Process the image

if not self.isRecording:
if self.somethingHasMoved():
self.trigger_time = instant #Update the trigger_time
if instant > started +10:#Wait 5 second after the webcam start for luminosity adjusting etc..
print("Something is moving !")
if self.doRecord: #set isRecording=True only if we record a video
self.isRecording = True
currentframe = cv.drawContours(currentframe, self.currentcontours, -1, (0, 255, 0), cv.FILLED)
else:
if instant >= self.trigger_time +10: #Record during 10 seconds
print("Stop recording")
self.isRecording = False
else:
cv.putText(currentframe,datetime.now().strftime("%b %d, %H:%M:%S"), (25,30),self.font, 1, (255, 0, 0), 2, cv.LINE_AA) #Put date on the frame
self.writer.write(currentframe) #Write the frame

if self.show:
cv.imshow("Image", currentframe)

c=cv.waitKey(1) % 0x100
if c==27 or c == 10: #Break if user enters 'Esc'.
break

Find the change between frames and do simple feature extraction, result saved to self.gray_frame.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def processImage(self, curframe):
curframe = cv.GaussianBlur(curframe, (21,21), 0) #Remove false positives

if self.absdiff_frame is None: #For the first time put values in difference, temp and moving_average
self.absdiff_frame = curframe.copy()
self.previous_frame = curframe.copy()
self.average_frame = np.float32(curframe) #Should convert because after runningavg take 32F pictures
else:
cv.accumulateWeighted(curframe, self.average_frame, 0.05) #Compute the average

self.previous_frame = self.average_frame.astype(np.uint8) #Convert back to 8U frame

self.absdiff_frame = cv.absdiff(curframe, self.previous_frame) # moving_average - curframe

self.gray_frame = cv.cvtColor(self.absdiff_frame, cv.COLOR_BGR2GRAY) #Convert to gray otherwise can't do threshold
self.gray_frame = cv.threshold(self.gray_frame, 5, 255, cv.THRESH_BINARY)[1]

self.gray_frame = cv.dilate(self.gray_frame, self.es) #to get object blobs
# cv.Erode(self.gray_frame, self.gray_frame, None, 10)

Find the area of changes and compare to threshold over the whole area:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def somethingHasMoved(self):

# Find contours, ignore other return values (image, contours, hierarchy)[1]
contours = cv.findContours(self.gray_frame, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)[1]

self.currentcontours = contours #Save contours

self.currentsurface = sum([cv.contourArea(c) for c in contours]) #For all contours compute the area

avg = (self.currentsurface*100)/self.surface #Calculate the average of contour area on the total size
self.currentsurface = 0 #Put back the current surface to 0

if avg > self.threshold:
return True
else:
return False

If user change threshold during runtime, onChange() method in the class will be triggered:

1
2
def onChange(self, val): #callback when the user change the detection threshold
self.threshold = val

Declare a video recorder writes frame to video file:

1
2
3
4
5
def initRecorder(self): #Create the recorder
codec = cv.VideoWriter_fourcc('M', 'J', 'P', 'G')
self.writer=cv.VideoWriter(datetime.now().strftime("%b-%d_%H_%M_%S")+".wmv", codec, 5, self.frame.shape[1::-1], 1)
#FPS set to 5 because it seems to be the fps of my cam but should be ajusted to your needs
self.font = cv.FONT_HERSHEY_SIMPLEX #Creates a font

Let’s try it.

1
2
3
if __name__=="__main__":
detect = MotionDetectorAdaptative(threshold=5, doRecord=True)
detect.run()

Improvements

There is a problem the video only record for 10 seconds. Base on MotionDetectorContours.py modify its condition to end recording:

1
if instant >= self.trigger_time +10 and not self.somethingHasMoved(): #Record until move stop 10 seconds

We can add an Email notification in this. You may refer to the tutorial sending Email notification for network usage report. And we can then combine it with Raspberry Pi to build a security camera.

You will need these things for a Raspberry Pi security camera:

If you ask, I used a wide angle camera

Sixfab has a tutorial about Raspberry Pi Security System with Sixfab 3G, 4G/LTE Base Shield. It seem to use a motion sensor to detect moment in the area and activate camera when sensor detect movement, it then send a frame to a email specified in the Python script.

I didn’t find the hardware list of their design, but the Python source code is here.

I think using a motion sensor is a good idea, Docker Pi Series of Sensor Hub seem to be a good option, considering to have it in my production environment.