Spaces:
Running
on
Zero
Running
on
Zero
| import numpy as np | |
| import cv2 | |
| import logging | |
| import traceback | |
| from typing import Dict, Any, Optional | |
| from configuration_manager import ConfigurationManager | |
| from feature_extractor import FeatureExtractor | |
| from indoor_outdoor_classifier import IndoorOutdoorClassifier | |
| from lighting_condition_analyzer import LightingConditionAnalyzer | |
| class LightingAnalyzer: | |
| """ | |
| Comprehensive lighting analysis system facade that coordinates feature extraction, | |
| indoor/outdoor classification, and lighting condition determination. | |
| 此class是一個總窗口,主要匯總各式光線分析相關的class | |
| This facade class maintains the original interface while internally delegating | |
| work to specialized components for improved maintainability and modularity. | |
| """ | |
| def __init__(self, config: Optional[Dict[str, Any]] = None): | |
| """ | |
| Initialize the lighting analyzer with configuration. | |
| Args: | |
| config: Optional configuration dictionary. If None, uses default configuration. | |
| """ | |
| self.logger = self._setup_logger() | |
| try: | |
| # Initialize configuration manager | |
| self.config_manager = ConfigurationManager() | |
| # Override default configuration if provided | |
| if config is not None: | |
| self._update_configuration(config) | |
| # Initialize specialized components | |
| self.feature_extractor = FeatureExtractor(self.config_manager) | |
| self.indoor_outdoor_classifier = IndoorOutdoorClassifier(self.config_manager) | |
| self.lighting_condition_analyzer = LightingConditionAnalyzer(self.config_manager) | |
| # Legacy configuration access for backward compatibility | |
| self.config = self.config_manager.get_legacy_config_dict() | |
| self.logger.info("LightingAnalyzer initialized successfully") | |
| except Exception as e: | |
| self.logger.error(f"Error initializing LightingAnalyzer: {str(e)}") | |
| self.logger.error(f"Traceback: {traceback.format_exc()}") | |
| raise | |
| def _setup_logger(self) -> logging.Logger: | |
| """Set up logger for lighting analysis operations.""" | |
| logger = logging.getLogger(f"{__name__}.LightingAnalyzer") | |
| if not logger.handlers: | |
| handler = logging.StreamHandler() | |
| formatter = logging.Formatter( | |
| '%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
| ) | |
| handler.setFormatter(formatter) | |
| logger.addHandler(handler) | |
| logger.setLevel(logging.INFO) | |
| return logger | |
| def _update_configuration(self, config: Dict[str, Any]) -> None: | |
| """ | |
| Update configuration manager with provided configuration dictionary. | |
| Args: | |
| config: Configuration dictionary to update existing configuration. | |
| """ | |
| try: | |
| # Update configuration through the manager's internal method | |
| self.config_manager._update_from_dict(config) | |
| self.logger.debug("Configuration updated successfully") | |
| except Exception as e: | |
| self.logger.warning(f"Error updating configuration: {str(e)}") | |
| self.logger.warning("Continuing with default configuration") | |
| def analyze(self, image, places365_info: Optional[Dict] = None) -> Dict[str, Any]: | |
| """ | |
| Analyze lighting conditions of an image. | |
| This is the main entry point that maintains compatibility with the original interface | |
| while leveraging the new modular architecture internally. | |
| Args: | |
| image: Input image (numpy array or PIL Image). | |
| places365_info: Optional Places365 classification information containing | |
| scene type, confidence, attributes, and indoor/outdoor classification. | |
| Returns: | |
| Dictionary containing comprehensive lighting analysis results including: | |
| - time_of_day: Specific lighting condition classification | |
| - confidence: Confidence score for the classification | |
| - is_indoor: Boolean indicating indoor/outdoor classification | |
| - indoor_probability: Probability score for indoor classification | |
| - brightness: Brightness analysis metrics | |
| - color_info: Color characteristic analysis | |
| - texture_info: Texture and gradient analysis | |
| - structure_info: Structural feature analysis | |
| - diagnostics: Detailed diagnostic information (if enabled) | |
| """ | |
| try: | |
| self.logger.debug("Starting comprehensive lighting analysis") | |
| # Step 1: Validate and preprocess input image | |
| processed_image = self._preprocess_image(image) | |
| if processed_image is None: | |
| return self._get_error_result("Invalid image input") | |
| # Step 2: Extract comprehensive features | |
| self.logger.debug("Extracting image features") | |
| features = self.feature_extractor.extract_features(processed_image) | |
| if not features or "avg_brightness" not in features: | |
| return self._get_error_result("Feature extraction failed") | |
| # Step 3: Classify indoor/outdoor with Places365 integration | |
| self.logger.debug("Performing indoor/outdoor classification") | |
| indoor_outdoor_result = self.indoor_outdoor_classifier.classify( | |
| features, places365_info | |
| ) | |
| is_indoor = indoor_outdoor_result["is_indoor"] | |
| indoor_probability = indoor_outdoor_result["indoor_probability"] | |
| # Step 4: Determine specific lighting conditions | |
| self.logger.debug(f"Analyzing lighting conditions for {'indoor' if is_indoor else 'outdoor'} scene") | |
| lighting_result = self.lighting_condition_analyzer.analyze_lighting_conditions( | |
| features, is_indoor, places365_info | |
| ) | |
| # Step 5: Consolidate comprehensive results | |
| result = self._consolidate_analysis_results( | |
| lighting_result, indoor_outdoor_result, features | |
| ) | |
| self.logger.info(f"Analysis complete: {result['time_of_day']} " | |
| f"({'indoor' if result['is_indoor'] else 'outdoor'}) " | |
| f"confidence: {result['confidence']:.3f}") | |
| return result | |
| except Exception as e: | |
| self.logger.error(f"Error in lighting analysis: {str(e)}") | |
| self.logger.error(f"Traceback: {traceback.format_exc()}") | |
| return self._get_error_result(str(e)) | |
| def _preprocess_image(self, image) -> Optional[np.ndarray]: | |
| """ | |
| Preprocess input image to ensure consistent format for analysis. | |
| Args: | |
| image: Input image in various possible formats. | |
| Returns: | |
| Preprocessed image as RGB numpy array, or None if preprocessing failed. | |
| """ | |
| try: | |
| # Convert to numpy array if needed | |
| if not isinstance(image, np.ndarray): | |
| image_np = np.array(image) | |
| else: | |
| image_np = image.copy() | |
| # Validate basic image properties | |
| if len(image_np.shape) < 2: | |
| self.logger.error("Image must have at least 2 dimensions") | |
| return None | |
| height, width = image_np.shape[:2] | |
| if height == 0 or width == 0: | |
| self.logger.error(f"Invalid image dimensions: {height}x{width}") | |
| return None | |
| # Handle different color formats and convert to RGB | |
| if len(image_np.shape) == 2: | |
| # 灰階 to RGB | |
| image_rgb = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB) | |
| elif image_np.shape[2] == 3: | |
| # Handle BGR vs RGB | |
| if not isinstance(image, np.ndarray): | |
| # PIL images are typically RGB | |
| image_rgb = image_np | |
| else: | |
| # OpenCV arrays are typically BGR, convert to RGB | |
| image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB) | |
| elif image_np.shape[2] == 4: | |
| # RGBA to RGB | |
| if not isinstance(image, np.ndarray): | |
| # PIL RGBA to RGB | |
| image_rgb = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB) | |
| else: | |
| # OpenCV BGRA to RGB | |
| image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGRA2RGB) | |
| else: | |
| self.logger.error(f"Unsupported image format with shape: {image_np.shape}") | |
| return None | |
| # Ensure uint8 data type | |
| if image_rgb.dtype != np.uint8: | |
| if image_rgb.dtype in [np.float32, np.float64]: | |
| # Assume normalized float values | |
| if image_rgb.max() <= 1.0: | |
| image_rgb = (image_rgb * 255).astype(np.uint8) | |
| else: | |
| image_rgb = image_rgb.astype(np.uint8) | |
| else: | |
| image_rgb = image_rgb.astype(np.uint8) | |
| self.logger.debug(f"Preprocessed image: {image_rgb.shape}, dtype: {image_rgb.dtype}") | |
| return image_rgb | |
| except Exception as e: | |
| self.logger.error(f"Error preprocessing image: {str(e)}") | |
| return None | |
| def _consolidate_analysis_results(self, lighting_result: Dict[str, Any], | |
| indoor_outdoor_result: Dict[str, Any], | |
| features: Dict[str, Any]) -> Dict[str, Any]: | |
| """ | |
| Consolidate results from all analysis components into final output format. | |
| Args: | |
| lighting_result: Results from lighting condition analysis. | |
| indoor_outdoor_result: Results from indoor/outdoor classification. | |
| features: Extracted image features. | |
| Returns: | |
| Consolidated analysis results in the expected output format. | |
| """ | |
| # Extract core results | |
| time_of_day = lighting_result["time_of_day"] | |
| confidence = lighting_result["confidence"] | |
| is_indoor = indoor_outdoor_result["is_indoor"] | |
| indoor_probability = indoor_outdoor_result["indoor_probability"] | |
| # Organize brightness information | |
| brightness_info = { | |
| "average": float(features.get("avg_brightness", 0.0)), | |
| "std_dev": float(features.get("brightness_std", 0.0)), | |
| "dark_ratio": float(features.get("dark_pixel_ratio", 0.0)), | |
| "bright_ratio": float(features.get("bright_pixel_ratio", 0.0)) | |
| } | |
| # Organize color information | |
| color_info = { | |
| "blue_ratio": float(features.get("blue_ratio", 0.0)), | |
| "sky_like_blue_ratio": float(features.get("sky_like_blue_ratio", 0.0)), | |
| "yellow_orange_ratio": float(features.get("yellow_orange_ratio", 0.0)), | |
| "gray_ratio": float(features.get("gray_ratio", 0.0)), | |
| "avg_saturation": float(features.get("avg_saturation", 0.0)), | |
| "sky_region_brightness_ratio": float(features.get("sky_region_brightness_ratio", 1.0)), | |
| "sky_region_saturation": float(features.get("sky_region_saturation", 0.0)), | |
| "sky_region_blue_dominance": float(features.get("sky_region_blue_dominance", 0.0)), | |
| "color_atmosphere": features.get("color_atmosphere", "neutral"), | |
| "warm_ratio": float(features.get("warm_ratio", 0.0)), | |
| "cool_ratio": float(features.get("cool_ratio", 0.0)) | |
| } | |
| # Organize texture information | |
| texture_info = { | |
| "gradient_ratio_vertical_horizontal": float(features.get("gradient_ratio_vertical_horizontal", 0.0)), | |
| "top_region_texture_complexity": float(features.get("top_region_texture_complexity", 0.0)), | |
| "shadow_clarity_score": float(features.get("shadow_clarity_score", 0.5)) | |
| } | |
| # Organize structure information | |
| structure_info = { | |
| "ceiling_likelihood": float(features.get("ceiling_likelihood", 0.0)), | |
| "boundary_clarity": float(features.get("boundary_clarity", 0.0)), | |
| "openness_top_edge": float(features.get("openness_top_edge", 0.5)) | |
| } | |
| # Compile final result | |
| result = { | |
| "time_of_day": time_of_day, | |
| "confidence": float(confidence), | |
| "is_indoor": is_indoor, | |
| "indoor_probability": float(indoor_probability), | |
| "brightness": brightness_info, | |
| "color_info": color_info, | |
| "texture_info": texture_info, | |
| "structure_info": structure_info | |
| } | |
| # Add diagnostic information if enabled | |
| if self.config_manager.algorithm_parameters.include_diagnostics: | |
| diagnostics = {} | |
| # Combine diagnostics from all components | |
| if "diagnostics" in lighting_result: | |
| diagnostics["lighting_diagnostics"] = lighting_result["diagnostics"] | |
| if "diagnostics" in indoor_outdoor_result: | |
| diagnostics["indoor_outdoor_diagnostics"] = indoor_outdoor_result["diagnostics"] | |
| if "feature_contributions" in indoor_outdoor_result: | |
| diagnostics["feature_contributions"] = indoor_outdoor_result["feature_contributions"] | |
| result["diagnostics"] = diagnostics | |
| return result | |
| def _get_error_result(self, error_message: str) -> Dict[str, Any]: | |
| """ | |
| Generate standardized error result format. | |
| Args: | |
| error_message: Description of the error that occurred. | |
| Returns: | |
| Dictionary containing error result with safe default values. | |
| """ | |
| return { | |
| "time_of_day": "unknown", | |
| "confidence": 0.0, | |
| "is_indoor": False, | |
| "indoor_probability": 0.5, | |
| "brightness": { | |
| "average": 100.0, | |
| "std_dev": 50.0, | |
| "dark_ratio": 0.0, | |
| "bright_ratio": 0.0 | |
| }, | |
| "color_info": { | |
| "blue_ratio": 0.0, | |
| "sky_like_blue_ratio": 0.0, | |
| "yellow_orange_ratio": 0.0, | |
| "gray_ratio": 0.0, | |
| "avg_saturation": 100.0, | |
| "sky_region_brightness_ratio": 1.0, | |
| "sky_region_saturation": 0.0, | |
| "sky_region_blue_dominance": 0.0, | |
| "color_atmosphere": "neutral", | |
| "warm_ratio": 0.0, | |
| "cool_ratio": 0.0 | |
| }, | |
| "texture_info": { | |
| "gradient_ratio_vertical_horizontal": 1.0, | |
| "top_region_texture_complexity": 0.5, | |
| "shadow_clarity_score": 0.5 | |
| }, | |
| "structure_info": { | |
| "ceiling_likelihood": 0.0, | |
| "boundary_clarity": 0.0, | |
| "openness_top_edge": 0.5 | |
| }, | |
| "error": error_message | |
| } | |
| def get_configuration(self) -> Dict[str, Any]: | |
| """ | |
| Get current configuration as dictionary for backward compatibility. | |
| Returns: | |
| Dictionary containing all current configuration parameters. | |
| """ | |
| return self.config_manager.get_legacy_config_dict() | |
| def update_configuration(self, config_updates: Dict[str, Any]) -> None: | |
| """ | |
| Update configuration parameters. | |
| Args: | |
| config_updates: Dictionary containing configuration parameters to update. | |
| """ | |
| try: | |
| self.config_manager._update_from_dict(config_updates) | |
| # Update legacy config reference | |
| self.config = self.config_manager.get_legacy_config_dict() | |
| self.logger.info("Configuration updated successfully") | |
| except Exception as e: | |
| self.logger.error(f"Error updating configuration: {str(e)}") | |
| raise | |
| def validate_configuration(self) -> bool: | |
| """ | |
| Validate current configuration for logical consistency. | |
| Returns: | |
| True if configuration is valid, False otherwise. | |
| """ | |
| try: | |
| validation_errors = self.config_manager.validate_configuration() | |
| if validation_errors: | |
| self.logger.error("Configuration validation failed:") | |
| for error in validation_errors: | |
| self.logger.error(f" - {error}") | |
| return False | |
| self.logger.info("Configuration validation passed") | |
| return True | |
| except Exception as e: | |
| self.logger.error(f"Error during configuration validation: {str(e)}") | |
| return False | |
| def save_configuration(self, filepath: str) -> None: | |
| """ | |
| Save current configuration to file. | |
| Args: | |
| filepath: Path where to save the configuration file. | |
| """ | |
| try: | |
| self.config_manager.save_to_file(filepath) | |
| self.logger.info(f"Configuration saved to {filepath}") | |
| except Exception as e: | |
| self.logger.error(f"Error saving configuration: {str(e)}") | |
| raise | |
| def load_configuration(self, filepath: str) -> None: | |
| """ | |
| Load configuration from file. | |
| Args: | |
| filepath: Path to the configuration file to load. | |
| """ | |
| try: | |
| self.config_manager.load_from_file(filepath) | |
| # Update legacy config reference | |
| self.config = self.config_manager.get_legacy_config_dict() | |
| self.logger.info(f"Configuration loaded from {filepath}") | |
| except Exception as e: | |
| self.logger.error(f"Error loading configuration: {str(e)}") | |
| raise | |