From fe2a9c97a6efbd37626f02d8fb95b6190b007853 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 9 Oct 2025 15:26:20 +0530 Subject: [PATCH 1/8] Add basic motion detection script Introduces motion_detection.py with frame differencing and background subtraction (MOG2) for motion detection using OpenCV. The script provides real-time motion mask and annotated frame display, with adjustable parameters for contour filtering and morphology. --- computer_vision/motion_detection.py | 135 ++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 computer_vision/motion_detection.py diff --git a/computer_vision/motion_detection.py b/computer_vision/motion_detection.py new file mode 100644 index 000000000000..3d0ebe3bc5ef --- /dev/null +++ b/computer_vision/motion_detection.py @@ -0,0 +1,135 @@ +import cv2 +import numpy as np + +""" +Basic Motion Detection using frame differencing and background subtraction (MOG2). + +Usage: + - Set VIDEO_SOURCE to a video file path or an integer (e.g., 0) for webcam. + - The script shows two windows: motion mask and annotated frame. + - Press 'q' to quit. + +Notes: + - Requires OpenCV (opencv-python) and NumPy. + - This example focuses on clarity and educational value, not production tuning. +""" + + +# Parameters +VIDEO_SOURCE = 0 # use integer for webcam (e.g., 0) or string path for video file +MIN_CONTOUR_AREA = 500 # pixels; filter tiny motions/noise +MORPH_KERNEL_SIZE = (5, 5) # kernel for opening/closing +DISPLAY_SCALE = 1.0 # resize factor for display + + +def create_background_subtractor() -> cv2.BackgroundSubtractor: + """ + Create and return a MOG2 background subtractor with sensible defaults. + """ + # history=500, varThreshold=16 are common defaults; detectShadows adds robustness + return cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=16, detectShadows=True) + + +def preprocess_frame(frame: cv2.Mat) -> cv2.Mat: + """ + Convert to grayscale and apply Gaussian blur to suppress noise. + """ + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + blurred = cv2.GaussianBlur(gray, (5, 5), 0) + return blurred + + +def frame_difference(prev_gray: cv2.Mat, curr_gray: cv2.Mat) -> cv2.Mat: + """ + Compute absolute difference between consecutive grayscale frames. + Returns a binary motion mask after thresholding and morphology. + """ + diff = cv2.absdiff(prev_gray, curr_gray) + _, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY) + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, MORPH_KERNEL_SIZE) + opened = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1) + closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=1) + return closed + + +def background_subtraction_mask(subtractor: cv2.BackgroundSubtractor, frame: cv2.Mat) -> cv2.Mat: + """ + Apply background subtraction to obtain a motion mask. Includes morphology. + """ + fg_mask = subtractor.apply(frame) + # Remove shadows if present (MOG2 shadows are typically 127) + _, fg_mask = cv2.threshold(fg_mask, 200, 255, cv2.THRESH_BINARY) + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, MORPH_KERNEL_SIZE) + opened = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN, kernel, iterations=1) + closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=1) + return closed + + +def annotate_motion(frame: cv2.Mat, motion_mask: cv2.Mat) -> cv2.Mat: + """ + Find contours on the motion mask and draw bounding boxes on the frame. + """ + contours, _ = cv2.findContours(motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + annotated = frame.copy() + for contour in contours: + if cv2.contourArea(contour) < MIN_CONTOUR_AREA: + continue + x, y, w, h = cv2.boundingRect(contour) + cv2.rectangle(annotated, (x, y), (x + w, y + h), (0, 255, 0), 2) + return annotated + + +def main() -> None: + cap = cv2.VideoCapture(VIDEO_SOURCE) + if not cap.isOpened(): + raise RuntimeError("Unable to open video source. Set VIDEO_SOURCE correctly.") + + subtractor = create_background_subtractor() + + ret, prev_frame = cap.read() + if not ret: + cap.release() + raise RuntimeError("Failed to read initial frame from source.") + + prev_gray = preprocess_frame(prev_frame) + + while True: + ret, frame = cap.read() + if not ret: + break + + # Optionally resize for display/performance + if DISPLAY_SCALE != 1.0: + frame = cv2.resize(frame, None, fx=DISPLAY_SCALE, fy=DISPLAY_SCALE) + + curr_gray = preprocess_frame(frame) + + # Frame differencing motion mask + diff_mask = frame_difference(prev_gray, curr_gray) + + # Background subtraction motion mask + bs_mask = background_subtraction_mask(subtractor, frame) + + # Combine masks to be more robust (logical OR) + combined_mask = cv2.bitwise_or(diff_mask, bs_mask) + + annotated = annotate_motion(frame, combined_mask) + + cv2.imshow("Motion Mask", combined_mask) + cv2.imshow("Motion Detection", annotated) + + prev_gray = curr_gray + + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break + + cap.release() + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() + print("DONE ✅") + + From f522c31f8d33bae36dca7fe2576494a81f639c40 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:00:54 +0000 Subject: [PATCH 2/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- computer_vision/motion_detection.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/computer_vision/motion_detection.py b/computer_vision/motion_detection.py index 3d0ebe3bc5ef..3ec376944488 100644 --- a/computer_vision/motion_detection.py +++ b/computer_vision/motion_detection.py @@ -27,7 +27,9 @@ def create_background_subtractor() -> cv2.BackgroundSubtractor: Create and return a MOG2 background subtractor with sensible defaults. """ # history=500, varThreshold=16 are common defaults; detectShadows adds robustness - return cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=16, detectShadows=True) + return cv2.createBackgroundSubtractorMOG2( + history=500, varThreshold=16, detectShadows=True + ) def preprocess_frame(frame: cv2.Mat) -> cv2.Mat: @@ -52,7 +54,9 @@ def frame_difference(prev_gray: cv2.Mat, curr_gray: cv2.Mat) -> cv2.Mat: return closed -def background_subtraction_mask(subtractor: cv2.BackgroundSubtractor, frame: cv2.Mat) -> cv2.Mat: +def background_subtraction_mask( + subtractor: cv2.BackgroundSubtractor, frame: cv2.Mat +) -> cv2.Mat: """ Apply background subtraction to obtain a motion mask. Includes morphology. """ @@ -69,7 +73,9 @@ def annotate_motion(frame: cv2.Mat, motion_mask: cv2.Mat) -> cv2.Mat: """ Find contours on the motion mask and draw bounding boxes on the frame. """ - contours, _ = cv2.findContours(motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + contours, _ = cv2.findContours( + motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) annotated = frame.copy() for contour in contours: if cv2.contourArea(contour) < MIN_CONTOUR_AREA: @@ -131,5 +137,3 @@ def main() -> None: if __name__ == "__main__": main() print("DONE ✅") - - From a4858c4d21a4ba88af71254c5babb4cb6c8c7ec8 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 9 Oct 2025 15:36:47 +0530 Subject: [PATCH 3/8] Add doctests to motion detection functions Doctests were added to all major functions in motion_detection.py to provide usage examples and enable easier testing. This improves code reliability and documentation by demonstrating expected input and output for each function. --- computer_vision/motion_detection.py | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/computer_vision/motion_detection.py b/computer_vision/motion_detection.py index 3d0ebe3bc5ef..d3b95f389722 100644 --- a/computer_vision/motion_detection.py +++ b/computer_vision/motion_detection.py @@ -25,6 +25,11 @@ def create_background_subtractor() -> cv2.BackgroundSubtractor: """ Create and return a MOG2 background subtractor with sensible defaults. + + Doctest: + >>> subtractor = create_background_subtractor() + >>> hasattr(subtractor, "apply") + True """ # history=500, varThreshold=16 are common defaults; detectShadows adds robustness return cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=16, detectShadows=True) @@ -33,6 +38,12 @@ def create_background_subtractor() -> cv2.BackgroundSubtractor: def preprocess_frame(frame: cv2.Mat) -> cv2.Mat: """ Convert to grayscale and apply Gaussian blur to suppress noise. + + Doctest: + >>> dummy = np.zeros((10, 10, 3), dtype=np.uint8) + >>> out = preprocess_frame(dummy) + >>> out.shape == (10, 10) and out.dtype == np.uint8 + True """ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) @@ -43,6 +54,16 @@ def frame_difference(prev_gray: cv2.Mat, curr_gray: cv2.Mat) -> cv2.Mat: """ Compute absolute difference between consecutive grayscale frames. Returns a binary motion mask after thresholding and morphology. + + Doctest: + >>> a = np.zeros((8, 8), dtype=np.uint8) + >>> b = np.zeros((8, 8), dtype=np.uint8) + >>> b[2:6, 2:6] = 255 + >>> mask = frame_difference(a, b) + >>> mask.shape + (8, 8) + >>> mask.dtype == np.uint8 + True """ diff = cv2.absdiff(prev_gray, curr_gray) _, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY) @@ -55,6 +76,15 @@ def frame_difference(prev_gray: cv2.Mat, curr_gray: cv2.Mat) -> cv2.Mat: def background_subtraction_mask(subtractor: cv2.BackgroundSubtractor, frame: cv2.Mat) -> cv2.Mat: """ Apply background subtraction to obtain a motion mask. Includes morphology. + + Doctest: + >>> subtractor = create_background_subtractor() + >>> frame = np.zeros((12, 12, 3), dtype=np.uint8) + >>> mask = background_subtraction_mask(subtractor, frame) + >>> mask.shape + (12, 12) + >>> mask.dtype == np.uint8 + True """ fg_mask = subtractor.apply(frame) # Remove shadows if present (MOG2 shadows are typically 127) @@ -68,6 +98,14 @@ def background_subtraction_mask(subtractor: cv2.BackgroundSubtractor, frame: cv2 def annotate_motion(frame: cv2.Mat, motion_mask: cv2.Mat) -> cv2.Mat: """ Find contours on the motion mask and draw bounding boxes on the frame. + + Doctest: + >>> frame = np.zeros((60, 60, 3), dtype=np.uint8) + >>> mask = np.zeros((60, 60), dtype=np.uint8) + >>> mask[10:40, 10:40] = 255 # large enough to exceed MIN_CONTOUR_AREA + >>> annotated = annotate_motion(frame, mask) + >>> np.any(annotated[..., 1] == 255) # green channel from rectangle + True """ contours, _ = cv2.findContours(motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) annotated = frame.copy() @@ -80,6 +118,19 @@ def annotate_motion(frame: cv2.Mat, motion_mask: cv2.Mat) -> cv2.Mat: def main() -> None: + """ + Run motion detection loop for the configured VIDEO_SOURCE. + + Doctest (expect a RuntimeError when pointing to an invalid source): + >>> _prev = VIDEO_SOURCE + >>> VIDEO_SOURCE = "nonexistent_file_does_not_exist.mp4" + >>> try: + ... main() + ... except RuntimeError as e: + ... isinstance(e, RuntimeError) + True + >>> VIDEO_SOURCE = _prev + """ cap = cv2.VideoCapture(VIDEO_SOURCE) if not cap.isOpened(): raise RuntimeError("Unable to open video source. Set VIDEO_SOURCE correctly.") From 084345c50ad86736e1448b70b9175fac0c1b07a9 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 9 Oct 2025 15:40:06 +0530 Subject: [PATCH 4/8] remove unused import --- computer_vision/motion_detection.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/computer_vision/motion_detection.py b/computer_vision/motion_detection.py index c4597ef58fa0..023fbc9a7ea1 100644 --- a/computer_vision/motion_detection.py +++ b/computer_vision/motion_detection.py @@ -1,5 +1,4 @@ import cv2 -import numpy as np """ Basic Motion Detection using frame differencing and background subtraction (MOG2). @@ -32,9 +31,7 @@ def create_background_subtractor() -> cv2.BackgroundSubtractor: True """ # history=500, varThreshold=16 are common defaults; detectShadows adds robustness - return cv2.createBackgroundSubtractorMOG2( - history=500, varThreshold=16, detectShadows=True - ) + return cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=16, detectShadows=True) def preprocess_frame(frame: cv2.Mat) -> cv2.Mat: @@ -75,9 +72,7 @@ def frame_difference(prev_gray: cv2.Mat, curr_gray: cv2.Mat) -> cv2.Mat: return closed -def background_subtraction_mask( - subtractor: cv2.BackgroundSubtractor, frame: cv2.Mat -) -> cv2.Mat: +def background_subtraction_mask(subtractor: cv2.BackgroundSubtractor, frame: cv2.Mat) -> cv2.Mat: """ Apply background subtraction to obtain a motion mask. Includes morphology. @@ -111,9 +106,7 @@ def annotate_motion(frame: cv2.Mat, motion_mask: cv2.Mat) -> cv2.Mat: >>> np.any(annotated[..., 1] == 255) # green channel from rectangle True """ - contours, _ = cv2.findContours( - motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE - ) + contours, _ = cv2.findContours(motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) annotated = frame.copy() for contour in contours: if cv2.contourArea(contour) < MIN_CONTOUR_AREA: @@ -188,3 +181,5 @@ def main() -> None: if __name__ == "__main__": main() print("DONE ✅") + + From 8d0db4870560d95b49f4eb1e6bcff91b054b0a64 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:10:30 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- computer_vision/motion_detection.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/computer_vision/motion_detection.py b/computer_vision/motion_detection.py index 023fbc9a7ea1..74dd6c050837 100644 --- a/computer_vision/motion_detection.py +++ b/computer_vision/motion_detection.py @@ -31,7 +31,9 @@ def create_background_subtractor() -> cv2.BackgroundSubtractor: True """ # history=500, varThreshold=16 are common defaults; detectShadows adds robustness - return cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=16, detectShadows=True) + return cv2.createBackgroundSubtractorMOG2( + history=500, varThreshold=16, detectShadows=True + ) def preprocess_frame(frame: cv2.Mat) -> cv2.Mat: @@ -72,7 +74,9 @@ def frame_difference(prev_gray: cv2.Mat, curr_gray: cv2.Mat) -> cv2.Mat: return closed -def background_subtraction_mask(subtractor: cv2.BackgroundSubtractor, frame: cv2.Mat) -> cv2.Mat: +def background_subtraction_mask( + subtractor: cv2.BackgroundSubtractor, frame: cv2.Mat +) -> cv2.Mat: """ Apply background subtraction to obtain a motion mask. Includes morphology. @@ -106,7 +110,9 @@ def annotate_motion(frame: cv2.Mat, motion_mask: cv2.Mat) -> cv2.Mat: >>> np.any(annotated[..., 1] == 255) # green channel from rectangle True """ - contours, _ = cv2.findContours(motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + contours, _ = cv2.findContours( + motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) annotated = frame.copy() for contour in contours: if cv2.contourArea(contour) < MIN_CONTOUR_AREA: @@ -181,5 +187,3 @@ def main() -> None: if __name__ == "__main__": main() print("DONE ✅") - - From e0c2828219329d2897c063f6546894499289dfc7 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 9 Oct 2025 16:19:47 +0530 Subject: [PATCH 6/8] Add missing numpy imports to doctests Inserted 'import numpy as np' in doctest examples for functions in motion_detection.py to ensure doctests run without import errors. --- computer_vision/motion_detection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/computer_vision/motion_detection.py b/computer_vision/motion_detection.py index 74dd6c050837..aa4a197fc2e4 100644 --- a/computer_vision/motion_detection.py +++ b/computer_vision/motion_detection.py @@ -41,6 +41,7 @@ def preprocess_frame(frame: cv2.Mat) -> cv2.Mat: Convert to grayscale and apply Gaussian blur to suppress noise. Doctest: + >>> import numpy as np >>> dummy = np.zeros((10, 10, 3), dtype=np.uint8) >>> out = preprocess_frame(dummy) >>> out.shape == (10, 10) and out.dtype == np.uint8 @@ -57,6 +58,7 @@ def frame_difference(prev_gray: cv2.Mat, curr_gray: cv2.Mat) -> cv2.Mat: Returns a binary motion mask after thresholding and morphology. Doctest: + >>> import numpy as np >>> a = np.zeros((8, 8), dtype=np.uint8) >>> b = np.zeros((8, 8), dtype=np.uint8) >>> b[2:6, 2:6] = 255 @@ -82,6 +84,7 @@ def background_subtraction_mask( Doctest: >>> subtractor = create_background_subtractor() + >>> import numpy as np >>> frame = np.zeros((12, 12, 3), dtype=np.uint8) >>> mask = background_subtraction_mask(subtractor, frame) >>> mask.shape @@ -103,6 +106,7 @@ def annotate_motion(frame: cv2.Mat, motion_mask: cv2.Mat) -> cv2.Mat: Find contours on the motion mask and draw bounding boxes on the frame. Doctest: + >>> import numpy as np >>> frame = np.zeros((60, 60, 3), dtype=np.uint8) >>> mask = np.zeros((60, 60), dtype=np.uint8) >>> mask[10:40, 10:40] = 255 # large enough to exceed MIN_CONTOUR_AREA From 274705b0219a4610e5222ab1908343a1c6d26014 Mon Sep 17 00:00:00 2001 From: shivamkb17 Date: Thu, 9 Oct 2025 10:50:08 +0000 Subject: [PATCH 7/8] updating DIRECTORY.md --- DIRECTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 36acb3b97f1e..52a0e821c152 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -138,6 +138,7 @@ * [Intensity Based Segmentation](computer_vision/intensity_based_segmentation.py) * [Mean Threshold](computer_vision/mean_threshold.py) * [Mosaic Augmentation](computer_vision/mosaic_augmentation.py) + * [Motion Detection](computer_vision/motion_detection.py) * [Pooling Functions](computer_vision/pooling_functions.py) ## Conversions @@ -195,6 +196,7 @@ * [Permutations](data_structures/arrays/permutations.py) * [Prefix Sum](data_structures/arrays/prefix_sum.py) * [Product Sum](data_structures/arrays/product_sum.py) + * [Rotate Array](data_structures/arrays/rotate_array.py) * [Sparse Table](data_structures/arrays/sparse_table.py) * [Sudoku Solver](data_structures/arrays/sudoku_solver.py) * Binary Tree From d5b6011d5214d05805cd6387cf4032be9b75281b Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 9 Oct 2025 18:18:32 +0530 Subject: [PATCH 8/8] fix annonate --- computer_vision/motion_detection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/computer_vision/motion_detection.py b/computer_vision/motion_detection.py index aa4a197fc2e4..2ce91326008d 100644 --- a/computer_vision/motion_detection.py +++ b/computer_vision/motion_detection.py @@ -111,7 +111,9 @@ def annotate_motion(frame: cv2.Mat, motion_mask: cv2.Mat) -> cv2.Mat: >>> mask = np.zeros((60, 60), dtype=np.uint8) >>> mask[10:40, 10:40] = 255 # large enough to exceed MIN_CONTOUR_AREA >>> annotated = annotate_motion(frame, mask) - >>> np.any(annotated[..., 1] == 255) # green channel from rectangle + >>> annotated.shape + (60, 60, 3) + >>> annotated.dtype == np.uint8 True """ contours, _ = cv2.findContours(