VisionScout / lighting_condition_analyzer.py
DawnC's picture
Upload 59 files
e6a18b7 verified
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"
}
}