Spaces:
Running
on
Zero
Running
on
Zero
| import numpy as np | |
| import logging | |
| import traceback | |
| from typing import Dict, Any, Optional, List, Tuple | |
| from configuration_manager import ConfigurationManager | |
| class LightingConditionAnalyzer: | |
| """ | |
| Determines specific lighting conditions and time of day based on scene analysis. | |
| 此class 會判斷一些光線的特定場景 | |
| This class analyzes lighting characteristics including natural and artificial illumination, | |
| color temperature patterns, and temporal indicators to classify scenes into specific | |
| lighting categories such as day clear, night with lights, indoor artificial, etc. | |
| """ | |
| def __init__(self, config_manager: ConfigurationManager): | |
| """ | |
| Initialize the lighting condition analyzer. | |
| Args: | |
| config_manager: Configuration manager instance for accessing thresholds and parameters. | |
| """ | |
| self.config_manager = config_manager | |
| self.logger = self._setup_logger() | |
| # Internal threshold constants for Places365 analysis | |
| self.P365_ATTRIBUTE_CONF_THRESHOLD = 0.60 | |
| self.P365_SCENE_MODERATE_CONF_THRESHOLD = 0.45 | |
| self.P365_SCENE_HIGH_CONF_THRESHOLD = 0.70 | |
| # Scene type keyword definitions | |
| self.P365_OUTDOOR_SCENE_KEYWORDS = [ | |
| "street", "road", "highway", "park", "beach", "mountain", "forest", "field", | |
| "outdoor", "sky", "coast", "courtyard", "square", "plaza", "bridge", | |
| "parking", "playground", "stadium", "construction", "river", "ocean", "desert", | |
| "garden", "trail", "natural_landmark", "airport_outdoor", "train_station_outdoor", | |
| "bus_station_outdoor", "intersection", "crosswalk", "sidewalk", "pathway" | |
| ] | |
| self.P365_INDOOR_RESTAURANT_KEYWORDS = [ | |
| "restaurant", "bar", "cafe", "dining_room", "pub", "bistro", "eatery" | |
| ] | |
| def _setup_logger(self) -> logging.Logger: | |
| """Set up logger for lighting condition analysis operations.""" | |
| logger = logging.getLogger(f"{__name__}.LightingConditionAnalyzer") | |
| 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 analyze_lighting_conditions(self, features: Dict[str, Any], is_indoor: bool, | |
| places365_info: Optional[Dict] = None) -> Dict[str, Any]: | |
| """ | |
| Determine specific lighting conditions based on features and scene context. | |
| Args: | |
| features: Dictionary containing extracted image features. | |
| is_indoor: Boolean indicating whether the scene is indoor (from previous classification). | |
| places365_info: Optional Places365 classification information. | |
| Returns: | |
| Dictionary containing lighting analysis results including time_of_day, confidence, | |
| and diagnostic information. | |
| """ | |
| try: | |
| self.logger.debug(f"Starting lighting analysis for {'indoor' if is_indoor else 'outdoor'} scene") | |
| # Initialize analysis results | |
| time_of_day = "unknown" | |
| confidence = 0.5 | |
| diagnostics = {} | |
| # Extract Places365 context | |
| p365_context = self._extract_places365_context(places365_info, diagnostics) | |
| # Priority 1: Use Places365 attributes if highly confident | |
| attribute_result = self._analyze_places365_attributes( | |
| p365_context, is_indoor, features, diagnostics | |
| ) | |
| if attribute_result["determined"] and attribute_result["confidence"] >= 0.75: | |
| self.logger.debug(f"High-confidence Places365 attribute determination: {attribute_result['time_of_day']}") | |
| return { | |
| "time_of_day": attribute_result["time_of_day"], | |
| "confidence": attribute_result["confidence"], | |
| "diagnostics": diagnostics | |
| } | |
| # Priority 2: Visual feature analysis with Places365 scene context | |
| visual_result = self._analyze_visual_features( | |
| features, is_indoor, p365_context, diagnostics | |
| ) | |
| time_of_day = visual_result["time_of_day"] | |
| confidence = visual_result["confidence"] | |
| # Combine with attribute result if it exists but wasn't decisive | |
| if attribute_result["determined"]: | |
| combined_result = self._combine_attribute_and_visual_results( | |
| attribute_result, visual_result, diagnostics | |
| ) | |
| time_of_day = combined_result["time_of_day"] | |
| confidence = combined_result["confidence"] | |
| # Priority 3: Special lighting refinement (neon, sodium vapor) | |
| refined_result = self._apply_special_lighting_refinement( | |
| time_of_day, confidence, features, is_indoor, p365_context, diagnostics | |
| ) | |
| time_of_day = refined_result["time_of_day"] | |
| confidence = refined_result["confidence"] | |
| # Final confidence clamping | |
| confidence = min(0.95, max(0.50, confidence)) | |
| # Record final results | |
| diagnostics["final_lighting_time_of_day"] = time_of_day | |
| diagnostics["final_lighting_confidence"] = round(confidence, 3) | |
| self.logger.debug(f"Lighting analysis complete: {time_of_day} (confidence: {confidence:.3f})") | |
| return { | |
| "time_of_day": time_of_day, | |
| "confidence": confidence, | |
| "diagnostics": diagnostics | |
| } | |
| except Exception as e: | |
| self.logger.error(f"Error in lighting condition analysis: {str(e)}") | |
| self.logger.error(f"Traceback: {traceback.format_exc()}") | |
| return self._get_default_lighting_result() | |
| def _extract_places365_context(self, places365_info: Optional[Dict], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Extract and validate Places365 context information for lighting analysis.""" | |
| context = { | |
| "mapped_scene": "unknown", | |
| "attributes": [], | |
| "confidence": 0.0 | |
| } | |
| if places365_info: | |
| context["mapped_scene"] = places365_info.get('mapped_scene_type', 'unknown').lower() | |
| context["attributes"] = [attr.lower() for attr in places365_info.get('attributes', [])] | |
| context["confidence"] = places365_info.get('confidence', 0.0) | |
| diagnostics["p365_context_for_lighting"] = ( | |
| f"P365 Scene: {context['mapped_scene']}, Attrs: {context['attributes']}, " | |
| f"Conf: {context['confidence']:.2f}" | |
| ) | |
| return context | |
| def _analyze_places365_attributes(self, p365_context: Dict[str, Any], is_indoor: bool, | |
| features: Dict[str, Any], diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Analyze Places365 attributes for lighting condition determination.""" | |
| if (not p365_context["attributes"] or | |
| p365_context["confidence"] <= self.P365_ATTRIBUTE_CONF_THRESHOLD): | |
| return {"determined": False, "time_of_day": "unknown", "confidence": 0.5} | |
| confidence = p365_context["confidence"] | |
| attributes = p365_context["attributes"] | |
| mapped_scene = p365_context["mapped_scene"] | |
| # Outdoor attribute analysis | |
| if not is_indoor: | |
| outdoor_result = self._analyze_outdoor_attributes( | |
| attributes, mapped_scene, confidence, diagnostics | |
| ) | |
| if outdoor_result["determined"]: | |
| return outdoor_result | |
| # Indoor attribute analysis | |
| if is_indoor: | |
| indoor_result = self._analyze_indoor_attributes( | |
| attributes, mapped_scene, features, confidence, diagnostics | |
| ) | |
| if indoor_result["determined"]: | |
| return indoor_result | |
| return {"determined": False, "time_of_day": "unknown", "confidence": 0.5} | |
| def _analyze_outdoor_attributes(self, attributes: List[str], mapped_scene: str, | |
| confidence: float, diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Analyze Places365 attributes for outdoor lighting conditions.""" | |
| base_confidence_boost = (confidence - self.P365_ATTRIBUTE_CONF_THRESHOLD) * 0.25 | |
| if "sunny" in attributes or "clear sky" in attributes: | |
| final_confidence = 0.85 + base_confidence_boost | |
| diagnostics["reason"] = "P365 attribute: sunny/clear sky (Outdoor)." | |
| return { | |
| "determined": True, | |
| "time_of_day": "day_clear", | |
| "confidence": final_confidence | |
| } | |
| elif "nighttime" in attributes or "night" in attributes: | |
| if ("artificial lighting" in attributes or "man-made lighting" in attributes or | |
| any(kw in mapped_scene for kw in ["street", "city", "road", "urban", "downtown"])): | |
| final_confidence = 0.82 + base_confidence_boost * 0.8 | |
| diagnostics["reason"] = "P365 attribute: nighttime with artificial/street lights (Outdoor)." | |
| return { | |
| "determined": True, | |
| "time_of_day": "night_with_lights", | |
| "confidence": final_confidence | |
| } | |
| else: | |
| final_confidence = 0.78 + base_confidence_boost * 0.8 | |
| diagnostics["reason"] = "P365 attribute: nighttime, dark (Outdoor)." | |
| return { | |
| "determined": True, | |
| "time_of_day": "night_dark", | |
| "confidence": final_confidence | |
| } | |
| elif "cloudy" in attributes or "overcast" in attributes: | |
| final_confidence = 0.80 + base_confidence_boost | |
| diagnostics["reason"] = "P365 attribute: cloudy/overcast (Outdoor)." | |
| return { | |
| "determined": True, | |
| "time_of_day": "day_cloudy_overcast", | |
| "confidence": final_confidence | |
| } | |
| return {"determined": False, "time_of_day": "unknown", "confidence": 0.5} | |
| def _analyze_indoor_attributes(self, attributes: List[str], mapped_scene: str, | |
| features: Dict[str, Any], confidence: float, | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Analyze Places365 attributes for indoor lighting conditions.""" | |
| base_confidence_boost = (confidence - self.P365_ATTRIBUTE_CONF_THRESHOLD) * 0.20 | |
| avg_brightness = features.get("avg_brightness", 128.0) | |
| if "artificial lighting" in attributes or "man-made lighting" in attributes: | |
| base_indoor_conf = 0.70 + base_confidence_boost | |
| thresholds = self.config_manager.lighting_thresholds | |
| if avg_brightness > thresholds.indoor_bright_thresh: | |
| time_of_day = "indoor_bright_artificial" | |
| final_confidence = base_indoor_conf + 0.10 | |
| elif avg_brightness > thresholds.indoor_moderate_thresh: | |
| time_of_day = "indoor_moderate_artificial" | |
| final_confidence = base_indoor_conf | |
| else: | |
| time_of_day = "indoor_dim_artificial" | |
| final_confidence = base_indoor_conf - 0.05 | |
| diagnostics["reason"] = ( | |
| f"P365 attribute: artificial lighting (Indoor), " | |
| f"brightness based category: {time_of_day}." | |
| ) | |
| return { | |
| "determined": True, | |
| "time_of_day": time_of_day, | |
| "confidence": final_confidence | |
| } | |
| elif "natural lighting" in attributes: | |
| is_applicable_scene = ( | |
| self._check_home_environment_pattern(features) or | |
| any(kw in mapped_scene for kw in ["living_room", "bedroom", "sunroom"]) | |
| ) | |
| if is_applicable_scene: | |
| final_confidence = 0.80 + base_confidence_boost | |
| diagnostics["reason"] = "P365 attribute: natural lighting in residential/applicable indoor scene." | |
| return { | |
| "determined": True, | |
| "time_of_day": "indoor_residential_natural", | |
| "confidence": final_confidence | |
| } | |
| return {"determined": False, "time_of_day": "unknown", "confidence": 0.5} | |
| def _analyze_visual_features(self, features: Dict[str, Any], is_indoor: bool, | |
| p365_context: Dict[str, Any], diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Analyze visual features for lighting condition determination.""" | |
| if is_indoor: | |
| return self._analyze_indoor_visual_features(features, p365_context, diagnostics) | |
| else: | |
| return self._analyze_outdoor_visual_features(features, p365_context, diagnostics) | |
| def _analyze_indoor_visual_features(self, features: Dict[str, Any], p365_context: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Analyze visual features for indoor lighting conditions.""" | |
| avg_brightness = features.get("avg_brightness", 128.0) | |
| thresholds = self.config_manager.lighting_thresholds | |
| # Extract relevant features | |
| sky_blue_in_sky_region = features.get("sky_region_blue_dominance", 0.0) | |
| sky_region_is_brighter = features.get("sky_region_brightness_ratio", 1.0) > 1.05 | |
| is_likely_home_environment = self._check_home_environment_pattern(features) | |
| # Lighting and structural features | |
| circular_lights = features.get("circular_light_count", 0) | |
| bright_spots_overall = features.get("bright_spot_count", 0) | |
| brightness_uniformity = features.get("brightness_uniformity", 0.0) | |
| warm_ratio = features.get("warm_ratio", 0.0) | |
| # Natural light hints calculation | |
| natural_light_hints = 0.0 | |
| if sky_blue_in_sky_region > 0.05 and sky_region_is_brighter: | |
| natural_light_hints += 1.0 | |
| if brightness_uniformity > 0.65 and features.get("brightness_std", 100.0) < 70: | |
| natural_light_hints += 1.0 | |
| if warm_ratio > 0.15 and avg_brightness > 110: | |
| natural_light_hints += 0.5 | |
| # Designer lighting detection | |
| is_designer_lit = ( | |
| (circular_lights > 0 or bright_spots_overall > 2) and | |
| brightness_uniformity > 0.6 and warm_ratio > 0.2 and avg_brightness > 90 | |
| ) | |
| # Brightness-based classification | |
| if avg_brightness > thresholds.indoor_bright_thresh: | |
| return self._classify_bright_indoor( | |
| features, natural_light_hints, is_designer_lit, is_likely_home_environment, | |
| p365_context, diagnostics | |
| ) | |
| elif avg_brightness > thresholds.indoor_moderate_thresh: | |
| return self._classify_moderate_indoor( | |
| features, is_designer_lit, is_likely_home_environment, p365_context, diagnostics | |
| ) | |
| else: | |
| return self._classify_dim_indoor(features, diagnostics) | |
| def _classify_bright_indoor(self, features: Dict[str, Any], natural_light_hints: float, | |
| is_designer_lit: bool, is_likely_home_environment: bool, | |
| p365_context: Dict[str, Any], diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify bright indoor lighting conditions.""" | |
| mapped_scene = p365_context["mapped_scene"] | |
| sky_blue_in_sky_region = features.get("sky_region_blue_dominance", 0.0) | |
| sky_region_is_brighter = features.get("sky_region_brightness_ratio", 1.0) > 1.05 | |
| # Natural residential lighting | |
| if (natural_light_hints >= 1.5 and | |
| (is_likely_home_environment or any(kw in mapped_scene for kw in ["home", "residential", "living", "bedroom"]))): | |
| return { | |
| "time_of_day": "indoor_residential_natural", | |
| "confidence": 0.82 | |
| } | |
| # Designer residential lighting | |
| elif (is_designer_lit and | |
| (is_likely_home_environment or any(kw in mapped_scene for kw in ["home", "designer", "modern_interior"]))): | |
| return { | |
| "time_of_day": "indoor_designer_residential", | |
| "confidence": 0.85 | |
| } | |
| # Mixed natural/artificial lighting | |
| elif sky_blue_in_sky_region > 0.03 and sky_region_is_brighter: | |
| return { | |
| "time_of_day": "indoor_bright_natural_mix", | |
| "confidence": 0.78 | |
| } | |
| # Pure artificial lighting | |
| else: | |
| return { | |
| "time_of_day": "indoor_bright_artificial", | |
| "confidence": 0.75 | |
| } | |
| def _classify_moderate_indoor(self, features: Dict[str, Any], is_designer_lit: bool, | |
| is_likely_home_environment: bool, p365_context: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify moderate brightness indoor lighting conditions.""" | |
| mapped_scene = p365_context["mapped_scene"] | |
| confidence = p365_context["confidence"] | |
| warm_ratio = features.get("warm_ratio", 0.0) | |
| yellow_orange_ratio = features.get("yellow_orange_ratio", 0.0) | |
| # Designer residential lighting | |
| if (is_designer_lit and | |
| (is_likely_home_environment or any(kw in mapped_scene for kw in ["home", "designer"]))): | |
| return { | |
| "time_of_day": "indoor_designer_residential", | |
| "confidence": 0.78 | |
| } | |
| # Restaurant/bar lighting | |
| elif warm_ratio > 0.35 and yellow_orange_ratio > 0.1: | |
| return self._classify_restaurant_bar_lighting( | |
| p365_context, features, diagnostics | |
| ) | |
| # Standard moderate artificial | |
| else: | |
| return { | |
| "time_of_day": "indoor_moderate_artificial", | |
| "confidence": 0.70 | |
| } | |
| def _classify_restaurant_bar_lighting(self, p365_context: Dict[str, Any], | |
| features: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify restaurant/bar specific lighting conditions.""" | |
| mapped_scene = p365_context["mapped_scene"] | |
| confidence = p365_context["confidence"] | |
| # Strong P365 restaurant/bar confirmation | |
| if (any(kw in mapped_scene for kw in self.P365_INDOOR_RESTAURANT_KEYWORDS) and | |
| confidence > self.P365_SCENE_MODERATE_CONF_THRESHOLD): | |
| diagnostics["visual_analysis_reason"] = ( | |
| "Visual: Moderate warm tones. P365 context confirms restaurant/bar." | |
| ) | |
| return { | |
| "time_of_day": "indoor_restaurant_bar", | |
| "confidence": 0.80 + confidence * 0.15 | |
| } | |
| # P365 outdoor conflict detection | |
| elif (any(kw in mapped_scene for kw in self.P365_OUTDOOR_SCENE_KEYWORDS) and | |
| confidence > self.P365_SCENE_MODERATE_CONF_THRESHOLD): | |
| diagnostics["visual_analysis_reason"] = ( | |
| "Visual: Moderate warm. CONFLICT: LA says indoor but P365 scene is outdoor. " | |
| "Defaulting to general indoor artificial." | |
| ) | |
| diagnostics["conflict_is_indoor_vs_p365_scene_for_restaurant_bar"] = True | |
| return { | |
| "time_of_day": "indoor_moderate_artificial", | |
| "confidence": 0.55 | |
| } | |
| # Neutral P365 context | |
| else: | |
| diagnostics["visual_analysis_reason"] = ( | |
| "Visual: Moderate warm tones, typical of restaurant/bar. P365 context neutral or weak." | |
| ) | |
| return { | |
| "time_of_day": "indoor_restaurant_bar", | |
| "confidence": 0.70 | |
| } | |
| def _classify_dim_indoor(self, features: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify dim indoor lighting conditions.""" | |
| warm_ratio = features.get("warm_ratio", 0.0) | |
| yellow_orange_ratio = features.get("yellow_orange_ratio", 0.0) | |
| if warm_ratio > 0.45 and yellow_orange_ratio > 0.15: | |
| return { | |
| "time_of_day": "indoor_dim_warm", | |
| "confidence": 0.75 | |
| } | |
| else: | |
| return { | |
| "time_of_day": "indoor_dim_general", | |
| "confidence": 0.70 | |
| } | |
| def _analyze_outdoor_visual_features(self, features: Dict[str, Any], p365_context: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Analyze visual features for outdoor lighting conditions.""" | |
| avg_brightness = features.get("avg_brightness", 128.0) | |
| thresholds = self.config_manager.lighting_thresholds | |
| # P365 enhanced street scene analysis | |
| street_result = self._analyze_p365_enhanced_street_scenes( | |
| features, p365_context, diagnostics | |
| ) | |
| if street_result["determined"]: | |
| return street_result | |
| # Brightness-based outdoor classification | |
| if avg_brightness < thresholds.outdoor_night_thresh_brightness: | |
| return self._classify_night_outdoor(features, diagnostics) | |
| elif (avg_brightness < thresholds.outdoor_dusk_dawn_thresh_brightness and | |
| self._check_warm_sunset_conditions(features)): | |
| return self._classify_sunset_sunrise(features, p365_context, diagnostics) | |
| elif avg_brightness > thresholds.outdoor_day_bright_thresh: | |
| return self._classify_bright_day_outdoor(features, diagnostics) | |
| elif avg_brightness > thresholds.outdoor_day_cloudy_thresh: | |
| return self._classify_cloudy_day_outdoor(features, diagnostics) | |
| else: | |
| return self._classify_general_outdoor(features, diagnostics) | |
| def _analyze_p365_enhanced_street_scenes(self, features: Dict[str, Any], p365_context: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Analyze outdoor scenes with Places365 street context enhancement.""" | |
| mapped_scene = p365_context["mapped_scene"] | |
| confidence = p365_context["confidence"] | |
| thresholds = self.config_manager.lighting_thresholds | |
| # Check for street scene with warm lighting | |
| is_street_scene = ( | |
| any(kw in mapped_scene for kw in ["street", "city", "road", "urban", "downtown", "intersection"]) and | |
| confidence > self.P365_SCENE_MODERATE_CONF_THRESHOLD and | |
| features.get("color_atmosphere") == "warm" | |
| ) | |
| if not is_street_scene: | |
| return {"determined": False, "time_of_day": "unknown", "confidence": 0.5} | |
| avg_brightness = features.get("avg_brightness", 128.0) | |
| bright_spots_overall = features.get("bright_spot_count", 0) | |
| # Night with street lights | |
| if (avg_brightness < thresholds.outdoor_night_thresh_brightness and | |
| bright_spots_overall > thresholds.outdoor_night_lights_thresh): | |
| diagnostics["visual_analysis_reason"] = ( | |
| f"P365 outdoor scene '{mapped_scene}' + visual low-warm light with spots -> night_with_lights." | |
| ) | |
| return { | |
| "determined": True, | |
| "time_of_day": "night_with_lights", | |
| "confidence": 0.88 + confidence * 0.1 | |
| } | |
| # Sunset/sunrise conditions | |
| elif avg_brightness >= thresholds.outdoor_night_thresh_brightness: | |
| diagnostics["visual_analysis_reason"] = ( | |
| f"P365 outdoor scene '{mapped_scene}' + visual moderate-warm light -> sunset/sunrise." | |
| ) | |
| return { | |
| "determined": True, | |
| "time_of_day": "sunset_sunrise", | |
| "confidence": 0.88 + confidence * 0.1 | |
| } | |
| # Very dark conditions | |
| else: | |
| diagnostics["visual_analysis_reason"] = ( | |
| f"P365 outdoor scene '{mapped_scene}' + visual very low light -> night_dark." | |
| ) | |
| return { | |
| "determined": True, | |
| "time_of_day": "night_dark", | |
| "confidence": 0.75 + confidence * 0.1 | |
| } | |
| def _classify_night_outdoor(self, features: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify nighttime outdoor conditions.""" | |
| bright_spots_overall = features.get("bright_spot_count", 0) | |
| dark_pixel_ratio = features.get("dark_pixel_ratio", 0.0) | |
| thresholds = self.config_manager.lighting_thresholds | |
| if bright_spots_overall > thresholds.outdoor_night_lights_thresh: | |
| confidence = 0.82 + min(0.13, dark_pixel_ratio / 2.5) | |
| diagnostics["visual_analysis_reason"] = "Visual: Low brightness with light sources (street/car lights)." | |
| return { | |
| "time_of_day": "night_with_lights", | |
| "confidence": confidence | |
| } | |
| else: | |
| confidence = 0.78 + min(0.17, dark_pixel_ratio / 1.8) | |
| diagnostics["visual_analysis_reason"] = "Visual: Very low brightness outdoor, deep night." | |
| return { | |
| "time_of_day": "night_dark", | |
| "confidence": confidence | |
| } | |
| def _classify_sunset_sunrise(self, features: Dict[str, Any], p365_context: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify sunset/sunrise outdoor conditions.""" | |
| yellow_orange_ratio = features.get("yellow_orange_ratio", 0.0) | |
| confidence = 0.75 + min(0.20, yellow_orange_ratio / 1.5) | |
| diagnostics["visual_analysis_reason"] = "Visual: Moderate brightness, warm tones -> sunset/sunrise." | |
| # P365 natural scene boost | |
| mapped_scene = p365_context["mapped_scene"] | |
| p365_confidence = p365_context["confidence"] | |
| if (any(kw in mapped_scene for kw in ["beach", "mountain", "lake", "ocean", "desert", "field", "natural_landmark", "sky"]) and | |
| p365_confidence > self.P365_SCENE_MODERATE_CONF_THRESHOLD): | |
| confidence = min(0.95, confidence + 0.15) | |
| diagnostics["visual_analysis_reason"] += f" P365 natural scene '{mapped_scene}' supports." | |
| return { | |
| "time_of_day": "sunset_sunrise", | |
| "confidence": confidence | |
| } | |
| def _classify_bright_day_outdoor(self, features: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify bright daytime outdoor conditions.""" | |
| sky_like_blue_in_sky_region = features.get("sky_region_blue_dominance", 0.0) | |
| sky_region_brightness_ratio = features.get("sky_region_brightness_ratio", 1.0) | |
| texture_complexity = features.get("top_region_texture_complexity", 0.5) | |
| thresholds = self.config_manager.lighting_thresholds | |
| # Clear sky conditions | |
| if (sky_like_blue_in_sky_region > thresholds.outdoor_day_blue_thresh or | |
| (sky_region_brightness_ratio > 1.05 and texture_complexity < 0.4)): | |
| confidence = 0.80 + min(0.15, sky_like_blue_in_sky_region * 2 + | |
| (sky_like_blue_in_sky_region * 1.5 if sky_region_brightness_ratio > 1.05 else 0)) | |
| diagnostics["visual_analysis_reason"] = "Visual: High brightness with blue/sky tones or bright smooth top." | |
| return { | |
| "time_of_day": "day_clear", | |
| "confidence": confidence | |
| } | |
| # Stadium/floodlit detection | |
| brightness_uniformity = features.get("brightness_uniformity", 0.0) | |
| bright_spots_overall = features.get("bright_spot_count", 0) | |
| if (brightness_uniformity > 0.70 and | |
| bright_spots_overall > thresholds.stadium_min_spots_thresh): | |
| diagnostics["visual_analysis_reason"] = ( | |
| "Visual: Very bright, uniform lighting with multiple sources, suggests floodlights (Outdoor)." | |
| ) | |
| return { | |
| "time_of_day": "stadium_or_floodlit_area", | |
| "confidence": 0.78 | |
| } | |
| # General bright day | |
| diagnostics["visual_analysis_reason"] = "Visual: High brightness outdoor, specific sky features unclear." | |
| return { | |
| "time_of_day": "day_bright_general", | |
| "confidence": 0.68 | |
| } | |
| def _classify_cloudy_day_outdoor(self, features: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify cloudy daytime outdoor conditions.""" | |
| sky_region_brightness_ratio = features.get("sky_region_brightness_ratio", 1.0) | |
| texture_complexity = features.get("top_region_texture_complexity", 0.5) | |
| avg_saturation = features.get("avg_saturation", 100.0) | |
| gray_ratio = features.get("gray_ratio", 0.0) | |
| brightness_uniformity = features.get("brightness_uniformity", 0.0) | |
| thresholds = self.config_manager.lighting_thresholds | |
| # Overcast conditions | |
| if (sky_region_brightness_ratio > 1.05 and texture_complexity < 0.45 and avg_saturation < 70): | |
| confidence = 0.75 + min(0.20, gray_ratio / 1.5 + (brightness_uniformity - 0.5) / 1.5) | |
| diagnostics["visual_analysis_reason"] = ( | |
| "Visual: Good brightness, uniform bright top, lower saturation -> overcast." | |
| ) | |
| return { | |
| "time_of_day": "day_cloudy_overcast", | |
| "confidence": confidence | |
| } | |
| # Gray cloudy conditions | |
| elif gray_ratio > thresholds.outdoor_day_gray_thresh: | |
| confidence = 0.72 + min(0.23, gray_ratio / 1.8) | |
| diagnostics["visual_analysis_reason"] = "Visual: Good brightness with higher gray tones." | |
| return { | |
| "time_of_day": "day_cloudy_gray", | |
| "confidence": confidence | |
| } | |
| # General bright outdoor | |
| else: | |
| diagnostics["visual_analysis_reason"] = "Visual: Bright outdoor, specific type less clear." | |
| return { | |
| "time_of_day": "day_bright_general", | |
| "confidence": 0.68 | |
| } | |
| def _classify_general_outdoor(self, features: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Classify general outdoor conditions when specific patterns are unclear.""" | |
| color_atmosphere = features.get("color_atmosphere", "neutral") | |
| yellow_orange_ratio = features.get("yellow_orange_ratio", 0.0) | |
| sky_like_blue_in_sky_region = features.get("sky_region_blue_dominance", 0.0) | |
| # Potential sunset/sunrise with low confidence | |
| if color_atmosphere == "warm" and yellow_orange_ratio > 0.08: | |
| diagnostics["visual_analysis_reason"] = ( | |
| "Visual: Outdoor, specific conditions less clear; broader visual cues suggest warm lighting." | |
| ) | |
| return { | |
| "time_of_day": "sunset_sunrise_low_confidence", | |
| "confidence": 0.62 | |
| } | |
| # Potential hazy day conditions | |
| elif sky_like_blue_in_sky_region > 0.02: | |
| diagnostics["visual_analysis_reason"] = ( | |
| "Visual: Outdoor, specific conditions less clear; some blue tones suggest daylight." | |
| ) | |
| return { | |
| "time_of_day": "day_hazy_or_partly_cloudy", | |
| "confidence": 0.62 | |
| } | |
| # Unknown outdoor daylight | |
| else: | |
| diagnostics["visual_analysis_reason"] = ( | |
| "Visual: Outdoor, specific conditions less clear; broader visual cues." | |
| ) | |
| return { | |
| "time_of_day": "outdoor_unknown_daylight", | |
| "confidence": 0.58 | |
| } | |
| def _apply_commercial_indoor_refinement(self, features: Dict[str, Any], p365_context: Dict[str, Any], | |
| time_of_day: str, confidence: float) -> Dict[str, Any]: | |
| """Apply commercial indoor lighting refinement if conditions are met.""" | |
| # Skip if already classified as residential, restaurant, or bar | |
| if any(category in time_of_day for category in ["residential", "restaurant", "bar"]): | |
| return {"time_of_day": time_of_day, "confidence": confidence} | |
| # Skip if P365 suggests home environment | |
| mapped_scene = p365_context["mapped_scene"] | |
| if any(kw in mapped_scene for kw in ["home", "residential"]): | |
| return {"time_of_day": time_of_day, "confidence": confidence} | |
| # Check commercial lighting indicators | |
| avg_brightness = features.get("avg_brightness", 100.0) | |
| bright_spots_overall = features.get("bright_spot_count", 0) | |
| light_dist_uniformity = features.get("light_distribution_uniformity", 0.5) | |
| ceiling_likelihood = features.get("ceiling_likelihood", 0.0) | |
| thresholds = self.config_manager.lighting_thresholds | |
| if (avg_brightness > thresholds.commercial_min_brightness_thresh and | |
| bright_spots_overall > thresholds.commercial_min_spots_thresh and | |
| (light_dist_uniformity > 0.5 or ceiling_likelihood > 0.4)): | |
| refined_confidence = 0.70 + min(0.2, bright_spots_overall * 0.02) | |
| return { | |
| "time_of_day": "indoor_commercial", | |
| "confidence": refined_confidence | |
| } | |
| return {"time_of_day": time_of_day, "confidence": confidence} | |
| def _apply_special_lighting_refinement(self, time_of_day: str, confidence: float, | |
| features: Dict[str, Any], is_indoor: bool, | |
| p365_context: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Apply special lighting refinement for neon and sodium vapor lighting.""" | |
| # Apply commercial refinement for indoor scenes first | |
| if is_indoor: | |
| commercial_result = self._apply_commercial_indoor_refinement( | |
| features, p365_context, time_of_day, confidence | |
| ) | |
| time_of_day = commercial_result["time_of_day"] | |
| confidence = commercial_result["confidence"] | |
| # Check for neon/sodium vapor lighting conditions | |
| is_current_night_or_dim_warm = "night" in time_of_day or time_of_day == "indoor_dim_warm" | |
| if not is_current_night_or_dim_warm: | |
| return {"time_of_day": time_of_day, "confidence": confidence} | |
| # Extract features for neon detection | |
| yellow_orange_ratio = features.get("yellow_orange_ratio", 0.0) | |
| bright_spots_overall = features.get("bright_spot_count", 0) | |
| color_atmosphere = features.get("color_atmosphere", "neutral") | |
| avg_saturation = features.get("avg_saturation", 0.0) | |
| # Get neon detection thresholds | |
| thresholds = self.config_manager.lighting_thresholds | |
| # Check neon lighting conditions | |
| if (yellow_orange_ratio > thresholds.neon_yellow_orange_thresh and | |
| bright_spots_overall > thresholds.neon_bright_spots_thresh and | |
| color_atmosphere == "warm" and | |
| avg_saturation > thresholds.neon_avg_saturation_thresh): | |
| old_time_of_day = time_of_day | |
| old_confidence = confidence | |
| # Check P365 context for neon scenes | |
| mapped_scene = p365_context["mapped_scene"] | |
| attributes = p365_context["attributes"] | |
| is_p365_neon_context = ( | |
| any(kw in mapped_scene for kw in ["neon", "nightclub", "bar_neon"]) or | |
| "neon" in attributes | |
| ) | |
| if is_indoor: | |
| if (is_p365_neon_context or | |
| any(kw in mapped_scene for kw in self.P365_INDOOR_RESTAURANT_KEYWORDS)): | |
| time_of_day = "indoor_neon_lit" | |
| confidence = max(confidence, 0.80) | |
| else: | |
| time_of_day = "indoor_dim_warm_neon_accent" | |
| confidence = max(confidence, 0.77) | |
| else: | |
| if (is_p365_neon_context or | |
| any(kw in mapped_scene for kw in ["street_night", "city_night", "downtown_night"])): | |
| time_of_day = "neon_or_sodium_vapor_night" | |
| confidence = max(confidence, 0.82) | |
| else: | |
| time_of_day = "night_with_neon_lights" | |
| confidence = max(confidence, 0.79) | |
| # Record the refinement | |
| diagnostics["special_lighting_detected"] = ( | |
| f"Refined from {old_time_of_day} (Conf:{old_confidence:.2f}) " | |
| f"to {time_of_day} (Conf:{confidence:.2f}) due to neon/sodium vapor light characteristics. " | |
| f"P365 Context: {mapped_scene if is_p365_neon_context else 'N/A'}." | |
| ) | |
| return {"time_of_day": time_of_day, "confidence": confidence} | |
| def _combine_attribute_and_visual_results(self, attribute_result: Dict[str, Any], | |
| visual_result: Dict[str, Any], | |
| diagnostics: Dict[str, Any]) -> Dict[str, Any]: | |
| """Combine Places365 attribute and visual analysis results.""" | |
| # If visual analysis provided a different and potentially more nuanced result | |
| if (attribute_result["time_of_day"] != visual_result["time_of_day"] and | |
| visual_result["confidence"] > 0.65): | |
| diagnostics["final_decision_source"] = "Visual features (potentially P365-context-refined)." | |
| diagnostics["p365_attr_overridden_by_visual"] = ( | |
| f"P365 Attr ToD {attribute_result['time_of_day']} " | |
| f"(Conf {attribute_result['confidence']:.2f}) was less certain or overridden by " | |
| f"visual logic result {visual_result['time_of_day']} (Conf {visual_result['confidence']:.2f})." | |
| ) | |
| return visual_result | |
| # Use attribute result if it was more confident | |
| elif attribute_result["confidence"] >= visual_result["confidence"]: | |
| diagnostics["final_decision_source"] = "High-confidence P365 attribute." | |
| return attribute_result | |
| # Use visual result | |
| else: | |
| diagnostics["final_decision_source"] = "Visual features (potentially P365-context-refined)." | |
| return visual_result | |
| def _check_home_environment_pattern(self, features: Dict[str, Any]) -> bool: | |
| """Check if features indicate a home/residential environment pattern.""" | |
| thresholds = self.config_manager.indoor_outdoor_thresholds | |
| return features.get("home_environment_pattern", 0.0) > thresholds.home_pattern_thresh_moderate * 0.7 | |
| def _check_warm_sunset_conditions(self, features: Dict[str, Any]) -> bool: | |
| """Check if features indicate warm sunset/sunrise lighting conditions.""" | |
| thresholds = self.config_manager.lighting_thresholds | |
| yellow_orange_ratio = features.get("yellow_orange_ratio", 0.0) | |
| color_atmosphere = features.get("color_atmosphere", "neutral") | |
| sky_brightness_ratio = features.get("sky_region_brightness_ratio", 1.0) | |
| return (yellow_orange_ratio > thresholds.outdoor_dusk_dawn_color_thresh and | |
| color_atmosphere == "warm" and | |
| sky_brightness_ratio < 1.5) | |
| def _get_default_lighting_result(self) -> Dict[str, Any]: | |
| """Return default lighting analysis result in case of errors.""" | |
| return { | |
| "time_of_day": "unknown", | |
| "confidence": 0.5, | |
| "diagnostics": { | |
| "error": "Lighting analysis failed, using default values" | |
| } | |
| } | |