Source code for config

"""Configuration management for the cat detection recording system.

This module handles loading and accessing configuration from YAML files.
It provides getter functions for all configuration values with fallback
to hardcoded defaults if the YAML file is missing or invalid.

Configuration is loaded from config.yaml (or a custom path) and can be
overridden by command-line arguments in main.py.

Example:
    Load configuration and get values::
        config.initialize_config(Path("config.yaml"))
        threshold = config.get_detection_threshold()
        output_dir = config.get_output_dir()
"""

import logging
import yaml
from pathlib import Path
from typing import Dict, Tuple, Any

logger = logging.getLogger(__name__)

# Fallback defaults if YAML file is missing or invalid
DEFAULT_DETECTION_THRESHOLD: float = 0.5
DEFAULT_MODEL_SIZE: int = 640  # Input size for YOLO model (640x640)

DEFAULT_CAMERA_DEVICE: str = "0"  # Camera index for picamera2 (0 = first camera)
DEFAULT_FPS: int = 10
DEFAULT_RESOLUTION: Tuple[int, int] = (640, 480)

DEFAULT_RECORD_SECONDS: int = 60
DEFAULT_OUTPUT_DIR: Path = Path("recordings")

MODEL_NAME: str = "yolov8n.pt"  # YOLOv8 nano model
CAT_CLASS_ID: int = 15  # COCO dataset class ID for "cat"


[docs] def load_config(config_path: Path) -> Dict[str, Any]: """ Load configuration from YAML file. Args: config_path: Path to YAML configuration file Returns: Dictionary containing configuration values, or empty dict if loading fails """ if not config_path.exists(): logger.warning(f"Config file not found: {config_path}. Using defaults.") return {} try: with open(config_path, "r") as f: config_data = yaml.safe_load(f) if config_data is None: logger.warning(f"Config file is empty: {config_path}. Using defaults.") return {} logger.info(f"Loaded configuration from {config_path}") return config_data except yaml.YAMLError as e: logger.error(f"Error parsing YAML config file {config_path}: {e}. Using defaults.") return {} except Exception as e: logger.error(f"Error loading config file {config_path}: {e}. Using defaults.") return {}
[docs] def get_config_value(config_data: Dict[str, Any], *keys: str, default: Any = None) -> Any: """ Safely get a nested config value. Args: config_data: Configuration dictionary keys: Nested keys to traverse (e.g., 'detection', 'threshold') default: Default value if key path doesn't exist Returns: Config value or default """ value = config_data for key in keys: if isinstance(value, dict): value = value.get(key) if value is None: return default else: return default return value if value is not None else default
# Global config dict (loaded by main.py) _config: Dict[str, Any] = {}
[docs] def initialize_config(config_path: Path = Path("config.yaml")) -> None: """ Initialize global configuration from YAML file. Args: config_path: Path to YAML configuration file """ global _config _config = load_config(config_path)
[docs] def get_detection_threshold() -> float: """Get detection threshold from config or default.""" return get_config_value(_config, "detection", "threshold", default=DEFAULT_DETECTION_THRESHOLD)
[docs] def get_model_size() -> int: """Get model size from config or default.""" return get_config_value(_config, "detection", "model_size", default=DEFAULT_MODEL_SIZE)
[docs] def get_camera_device() -> str: """Get camera device from config or default.""" return get_config_value(_config, "camera", "device", default=DEFAULT_CAMERA_DEVICE)
[docs] def get_fps() -> int: """Get FPS from config or default.""" return get_config_value(_config, "camera", "fps", default=DEFAULT_FPS)
[docs] def get_resolution() -> Tuple[int, int]: """Get resolution from config or default.""" res_config = get_config_value(_config, "camera", "resolution", default=None) if res_config and isinstance(res_config, dict): width = res_config.get("width", DEFAULT_RESOLUTION[0]) height = res_config.get("height", DEFAULT_RESOLUTION[1]) return (width, height) return DEFAULT_RESOLUTION
[docs] def get_record_seconds() -> int: """Get recording duration from config or default.""" value = get_config_value(_config, "recording", "duration_seconds", default=DEFAULT_RECORD_SECONDS) return int(value) # Ensure it's an integer
[docs] def get_output_dir() -> Path: """ Get output directory from config or default. Supports: - Relative paths: "recordings" (relative to current working directory) - Absolute paths: "/home/pi/cat_videos" - Home directory expansion: "~/cat_recordings" (expands to /home/user/cat_recordings) Returns: Path object to the output directory """ output_dir_str = get_config_value(_config, "recording", "output_dir", default=str(DEFAULT_OUTPUT_DIR)) # Expand ~ to home directory and resolve the path return Path(output_dir_str).expanduser().resolve()
[docs] def get_model_name() -> str: """Get model name from config or default.""" return get_config_value(_config, "model", "name", default=MODEL_NAME)
[docs] def get_cat_class_id() -> int: """Get cat class ID from config or default.""" return get_config_value(_config, "model", "cat_class_id", default=CAT_CLASS_ID)
[docs] def get_camera_controls() -> Dict[str, Any]: """ Get camera controls from config or empty dict. Returns: Dictionary of camera control settings (e.g., ExposureTime, AnalogueGain, AeEnable) Returns empty dict if no controls are configured """ controls = get_config_value(_config, "camera", "controls", default=None) if controls and isinstance(controls, dict): return controls return {}