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:
parent
6d671afdad
commit
4d1a51119d
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import imutils
|
||||
import numpy as np
|
||||
|
||||
|
||||
|
|
|
|||
65
README.md
65
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue