diff --git a/Application/Config.py b/Application/Config.py index 3f1c7c9..c6af5d5 100644 --- a/Application/Config.py +++ b/Application/Config.py @@ -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) diff --git a/Application/Layer.py b/Application/Layer.py index 5c700c7..ac3558f 100644 --- a/Application/Layer.py +++ b/Application/Layer.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Optional, Tuple -import cv2 -import imutils import numpy as np diff --git a/README.md b/README.md index d94100d..520f52c 100644 --- a/README.md +++ b/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 diff --git a/configs/README.md b/configs/README.md new file mode 100644 index 0000000..98b1698 --- /dev/null +++ b/configs/README.md @@ -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 diff --git a/configs/default.yaml b/configs/default.yaml new file mode 100644 index 0000000..8329cf6 --- /dev/null +++ b/configs/default.yaml @@ -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 diff --git a/configs/fast.yaml b/configs/fast.yaml new file mode 100644 index 0000000..4f8d79a --- /dev/null +++ b/configs/fast.yaml @@ -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 diff --git a/configs/high-sensitivity.yaml b/configs/high-sensitivity.yaml new file mode 100644 index 0000000..eead424 --- /dev/null +++ b/configs/high-sensitivity.yaml @@ -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 diff --git a/configs/low-sensitivity.yaml b/configs/low-sensitivity.yaml new file mode 100644 index 0000000..9fa1db0 --- /dev/null +++ b/configs/low-sensitivity.yaml @@ -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 diff --git a/requirements.txt b/requirements.txt index 8b0252c..c0d039e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index 143859e..c3e511f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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"]