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 json
|
||||||
import os
|
import os
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
YAML_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
YAML_AVAILABLE = False
|
||||||
|
|
||||||
from Application.Logger import get_logger
|
from Application.Logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
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 = {
|
c = {
|
||||||
"min_area": 300,
|
"min_area": 300,
|
||||||
"max_area": 900000,
|
"max_area": 900000,
|
||||||
|
|
@ -30,27 +46,109 @@ class Config:
|
||||||
Initialize configuration from file or use defaults.
|
Initialize configuration from file or use defaults.
|
||||||
|
|
||||||
Args:
|
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):
|
if config_path and os.path.isfile(config_path):
|
||||||
logger.info(f"Using supplied configuration at {config_path}")
|
logger.info(f"Using supplied configuration at {config_path}")
|
||||||
try:
|
try:
|
||||||
with open(config_path) as file:
|
self.c = self._load_config_file(config_path)
|
||||||
self.c = json.load(file)
|
except Exception as e:
|
||||||
except (json.JSONDecodeError, IOError) as e:
|
|
||||||
logger.error(f"Failed to parse config file: {e}")
|
logger.error(f"Failed to parse config file: {e}")
|
||||||
logger.warning("Falling back to default configuration")
|
logger.warning("Falling back to default configuration")
|
||||||
else:
|
else:
|
||||||
logger.info("Using default configuration")
|
logger.info("Using default configuration")
|
||||||
|
|
||||||
|
# Apply environment variable overrides
|
||||||
|
self._apply_env_overrides()
|
||||||
|
|
||||||
logger.info("Current Configuration:")
|
logger.info("Current Configuration:")
|
||||||
for key, value in self.c.items():
|
for key, value in self.c.items():
|
||||||
logger.info(f" {key}: {value}")
|
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:
|
def __getitem__(self, key: str) -> Any:
|
||||||
|
"""Get configuration value by key."""
|
||||||
if key not in self.c:
|
if key not in self.c:
|
||||||
return None
|
return None
|
||||||
return self.c[key]
|
return self.c[key]
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: Any) -> None:
|
def __setitem__(self, key: str, value: Any) -> None:
|
||||||
|
"""Set configuration value by key."""
|
||||||
self.c[key] = value
|
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
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import cv2
|
|
||||||
import imutils
|
|
||||||
import numpy as np
|
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
|
## ⚙️ 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
|
### Example YAML Configuration
|
||||||
{
|
|
||||||
"min_area": 300,
|
```yaml
|
||||||
"max_area": 900000,
|
# Detection sensitivity
|
||||||
"threshold": 7,
|
min_area: 300 # Minimum contour area in pixels
|
||||||
"resizeWidth": 700,
|
max_area: 900000 # Maximum contour area in pixels
|
||||||
"maxLayerLength": 5000,
|
threshold: 7 # Movement detection sensitivity (lower = more sensitive)
|
||||||
"minLayerLength": 40,
|
|
||||||
"tolerance": 20,
|
# Processing parameters
|
||||||
"ttolerance": 50,
|
resizeWidth: 700 # Processing width (smaller = faster but less accurate)
|
||||||
"videoBufferLength": 250,
|
videoBufferLength: 250 # Frame buffer size
|
||||||
"LayersPerContour": 220,
|
|
||||||
"avgNum": 10
|
# 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
|
### 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
|
# Visualization
|
||||||
matplotlib>=3.3.0
|
matplotlib>=3.3.0
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
pyyaml>=6.0
|
||||||
|
|
||||||
# Optional: Machine Learning (for classification features)
|
# Optional: Machine Learning (for classification features)
|
||||||
# Uncomment if you need TensorFlow support:
|
# Uncomment if you need TensorFlow support:
|
||||||
# tensorflow>=2.10.0,<3.0.0
|
# tensorflow>=2.10.0,<3.0.0
|
||||||
|
|
@ -18,7 +18,7 @@ class TestConfig:
|
||||||
assert config["max_area"] == 900000
|
assert config["max_area"] == 900000
|
||||||
assert config["threshold"] == 7
|
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 loading config from a JSON file."""
|
||||||
test_config = {"min_area": 500, "max_area": 1000000, "threshold": 10}
|
test_config = {"min_area": 500, "max_area": 1000000, "threshold": 10}
|
||||||
|
|
||||||
|
|
@ -34,6 +34,25 @@ class TestConfig:
|
||||||
finally:
|
finally:
|
||||||
os.unlink(temp_path)
|
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):
|
def test_config_with_invalid_file(self):
|
||||||
"""Test that default config is used when file doesn't exist."""
|
"""Test that default config is used when file doesn't exist."""
|
||||||
config = Config("/nonexistent/path/config.json")
|
config = Config("/nonexistent/path/config.json")
|
||||||
|
|
@ -63,3 +82,29 @@ class TestConfig:
|
||||||
assert config["min_area"] == 300
|
assert config["min_area"] == 300
|
||||||
finally:
|
finally:
|
||||||
os.unlink(temp_path)
|
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