Phase 3: Add YAML config support, environment overrides, and config profiles

Co-authored-by: Askill <16598120+Askill@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-02-15 17:21:55 +00:00
parent 6d671afdad
commit 4d1a51119d
10 changed files with 373 additions and 22 deletions

View File

@ -1,13 +1,29 @@
"""Configuration management for Video Summary application."""
import json
import os
from typing import Any, Optional
try:
import yaml
YAML_AVAILABLE = True
except ImportError:
YAML_AVAILABLE = False
from Application.Logger import get_logger
logger = get_logger(__name__)
class Config:
"""
Configuration management supporting JSON and YAML formats.
Supports loading configuration from JSON or YAML files, with fallback
to default values. Also supports environment variable overrides.
"""
c = {
"min_area": 300,
"max_area": 900000,
@ -30,27 +46,109 @@ class Config:
Initialize configuration from file or use defaults.
Args:
config_path: Path to JSON configuration file. If None or invalid, uses defaults.
config_path: Path to JSON or YAML configuration file.
If None or invalid, uses defaults.
Supports .json, .yaml, and .yml extensions.
"""
if config_path and os.path.isfile(config_path):
logger.info(f"Using supplied configuration at {config_path}")
try:
with open(config_path) as file:
self.c = json.load(file)
except (json.JSONDecodeError, IOError) as e:
self.c = self._load_config_file(config_path)
except Exception as e:
logger.error(f"Failed to parse config file: {e}")
logger.warning("Falling back to default configuration")
else:
logger.info("Using default configuration")
# Apply environment variable overrides
self._apply_env_overrides()
logger.info("Current Configuration:")
for key, value in self.c.items():
logger.info(f" {key}: {value}")
def _load_config_file(self, config_path: str) -> dict:
"""
Load configuration from JSON or YAML file.
Args:
config_path: Path to configuration file
Returns:
Dictionary with configuration values
Raises:
ValueError: If file format is not supported
"""
ext = os.path.splitext(config_path)[1].lower()
with open(config_path, "r") as file:
if ext == ".json":
return json.load(file)
elif ext in [".yaml", ".yml"]:
if not YAML_AVAILABLE:
raise ValueError("PyYAML is not installed. Install with: pip install pyyaml")
return yaml.safe_load(file)
else:
# Try JSON first, then YAML
content = file.read()
file.seek(0)
try:
return json.loads(content)
except json.JSONDecodeError:
if YAML_AVAILABLE:
file.seek(0)
return yaml.safe_load(file)
else:
raise ValueError(f"Unsupported config file format: {ext}")
def _apply_env_overrides(self):
"""Apply environment variable overrides to configuration."""
env_prefix = "VIDEO_SUMMARY_"
for key in self.c.keys():
env_key = f"{env_prefix}{key.upper()}"
env_value = os.environ.get(env_key)
if env_value is not None:
# Try to convert to appropriate type
try:
# Try integer
self.c[key] = int(env_value)
except ValueError:
try:
# Try float
self.c[key] = float(env_value)
except ValueError:
# Use as string
self.c[key] = env_value
logger.info(f"Environment override: {key} = {self.c[key]}")
def __getitem__(self, key: str) -> Any:
"""Get configuration value by key."""
if key not in self.c:
return None
return self.c[key]
def __setitem__(self, key: str, value: Any) -> None:
"""Set configuration value by key."""
self.c[key] = value
def save(self, output_path: str):
"""
Save current configuration to file.
Args:
output_path: Path to save configuration file.
Format is determined by extension (.json, .yaml, .yml)
"""
ext = os.path.splitext(output_path)[1].lower()
with open(output_path, "w") as file:
if ext == ".json":
json.dump(self.c, file, indent=2)
elif ext in [".yaml", ".yml"]:
if not YAML_AVAILABLE:
raise ValueError("PyYAML is not installed. Install with: pip install pyyaml")
yaml.dump(self.c, file, default_flow_style=False)
else:
# Default to JSON
json.dump(self.c, file, indent=2)

View File

@ -2,8 +2,6 @@
from typing import Any, Dict, List, Optional, Tuple
import cv2
import imutils
import numpy as np

View File

@ -77,22 +77,57 @@ The heatmap shows areas of activity throughout the video, with brighter regions
## ⚙️ Configuration
Create a JSON configuration file to customize processing parameters:
Video-Summary supports both JSON and YAML configuration files. YAML is recommended for its readability and support for comments.
```json
{
"min_area": 300,
"max_area": 900000,
"threshold": 7,
"resizeWidth": 700,
"maxLayerLength": 5000,
"minLayerLength": 40,
"tolerance": 20,
"ttolerance": 50,
"videoBufferLength": 250,
"LayersPerContour": 220,
"avgNum": 10
}
### Example YAML Configuration
```yaml
# Detection sensitivity
min_area: 300 # Minimum contour area in pixels
max_area: 900000 # Maximum contour area in pixels
threshold: 7 # Movement detection sensitivity (lower = more sensitive)
# Processing parameters
resizeWidth: 700 # Processing width (smaller = faster but less accurate)
videoBufferLength: 250 # Frame buffer size
# Layer management
maxLayerLength: 5000 # Maximum frames per layer
minLayerLength: 40 # Minimum frames per layer
tolerance: 20 # Pixel distance for grouping contours
ttolerance: 50 # Frame gap tolerance
# Advanced
LayersPerContour: 220 # Max layers per contour
avgNum: 10 # Frame averaging (higher = less noise, slower)
```
### Pre-configured Profiles
Use the provided configuration profiles in the `configs/` directory:
```bash
# Default balanced settings
python main.py video.mp4 output configs/default.yaml
# High sensitivity - detect smaller movements
python main.py video.mp4 output configs/high-sensitivity.yaml
# Low sensitivity - outdoor scenes, reduce noise
python main.py video.mp4 output configs/low-sensitivity.yaml
# Fast processing - optimized for speed
python main.py video.mp4 output configs/fast.yaml
```
### Environment Variable Overrides
Override any configuration parameter using environment variables:
```bash
export VIDEO_SUMMARY_THRESHOLD=10
export VIDEO_SUMMARY_MIN_AREA=500
python main.py video.mp4 output
```
### Configuration Parameters

81
configs/README.md Normal file
View File

@ -0,0 +1,81 @@
# Configuration Profiles
This directory contains pre-configured YAML files for common use cases.
## Available Profiles
### default.yaml
Balanced settings suitable for most indoor surveillance scenarios.
- Good balance between sensitivity and noise reduction
- Moderate processing speed
- **Use when**: Processing typical indoor surveillance footage
### high-sensitivity.yaml
Optimized for detecting smaller movements and objects.
- Lower detection thresholds
- Shorter minimum layer lengths
- Less frame averaging
- **Use when**: You need to catch subtle movements or smaller objects
- **Use when**: Indoor scenes with good lighting
### low-sensitivity.yaml
Reduced sensitivity to avoid false positives from environmental noise.
- Higher detection thresholds
- Longer minimum layer lengths
- More frame averaging
- **Use when**: Outdoor scenes with weather changes (clouds, wind)
- **Use when**: You want to focus only on significant movements
- **Use when**: Reducing false positives is more important than catching everything
### fast.yaml
Optimized for processing speed at the cost of some accuracy.
- Lower resolution processing (480p instead of 700p)
- Smaller buffers
- Minimal averaging
- **Use when**: Quick preview or testing
- **Use when**: Processing very long videos
- **Use when**: Running on limited hardware
## Usage
```bash
# Use a specific profile
python main.py input_video.mp4 output_dir configs/default.yaml
# Override specific settings with environment variables
export VIDEO_SUMMARY_THRESHOLD=10
python main.py input_video.mp4 output_dir configs/default.yaml
```
## Creating Custom Profiles
Copy any of these files and modify parameters to create your own profile:
```bash
cp configs/default.yaml configs/my-custom.yaml
# Edit my-custom.yaml with your preferred settings
python main.py input_video.mp4 output_dir configs/my-custom.yaml
```
## Parameter Tuning Guide
### Increasing Sensitivity (detect more movement)
- Decrease `threshold` (e.g., 4-5)
- Decrease `min_area` (e.g., 100-200)
- Decrease `minLayerLength` (e.g., 20-30)
### Decreasing Sensitivity (reduce noise)
- Increase `threshold` (e.g., 10-15)
- Increase `min_area` (e.g., 500-1000)
- Increase `minLayerLength` (e.g., 60-100)
- Increase `avgNum` (e.g., 15-20)
### Improving Performance
- Decrease `resizeWidth` (e.g., 480-600)
- Decrease `videoBufferLength` (e.g., 100-150)
- Decrease `avgNum` (e.g., 5)
### Handling Outdoor Scenes
- Increase `avgNum` (e.g., 15-20) to smooth out clouds/leaves
- Increase `threshold` (e.g., 10-12)
- Increase `ttolerance` (e.g., 80-100) for wind-affected objects

28
configs/default.yaml Normal file
View File

@ -0,0 +1,28 @@
# Default Configuration for Video Summary
# This is a YAML configuration file with explanatory comments
# Contour detection parameters
min_area: 300 # Minimum contour area in pixels (smaller contours ignored)
max_area: 900000 # Maximum contour area in pixels (larger contours ignored)
threshold: 7 # Luminance difference threshold for movement detection (lower = more sensitive)
# Video processing
resizeWidth: 700 # Video width for processing (reduces memory usage and speeds up processing)
videoBufferLength: 250 # Number of frames to buffer in memory
# Layer management
maxLayerLength: 5000 # Maximum length of a layer in frames
minLayerLength: 40 # Minimum length of a layer in frames (shorter layers discarded)
tolerance: 20 # Maximum distance in pixels between contours to group into same layer
ttolerance: 50 # Number of frames a movement can be absent before creating a new layer
LayersPerContour: 220 # Maximum number of layers a single contour can belong to
# Advanced options
avgNum: 10 # Number of frames to average before calculating difference
# Higher values reduce noise but increase computation time
# Useful for outdoor scenes with clouds, wind, etc.
# Paths (typically overridden by CLI arguments)
inputPath: null
outputPath: null
maxLength: null

21
configs/fast.yaml Normal file
View File

@ -0,0 +1,21 @@
# Fast Processing Configuration
# Optimized for speed over quality
min_area: 400
max_area: 900000
threshold: 8
resizeWidth: 480 # Lower resolution = faster processing
videoBufferLength: 100 # Smaller buffer = less memory
maxLayerLength: 3000
minLayerLength: 30
tolerance: 25
ttolerance: 60
LayersPerContour: 150 # Fewer layers per contour
avgNum: 5 # Minimal averaging
inputPath: null
outputPath: null
maxLength: null

View File

@ -0,0 +1,21 @@
# High Sensitivity Configuration
# Use for detecting smaller movements or objects
min_area: 100 # Lower threshold to detect smaller objects
max_area: 900000
threshold: 4 # Lower threshold = more sensitive to changes
resizeWidth: 700
videoBufferLength: 250
maxLayerLength: 5000
minLayerLength: 20 # Allow shorter layers
tolerance: 30 # More tolerant of position changes
ttolerance: 40 # Shorter gap tolerance
LayersPerContour: 220
avgNum: 5 # Less averaging for faster response
inputPath: null
outputPath: null
maxLength: null

View File

@ -0,0 +1,21 @@
# Low Sensitivity Configuration
# Use for outdoor scenes with weather changes, or to focus on larger movements
min_area: 500 # Higher threshold ignores small noise
max_area: 900000
threshold: 12 # Higher threshold = less sensitive, reduces false positives
resizeWidth: 700
videoBufferLength: 250
maxLayerLength: 5000
minLayerLength: 60 # Require longer sustained movement
tolerance: 15 # Stricter position matching
ttolerance: 80 # Longer gap tolerance
LayersPerContour: 220
avgNum: 20 # More averaging to smooth out noise (clouds, leaves, etc.)
inputPath: null
outputPath: null
maxLength: null

View File

@ -10,6 +10,9 @@ imageio-ffmpeg>=0.4.0
# Visualization
matplotlib>=3.3.0
# Configuration
pyyaml>=6.0
# Optional: Machine Learning (for classification features)
# Uncomment if you need TensorFlow support:
# tensorflow>=2.10.0,<3.0.0

View File

@ -18,7 +18,7 @@ class TestConfig:
assert config["max_area"] == 900000
assert config["threshold"] == 7
def test_load_config_from_file(self):
def test_load_config_from_json_file(self):
"""Test loading config from a JSON file."""
test_config = {"min_area": 500, "max_area": 1000000, "threshold": 10}
@ -34,6 +34,25 @@ class TestConfig:
finally:
os.unlink(temp_path)
def test_load_config_from_yaml_file(self):
"""Test loading config from a YAML file."""
test_config_yaml = """
min_area: 600
max_area: 2000000
threshold: 15
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
f.write(test_config_yaml)
temp_path = f.name
try:
config = Config(temp_path)
assert config["min_area"] == 600
assert config["max_area"] == 2000000
assert config["threshold"] == 15
finally:
os.unlink(temp_path)
def test_config_with_invalid_file(self):
"""Test that default config is used when file doesn't exist."""
config = Config("/nonexistent/path/config.json")
@ -63,3 +82,29 @@ class TestConfig:
assert config["min_area"] == 300
finally:
os.unlink(temp_path)
def test_config_save_json(self):
"""Test saving config to JSON file."""
config = Config(None)
config["test_value"] = 123
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
temp_path = f.name
try:
config.save(temp_path)
# Load it back and verify
with open(temp_path, "r") as f:
loaded = json.load(f)
assert loaded["test_value"] == 123
finally:
os.unlink(temp_path)
def test_env_override(self):
"""Test environment variable override."""
os.environ["VIDEO_SUMMARY_MIN_AREA"] = "999"
try:
config = Config(None)
assert config["min_area"] == 999
finally:
del os.environ["VIDEO_SUMMARY_MIN_AREA"]