Spaces:
Running
on
Zero
Running
on
Zero
File size: 40,243 Bytes
e6a18b7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 |
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"
}
}
|