├── LICENSE ├── Motion Detector.py ├── README.md └── assets └── youtube_thumbnail.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 methylDragon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Motion Detector.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | OpenCV Motion Detector 4 | @author: methylDragon 5 | 6 | . . 7 | . |\-^-/| . 8 | /| } O.=.O { |\ 9 | /´ \ \_ ~ _/ / `\ 10 | /´ | \-/ ~ \-/ | `\ 11 | | | /\\ //\ | | 12 | \|\|\/-""-""-\/|/|/ 13 | ______/ / 14 | '------' 15 | _ _ _ ___ 16 | _ __ ___| |_| |_ _ _| || \ _ _ __ _ __ _ ___ _ _ 17 | | ' \/ -_) _| ' \ || | || |) | '_/ _` / _` / _ \ ' \ 18 | |_|_|_\___|\__|_||_\_, |_||___/|_| \__,_\__, \___/_||_| 19 | |__/ |___/ 20 | ------------------------------------------------------- 21 | github.com/methylDragon 22 | 23 | References/Adapted From: 24 | https://www.pyimagesearch.com/2015/05/25/basic-motion-detection-and-tracking-with-python-and-opencv/ 25 | 26 | Description: 27 | This script runs a motion detector! It detects transient motion in a room 28 | and said movement is large enough, and recent enough, reports that there is 29 | motion! 30 | 31 | Run the script with a working webcam! You'll see how it works! 32 | """ 33 | 34 | import imutils 35 | import cv2 36 | import numpy as np 37 | 38 | # ============================================================================= 39 | # USER-SET PARAMETERS 40 | # ============================================================================= 41 | 42 | # Number of frames to pass before changing the frame to compare the current 43 | # frame against 44 | FRAMES_TO_PERSIST = 10 45 | 46 | # Minimum boxed area for a detected motion to count as actual motion 47 | # Use to filter out noise or small objects 48 | MIN_SIZE_FOR_MOVEMENT = 2000 49 | 50 | # Minimum length of time where no motion is detected it should take 51 | #(in program cycles) for the program to declare that there is no movement 52 | MOVEMENT_DETECTED_PERSISTENCE = 100 53 | 54 | # ============================================================================= 55 | # CORE PROGRAM 56 | # ============================================================================= 57 | 58 | 59 | # Create capture object 60 | cap = cv2.VideoCapture(5) # Flush the stream 61 | cap.release() 62 | cap = cv2.VideoCapture(0) # Then start the webcam 63 | 64 | # Init frame variables 65 | first_frame = None 66 | next_frame = None 67 | 68 | # Init display font and timeout counters 69 | font = cv2.FONT_HERSHEY_SIMPLEX 70 | delay_counter = 0 71 | movement_persistent_counter = 0 72 | 73 | # LOOP! 74 | while True: 75 | 76 | # Set transient motion detected as false 77 | transient_movement_flag = False 78 | 79 | # Read frame 80 | ret, frame = cap.read() 81 | text = "Unoccupied" 82 | 83 | # If there's an error in capturing 84 | if not ret: 85 | print("CAPTURE ERROR") 86 | continue 87 | 88 | # Resize and save a greyscale version of the image 89 | frame = imutils.resize(frame, width = 750) 90 | gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 91 | 92 | # Blur it to remove camera noise (reducing false positives) 93 | gray = cv2.GaussianBlur(gray, (21, 21), 0) 94 | 95 | # If the first frame is nothing, initialise it 96 | if first_frame is None: first_frame = gray 97 | 98 | delay_counter += 1 99 | 100 | # Otherwise, set the first frame to compare as the previous frame 101 | # But only if the counter reaches the appriopriate value 102 | # The delay is to allow relatively slow motions to be counted as large 103 | # motions if they're spread out far enough 104 | if delay_counter > FRAMES_TO_PERSIST: 105 | delay_counter = 0 106 | first_frame = next_frame 107 | 108 | 109 | # Set the next frame to compare (the current frame) 110 | next_frame = gray 111 | 112 | # Compare the two frames, find the difference 113 | frame_delta = cv2.absdiff(first_frame, next_frame) 114 | thresh = cv2.threshold(frame_delta, 25, 255, cv2.THRESH_BINARY)[1] 115 | 116 | # Fill in holes via dilate(), and find contours of the thesholds 117 | thresh = cv2.dilate(thresh, None, iterations = 2) 118 | _, cnts, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 119 | 120 | # loop over the contours 121 | for c in cnts: 122 | 123 | # Save the coordinates of all found contours 124 | (x, y, w, h) = cv2.boundingRect(c) 125 | 126 | # If the contour is too small, ignore it, otherwise, there's transient 127 | # movement 128 | if cv2.contourArea(c) > MIN_SIZE_FOR_MOVEMENT: 129 | transient_movement_flag = True 130 | 131 | # Draw a rectangle around big enough movements 132 | cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) 133 | 134 | # The moment something moves momentarily, reset the persistent 135 | # movement timer. 136 | if transient_movement_flag == True: 137 | movement_persistent_flag = True 138 | movement_persistent_counter = MOVEMENT_DETECTED_PERSISTENCE 139 | 140 | # As long as there was a recent transient movement, say a movement 141 | # was detected 142 | if movement_persistent_counter > 0: 143 | text = "Movement Detected " + str(movement_persistent_counter) 144 | movement_persistent_counter -= 1 145 | else: 146 | text = "No Movement Detected" 147 | 148 | # Print the text on the screen, and display the raw and processed video 149 | # feeds 150 | cv2.putText(frame, str(text), (10,35), font, 0.75, (255,255,255), 2, cv2.LINE_AA) 151 | 152 | # For if you want to show the individual video frames 153 | # cv2.imshow("frame", frame) 154 | # cv2.imshow("delta", frame_delta) 155 | 156 | # Convert the frame_delta to color for splicing 157 | frame_delta = cv2.cvtColor(frame_delta, cv2.COLOR_GRAY2BGR) 158 | 159 | # Splice the two video frames together to make one long horizontal one 160 | cv2.imshow("frame", np.hstack((frame_delta, frame))) 161 | 162 | 163 | # Interrupt trigger by pressing q to quit the open CV program 164 | ch = cv2.waitKey(1) 165 | if ch & 0xFF == ord('q'): 166 | break 167 | 168 | # Cleanup when closed 169 | cv2.waitKey(0) 170 | cv2.destroyAllWindows() 171 | cap.release() 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opencv-motion-detector 2 | Detects transient motion in a video feed. If said motion is large enough, and recent enough, reports that there is motion! 3 | 4 | 5 | 6 | The motion detected state is then held for some user specified amount of time even when no motion is detected until finally the program declares that no motion is detected. 7 | 8 | 9 | 10 | [![Click for video!](./assets/youtube_thumbnail.png)](https://youtu.be/z_X5PFkaPwY) 11 | 12 | 13 | 14 | ## **Dependencies** 15 | 16 | - OpenCV 2 17 | - Image Utilities (imutils) 18 | 19 | 20 | 21 | ## Working Theory 22 | 23 | OpenCV is used to calculate absolute frame deltas against the most recent saved frame and the current frame. The frame deltas are then passed through a threshold filter, and bounding boxes are drawn around contours of the thresholded frame. (The frequency of saving frames is determined via the user parameter.) 24 | 25 | 26 | 27 | A sufficiently large bounding box derived from the contours of a thresholded frame delta image is considered movement. 28 | 29 | 30 | 31 | ## **User Configurable Parameters** 32 | 33 | Just change these few parameters located near the top of the Python script 34 | 35 | - **FRAMES_TO_PERSIST** 36 | - Number of frames to pass before changing the frame to compare the current frame against. 37 | - **MIN_SIZE_FOR_MOVEMENT** 38 | - Minimum boxed area for a detected motion to count as actual motion. Use it to filter out noise or small objects. 39 | - **MOVEMENT_DETECTED_PERSISTENCE** 40 | - Minimum length of time where no motion is detected it should take (in program cycles) for the program to declare that there is no movement. -------------------------------------------------------------------------------- /assets/youtube_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/methylDragon/opencv-motion-detector/b2c762d92113fe47433433982a81294bb476754a/assets/youtube_thumbnail.png --------------------------------------------------------------------------------