VisionScout / configuration_manager.py
DawnC's picture
Upload 59 files
e6a18b7 verified
from typing import Dict, Any, List, Tuple, Optional, Union
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class FeatureThresholds:
"""Configuration class for feature extraction thresholds."""
dark_pixel_threshold: float = 50.0
bright_pixel_threshold: float = 220.0
sky_blue_hue_min: float = 95.0
sky_blue_hue_max: float = 135.0
sky_blue_sat_min: float = 40.0
sky_blue_val_min: float = 90.0
gray_sat_max: float = 70.0
gray_val_min: float = 60.0
gray_val_max: float = 220.0
light_source_abs_thresh: float = 220.0
@dataclass
class IndoorOutdoorThresholds:
"""Configuration class for indoor/outdoor classification thresholds."""
sky_blue_dominance_thresh: float = 0.18
sky_brightness_ratio_thresh: float = 1.25
openness_top_thresh: float = 0.68
sky_texture_complexity_thresh: float = 0.35
ceiling_likelihood_thresh: float = 0.4
boundary_clarity_thresh: float = 0.38
brightness_uniformity_thresh_indoor: float = 0.6
brightness_uniformity_thresh_outdoor: float = 0.40
many_bright_spots_thresh: int = 6
dim_scene_for_spots_thresh: float = 115.0
home_pattern_thresh_strong: float = 2.0
home_pattern_thresh_moderate: float = 1.0
warm_indoor_max_brightness_thresh: float = 135.0
aerial_top_dark_ratio_thresh: float = 0.9
aerial_top_complex_thresh: float = 0.60
aerial_min_avg_brightness_thresh: float = 65.0
@dataclass
class LightingThresholds:
"""Configuration class for lighting condition analysis thresholds."""
outdoor_night_thresh_brightness: float = 80.0
outdoor_night_lights_thresh: int = 2
outdoor_dusk_dawn_thresh_brightness: float = 130.0
outdoor_dusk_dawn_color_thresh: float = 0.10
outdoor_day_bright_thresh: float = 140.0
outdoor_day_blue_thresh: float = 0.05
outdoor_day_cloudy_thresh: float = 120.0
outdoor_day_gray_thresh: float = 0.18
indoor_bright_thresh: float = 130.0
indoor_moderate_thresh: float = 95.0
commercial_min_brightness_thresh: float = 105.0
commercial_min_spots_thresh: int = 3
stadium_min_spots_thresh: int = 6
neon_yellow_orange_thresh: float = 0.12
neon_bright_spots_thresh: int = 4
neon_avg_saturation_thresh: float = 60.0
@dataclass
class WeightingFactors:
"""Configuration class for feature weighting factors."""
# Sky/Openness weights (negative values push towards outdoor)
sky_blue_dominance_w: float = 3.5
sky_brightness_ratio_w: float = 3.0
openness_top_w: float = 2.8
sky_texture_w: float = 2.0
# Ceiling/Enclosure weights (positive values push towards indoor)
ceiling_likelihood_w: float = 1.5
boundary_clarity_w: float = 1.2
# Brightness weights
brightness_uniformity_w: float = 0.6
brightness_non_uniformity_outdoor_w: float = 1.0
brightness_non_uniformity_indoor_penalty_w: float = 0.1
# Light source weights
circular_lights_w: float = 1.2
indoor_light_score_w: float = 0.8
many_bright_spots_indoor_w: float = 0.3
# Color atmosphere weights
warm_atmosphere_indoor_w: float = 0.15
# Environment pattern weights
home_env_strong_w: float = 1.5
home_env_moderate_w: float = 0.7
# Structural pattern weights
aerial_street_w: float = 2.5
places365_outdoor_scene_w: float = 4.0
places365_indoor_scene_w: float = 3.0
places365_attribute_w: float = 1.5
@dataclass
class OverrideFactors:
"""Configuration class for override and reduction factors."""
sky_override_factor_ceiling: float = 0.1
sky_override_factor_boundary: float = 0.2
sky_override_factor_uniformity: float = 0.15
sky_override_factor_lights: float = 0.05
sky_override_factor_p365_indoor_decision: float = 0.3
aerial_enclosure_reduction_factor: float = 0.75
ceiling_sky_override_factor: float = 0.1
p365_outdoor_reduces_enclosure_factor: float = 0.3
p365_indoor_boosts_ceiling_factor: float = 1.5
@dataclass
class ColorRanges:
"""Configuration class for color range definitions."""
warm_hue_ranges: List[Tuple[float, float]] = field(
default_factory=lambda: [(0, 50), (330, 360)]
)
cool_hue_ranges: List[Tuple[float, float]] = field(
default_factory=lambda: [(90, 270)]
)
@dataclass
class AlgorithmParameters:
"""Configuration class for algorithm-specific parameters."""
indoor_score_sigmoid_scale: float = 0.3
indoor_decision_threshold: float = 0.5
places365_high_confidence_thresh: float = 0.75
places365_moderate_confidence_thresh: float = 0.5
places365_attribute_confidence_thresh: float = 0.6
include_diagnostics: bool = True
class ConfigurationManager:
"""
這主要是管理光線分析的參數,會有很多不同情況, 做parameters配置
This class provides type-safe access to all configuration parameters,
supports loading from external files, and includes validation mechanisms.
"""
def __init__(self, config_path: Optional[Union[str, Path]] = None):
"""
Initialize the configuration manager.
Args:
config_path: Optional path to external configuration file.
If None, uses default configuration.
"""
self._feature_thresholds = FeatureThresholds()
self._indoor_outdoor_thresholds = IndoorOutdoorThresholds()
self._lighting_thresholds = LightingThresholds()
self._weighting_factors = WeightingFactors()
self._override_factors = OverrideFactors()
self._color_ranges = ColorRanges()
self._algorithm_parameters = AlgorithmParameters()
if config_path is not None:
self.load_from_file(config_path)
@property
def feature_thresholds(self) -> FeatureThresholds:
"""Get feature extraction thresholds."""
return self._feature_thresholds
@property
def indoor_outdoor_thresholds(self) -> IndoorOutdoorThresholds:
"""Get indoor/outdoor classification thresholds."""
return self._indoor_outdoor_thresholds
@property
def lighting_thresholds(self) -> LightingThresholds:
"""Get lighting condition analysis thresholds."""
return self._lighting_thresholds
@property
def weighting_factors(self) -> WeightingFactors:
"""Get feature weighting factors."""
return self._weighting_factors
@property
def override_factors(self) -> OverrideFactors:
"""Get override and reduction factors."""
return self._override_factors
@property
def color_ranges(self) -> ColorRanges:
"""Get color range definitions."""
return self._color_ranges
@property
def algorithm_parameters(self) -> AlgorithmParameters:
"""Get algorithm-specific parameters."""
return self._algorithm_parameters
def get_legacy_config_dict(self) -> Dict[str, Any]:
"""
Generate legacy configuration dictionary for backward compatibility.
Returns:
Dictionary containing all configuration parameters in the original format.
"""
config_dict = {}
# Feature thresholds
for field_name, field_value in self._feature_thresholds.__dict__.items():
config_dict[field_name] = field_value
# Indoor/outdoor thresholds
for field_name, field_value in self._indoor_outdoor_thresholds.__dict__.items():
config_dict[field_name] = field_value
# Lighting thresholds
for field_name, field_value in self._lighting_thresholds.__dict__.items():
config_dict[field_name] = field_value
# Override factors
for field_name, field_value in self._override_factors.__dict__.items():
config_dict[field_name] = field_value
# Color ranges
for field_name, field_value in self._color_ranges.__dict__.items():
config_dict[field_name] = field_value
# Algorithm parameters
for field_name, field_value in self._algorithm_parameters.__dict__.items():
config_dict[field_name] = field_value
# Weighting factors - stored under 'indoor_outdoor_weights' key
config_dict["indoor_outdoor_weights"] = self._weighting_factors.__dict__.copy()
return config_dict
def load_from_file(self, config_path: Union[str, Path]) -> None:
"""
Load configuration from external JSON file.
Args:
config_path: Path to the configuration file.
Raises:
FileNotFoundError: If the configuration file doesn't exist.
ValueError: If the configuration file contains invalid data.
"""
config_path = Path(config_path)
if not config_path.exists():
raise FileNotFoundError(f"Configuration file not found: {config_path}")
try:
with open(config_path, 'r', encoding='utf-8') as file:
config_data = json.load(file)
self._update_from_dict(config_data)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in configuration file: {e}")
except Exception as e:
raise ValueError(f"Error loading configuration: {e}")
def save_to_file(self, config_path: Union[str, Path]) -> None:
"""
Save current configuration to JSON file.
Args:
config_path: Path where to save the configuration file.
"""
config_path = Path(config_path)
config_path.parent.mkdir(parents=True, exist_ok=True)
config_dict = self.get_legacy_config_dict()
with open(config_path, 'w', encoding='utf-8') as file:
json.dump(config_dict, file, indent=2, ensure_ascii=False)
def _update_from_dict(self, config_data: Dict[str, Any]) -> None:
"""
Update configuration from dictionary data.
Args:
config_data: Dictionary containing configuration parameters.
"""
# Update feature thresholds
self._update_dataclass_from_dict(self._feature_thresholds, config_data)
# Update indoor/outdoor thresholds
self._update_dataclass_from_dict(self._indoor_outdoor_thresholds, config_data)
# Update lighting thresholds
self._update_dataclass_from_dict(self._lighting_thresholds, config_data)
# Update override factors
self._update_dataclass_from_dict(self._override_factors, config_data)
# Update color ranges
self._update_dataclass_from_dict(self._color_ranges, config_data)
# Update algorithm parameters
self._update_dataclass_from_dict(self._algorithm_parameters, config_data)
# Update weighting factors from nested dictionary
if "indoor_outdoor_weights" in config_data:
self._update_dataclass_from_dict(
self._weighting_factors,
config_data["indoor_outdoor_weights"]
)
def _update_dataclass_from_dict(self, dataclass_instance: object, data_dict: Dict[str, Any]) -> None:
"""
Update dataclass instance fields from dictionary.
Args:
dataclass_instance: The dataclass instance to update.
data_dict: Dictionary containing the update values.
"""
for field_name, field_value in data_dict.items():
if hasattr(dataclass_instance, field_name):
# Type validation could be added here
setattr(dataclass_instance, field_name, field_value)
def validate_configuration(self) -> List[str]:
"""
Validate the current configuration for logical consistency.
Returns:
List of validation error messages. Empty list if configuration is valid.
"""
errors = []
# Validate threshold ranges
ft = self._feature_thresholds
if ft.dark_pixel_threshold >= ft.bright_pixel_threshold:
errors.append("Dark pixel threshold must be less than bright pixel threshold")
if ft.sky_blue_hue_min >= ft.sky_blue_hue_max:
errors.append("Sky blue hue min must be less than sky blue hue max")
if ft.gray_val_min >= ft.gray_val_max:
errors.append("Gray value min must be less than gray value max")
# Validate probability thresholds
ap = self._algorithm_parameters
if not (0.0 <= ap.indoor_decision_threshold <= 1.0):
errors.append("Indoor decision threshold must be between 0 and 1")
if not (0.0 <= ap.places365_high_confidence_thresh <= 1.0):
errors.append("Places365 high confidence threshold must be between 0 and 1")
# Validate color ranges
for warm_range in self._color_ranges.warm_hue_ranges:
if warm_range[0] >= warm_range[1]:
errors.append(f"Invalid warm hue range: {warm_range}")
for cool_range in self._color_ranges.cool_hue_ranges:
if cool_range[0] >= cool_range[1]:
errors.append(f"Invalid cool hue range: {cool_range}")
return errors
def get_threshold_value(self, threshold_name: str) -> Any:
"""
Get a specific threshold value by name.
Args:
threshold_name: Name of the threshold parameter.
Returns:
The threshold value.
Raises:
AttributeError: If the threshold name doesn't exist.
"""
# Search through all configuration sections
for config_section in [
self._feature_thresholds,
self._indoor_outdoor_thresholds,
self._lighting_thresholds,
self._override_factors,
self._algorithm_parameters
]:
if hasattr(config_section, threshold_name):
return getattr(config_section, threshold_name)
# Check weighting factors
if hasattr(self._weighting_factors, threshold_name):
return getattr(self._weighting_factors, threshold_name)
raise AttributeError(f"Threshold '{threshold_name}' not found")
def update_threshold(self, threshold_name: str, value: Any) -> None:
"""
Update a specific threshold value.
Args:
threshold_name: Name of the threshold parameter.
value: New value for the threshold.
Raises:
AttributeError: If the threshold name doesn't exist.
"""
# Search through all configuration sections
for config_section in [
self._feature_thresholds,
self._indoor_outdoor_thresholds,
self._lighting_thresholds,
self._override_factors,
self._algorithm_parameters,
self._weighting_factors
]:
if hasattr(config_section, threshold_name):
setattr(config_section, threshold_name, value)
return
raise AttributeError(f"Threshold '{threshold_name}' not found")