A Python tool for capturing frames from RTSP cameras and generating time-lapse summary videos. Ideal for monitoring multiple cameras with activity-focused recording using motion detection.
IMPORTANT NOTE Majority of this project was created by Copilot, using my instructions.
- Multi-camera support: Monitor multiple RTSP cameras simultaneously
- Motion detection: Optional motion tracking to capture only frames with activity
- Automatic cleanup: Removes old frames based on configurable retention period (default: 24 hours)
- Video generation: Creates time-lapse videos from captured frames (default: hourly)
- Efficient storage: Organizes frames and videos in separate directories per camera
- Configurable: Easy YAML-based configuration
- Python 3.7+
- ffmpeg (for frame capture and video generation)
- OpenCV (for motion detection)
-
Install system dependencies:
# macOS brew install ffmpeg # Ubuntu/Debian sudo apt-get install ffmpeg
-
Install Python dependencies:
cd /Users/igor/Projects/HomeAssistaint/scripts/ha-config/cctv_summarizer pip3 install -r requirements.txtOr use a virtual environment (recommended):
python3 -m venv venv source venv/bin/activate pip install -r requirements.txt
Edit config.yaml to configure your cameras and settings:
config:
summary_duration: 24h # How long to keep captured frames
capture_interval: 1m # How often to capture frames
video_generation_interval: 1h # How often to generate videos (optional, defaults to 1h)
output_path: ../www/cctv_summaries/ # Where to store frames and videos
video_format: mp4 # Output video format
resolution: 720p # Output video resolution
cameras:
front:
name: Front Door Camera
track_changes: true # Enable motion detection (only save frames with activity)
url: rtsp://user:pass@192.168.1.15:554/stream
park:
name: Parking Lot
url: rtsp://user:pass@192.168.1.16:554/stream- summary_duration: How long to retain captured frames (e.g.,
24h,2d,168h) - capture_interval: Time between frame captures (e.g.,
30s,1m,5m) - video_generation_interval: Time between video generations (e.g.,
1h,6h,24h) - output_path: Directory for storing frames and videos
- video_format: Video output format (
mp4,avi, etc.) - resolution: Video height in pixels (e.g.,
720p,1080p) - video_fps: Frames per second for generated videos (default:
25) - each captured image becomes one frame - log_level: Logging verbosity -
DEBUG,INFO,WARNING,ERROR, orCRITICAL(default:INFO) - average_filter: Number of frames to capture and average together (default:
1= no averaging). When set to > 1, multiple frames are captured immediately and blended into one averaged image, reducing temporal noise like rain, snow, or flickering - track_changes: Enable motion detection for a camera (applies filtering during video generation - all frames are still captured)
Motion detection filters frames during video generation, not during capture. All frames are saved, but only frames with detected motion are included in the generated videos. This allows you to tune parameters without losing data.
config:
# Global defaults
motion_threshold: 25 # Pixel difference threshold (0-255, higher = less sensitive)
min_motion_area: 500 # Minimum contour area in pixels (higher = only larger movements)
blur_kernel: 5 # Gaussian blur to reduce noise (0 to disable, use odd numbers: 3,5,7,9)
average_filter: 1 # Number of frames to average (1 = disabled, 3-5 recommended for noisy conditions)
cameras:
front:
track_changes: true
# Override defaults for this camera
motion_threshold: 30
min_motion_area: 800
blur_kernel: 7
average_filter: 3 # Capture and average 3 frames to reduce rain/snow/flickerParameters explained:
- motion_threshold: Pixel brightness difference threshold (0-255). Higher values make it less sensitive to subtle changes
- min_motion_area: Minimum area in pixels for a contour to be considered significant motion. Increase to ignore small movements
- blur_kernel: Apply Gaussian blur before comparison to reduce camera sensor noise. Set to 0 to disable, or use odd numbers (3, 5, 7, 9). Higher values = more smoothing but may miss fine details
- average_filter: Number of frames to capture and blend together (1 = no averaging). When set to 2 or higher, the system captures multiple frames immediately (without delay) and averages them into a single image. This significantly reduces temporal noise such as rain, snow, flickering lights, or sensor noise. Recommended values: 3-5 for moderate noise, 7-10 for heavy rain/snow. Higher values increase capture time
How it works:
- All frames are captured and saved unconditionally
- During video generation, if
track_changes: true, motion detection analyzes each frame - Only frames with detected motion are included in the video
- Original frames remain on disk until cleaned up by
summary_durationsetting
Tuning tips:
- If too many similar frames are in videos: increase
motion_thresholdormin_motion_area - If important motion is missed: decrease
motion_thresholdormin_motion_area - If camera has noisy sensor (many small contours): increase
blur_kernelto 7 or 9 - If temporal noise (rain, snow, flickering): enable
average_filterwith values 3-5 - If heavy rain/snow: increase
average_filterto 7-10 (note: increases capture time) - Use
--test-changes --save-debug-imagesto visualize what the algorithm sees
The average_filter parameter helps eliminate temporal noise from captured frames by averaging multiple exposures:
When to use:
- Rain or snow visible in frames
- Flickering lights or reflections
- Camera sensor noise (grainy images in low light)
- Transient objects you want to filter out (birds, insects)
How it works:
- When
average_filteris set to a value > 1 (e.g., 3), the system captures that many frames immediately in succession - All frames are loaded into memory and averaged pixel-by-pixel using NumPy
- The resulting averaged frame is saved as the final capture
- Temporary frames are discarded
Performance considerations:
- Each capture takes proportionally longer (average_filter=3 means 3× the capture time)
- Requires more CPU and memory during capture (frames loaded into memory)
- Does NOT affect storage (only the final averaged frame is saved)
Recommended values:
1- No averaging (default, fastest)3- Light temporal noise reduction (good balance)5- Moderate noise reduction (recommended for rain/snow)7-10- Heavy noise reduction (use for severe conditions)
Example configuration:
config:
average_filter: 3 # Global default for all cameras
cameras:
outdoor:
average_filter: 5 # This camera needs more noise reduction
indoor:
average_filter: 1 # Indoor camera doesn't need averagingThe tool runs in a continuous loop, capturing frames at the specified interval:
python3 cctv_summarizer.pyOr use the provided shell script:
./run_summarizer.shTest capture from a specific camera:
python3 cctv_summarizer.py --test-capture frontGenerate videos from existing frames without starting the capture loop:
# Generate videos for all cameras
python3 cctv_summarizer.py --generate-videos
# Generate video for a specific camera
python3 cctv_summarizer.py --generate-videos frontTest motion detection on existing frames to debug and tune thresholds:
# Test all cameras with detailed output
python3 cctv_summarizer.py --test-changes
# Test specific camera
python3 cctv_summarizer.py --test-changes front
# Save debug visualization images
python3 cctv_summarizer.py --test-changes front --save-debug-images
# Test only a range of frames (e.g., frames 10-20)
python3 cctv_summarizer.py --test-changes front --frame-range 10:20 --save-debug-imagesThe test mode shows detailed statistics for each frame comparison:
- Pixel difference statistics (mean, max)
- Percentage of changed pixels
- Number of contours detected
- Areas of significant contours
- Decision (KEEP or DISCARD)
When using --save-debug-images, four visualization images are created for each frame:
*_1_diff.jpg: Difference between frames (amplified for visibility)*_2_thresh.jpg: Thresholded binary image showing changed pixels*_3_all_contours.jpg: All detected contours in green*_4_significant.jpg: Only significant contours in red with statistics overlay
Debug images are saved to output_path/debug/camera_id/.
Specify a different configuration file:
python3 cctv_summarizer.py --config /path/to/config.yaml- Edit
cctv_summarizer.serviceto match your paths and user - Copy the service file:
sudo cp cctv_summarizer.service /etc/systemd/system/
- Enable and start the service:
sudo systemctl enable cctv_summarizer sudo systemctl start cctv_summarizer - Check status:
sudo systemctl status cctv_summarizer
Create a plist file in ~/Library/LaunchAgents/:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.homeassistant.cctv-summarizer</string>
<key>ProgramArguments</key>
<array>
<string>/Users/igor/Projects/HomeAssistaint/scripts/ha-config/cctv_summarizer/run_summarizer.sh</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/igor/Projects/HomeAssistaint/scripts/ha-config/cctv_summarizer</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/tmp/cctv-summarizer.err</string>
<key>StandardOutPath</key>
<string>/tmp/cctv-summarizer.out</string>
</dict>
</plist>Load the service:
launchctl load ~/Library/LaunchAgents/com.homeassistant.cctv-summarizer.plistAdd to crontab to run at startup:
@reboot cd /Users/igor/Projects/HomeAssistaint/scripts/ha-config/cctv_summarizer && ./run_summarizer.shwww/cctv_summaries/
├── frames/
│ ├── front/
│ │ ├── 20231115_120000.jpg
│ │ ├── 20231115_120100.jpg
│ │ └── ...
│ ├── park/
│ └── kotel/
└── videos/
├── front/
│ ├── 20231115_120000.mp4
│ └── ...
├── park/
└── kotel/
- Frame Capture: Every
capture_intervalseconds, the tool captures a frame from each camera using ffmpeg - all frames are saved unconditionally - Storage: Frames are saved with timestamps in camera-specific subdirectories
- Cleanup: Old frames beyond
summary_durationare automatically deleted - Video Generation: At the specified interval (default: hourly), a time-lapse video is generated:
- If
track_changesis disabled: all captured frames are included - If
track_changesis enabled: frames are analyzed for motion, only frames with significant changes are included in the video
- If
- Video Cleanup: Old videos are kept at one per day to save space
This design has several advantages:
- No data loss: All captured frames are preserved
- Tune anytime: Adjust motion detection parameters and regenerate videos from existing frames
- Debug easily: Use
--test-changesto see how different parameters would affect video generation - Flexibility: Disable motion detection later without losing historical data
If frame capture fails:
- Verify RTSP URL is correct
- Check network connectivity to camera
- Test RTSP stream with:
ffplay -rtsp_transport tcp "rtsp://..." - Ensure camera supports RTSP and is not limited by max connections
Use the test mode to debug and tune parameters:
# Test and see statistics
python3 cctv_summarizer.py --test-changes front
# Generate debug images to visualize
python3 cctv_summarizer.py --test-changes front --save-debug-images --frame-range 0:10Then adjust parameters in config.yaml:
motion_threshold: Pixel difference threshold (0-255, higher = less sensitive)min_motion_area: Minimum contour area in pixels (higher = only larger movements)blur_kernel: Gaussian blur size to reduce noise (0 to disable, or 3,5,7,9)
See Motion Detection Parameters section above for detailed tuning guidance.
- Reduce
capture_intervalto capture less frequently - Enable
track_changesfor motion-based capture - Reduce
summary_durationto retain frames for less time - Lower video
resolution
The tool generates HTML files with embedded video players for each camera, which can be directly integrated into Home Assistant dashboards using iframe cards.
Home Assistant's image element doesn't support MP4 video playback. The solution is to use iframe cards that load HTML files containing video players.
The tool automatically generates HTML files for each camera:
camera.html- Latest video for the camera (e.g.,front.html,park.html)camera-history.html- All previous day videos in chronological order (e.g.,front-history.html)- Videos are stored in
output_path/videos/camera_id/(default:../ha-config/www/cctv_summaries/videos/)
The history file shows one video per day (the most recent one generated that day), with videos displayed newest first.
- Each HTML file contains a video player pointing to the latest generated video
- Videos are stored in
output_path/videos/camera_id/(default:../ha-config/www/cctv_summaries/videos/)
Configure in config.yaml:
config:
iframe_template: iframe.html # Template file for HTML generation
create_latest_link: false # Optional: create latest.mp4 symlink (may cause caching issues)Use the iframe card to embed the video player in your dashboard:
Latest video:
type: iframe
url: /local/cctv_summaries/videos/front.html
aspect_ratio: 16:9Video history (shows all previous days):
type: iframe
url: /local/cctv_summaries/videos/front-history.html
aspect_ratio: 16:9Note the above static iframe may still cause cacheing issues, you then will see "white" iframe content. Here is a solution with dynamic URL generation, which makes Home Assistant to regenerate url on each page load, and therefore caching is not a problem:
type: custom:config-template-card
entities:
- sensor.date_time
card:
type: iframe
url: ${'/local/cctv_summaries/videos/park.html?' +new Date().getTime()}
refresh_interval: 300Or create a grid of camera feeds:
type: horizontal-stack
cards:
- type: iframe
url: /local/cctv_summaries/videos/front.html
aspect_ratio: 16:9
- type: iframe
url: /local/cctv_summaries/videos/park.html
aspect_ratio: 16:9
- type: iframe
url: /local/cctv_summaries/videos/kotel.html
aspect_ratio: 16:9- When a new video is generated, the tool updates the corresponding HTML file
- The HTML file contains a relative path to the latest video (e.g.,
front/20251115_163003.mp4) - Home Assistant's iframe card loads the HTML file, which plays the video
- Each video generation updates the HTML file with the new video path
- Browsers cache-bust automatically since the video filename includes a timestamp
Edit iframe.html template to customize the video player:
<video autoplay muted loop playsinline style="width:100%;height:100%;object-fit:cover;">
<source src="$RELPATH" type="video/mp4">
</video>Available placeholders:
$RELPATHor{{video_path}}- relative path to the video file
If you enable create_latest_link: true in config, a latest.mp4 symlink is created for each camera. However, this may cause browser caching issues where old videos continue to display.
Direct URLs (if enabled):
http://your-ha-instance/local/cctv_summaries/videos/front/latest.mp4http://your-ha-instance/local/cctv_summaries/videos/park/latest.mp4
Timestamped videos are always available:
http://your-ha-instance/local/cctv_summaries/videos/front/20251115_163003.mp4
MIT