VisionScout / configuration_manager.py
DawnC's picture
Upload 59 files
e6a18b7 verified
raw
history blame
15 kB
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")