| import cv2 |
| import numpy as np |
| import torch |
| import torch.nn as nn |
| from PIL import Image |
| from typing import Dict, Tuple |
| import torchvision.models as models |
| import torchvision.transforms as transforms |
|
|
| class LightingAnalysisManager: |
| """Advanced lighting analysis using Places365 scene recognition + CV features""" |
|
|
| def __init__(self): |
| print("Initializing Lighting Analysis Manager with Places365...") |
|
|
| |
| self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| self._load_places365_model() |
|
|
| |
| self.feature_weights = { |
| 'places365': 0.50, |
| 'brightness': 0.15, |
| 'color_temp': 0.15, |
| 'contrast': 0.08, |
| 'gradient': 0.05, |
| 'laplacian': 0.04, |
| 'color_variation': 0.03 |
| } |
|
|
| print("✓ Lighting Analysis Manager initialized with Places365 + advanced CV features") |
|
|
| def _load_places365_model(self): |
| """Load Places365 ResNet18 for scene attributes""" |
| try: |
| |
| model = models.resnet18(weights=None) |
| model.fc = nn.Linear(model.fc.in_features, 365) |
|
|
| |
| try: |
| import urllib |
| checkpoint_url = 'http://places2.csail.mit.edu/models_places365/resnet18_places365.pth.tar' |
| checkpoint = torch.hub.load_state_dict_from_url( |
| checkpoint_url, |
| map_location=self.device, |
| progress=False |
| ) |
| state_dict = {str.replace(k, 'module.', ''): v for k, v in checkpoint['state_dict'].items()} |
| model.load_state_dict(state_dict) |
| print(" Loaded Places365 ResNet18 weights") |
| except: |
| print(" Using ImageNet pretrained ResNet18 (fallback)") |
| model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) |
|
|
| model = model.to(self.device) |
| model.eval() |
| self.places_model = model |
|
|
| |
| self.places_transform = transforms.Compose([ |
| transforms.Resize((224, 224)), |
| transforms.ToTensor(), |
| transforms.Normalize( |
| mean=[0.485, 0.456, 0.406], |
| std=[0.229, 0.224, 0.225] |
| ) |
| ]) |
|
|
| |
| self.lighting_scenes = { |
| 'sunny': ['street', 'downtown', 'plaza', 'park', 'field'], |
| 'overcast': ['alley', 'covered_bridge', 'corridor'], |
| 'indoor': ['lobby', 'office', 'museum', 'restaurant'], |
| 'evening': ['street', 'downtown', 'plaza'], |
| 'natural': ['park', 'forest', 'mountain', 'coast'] |
| } |
|
|
| except Exception as e: |
| print(f" Warning: Places365 loading failed ({e}), using CV-only mode") |
| self.places_model = None |
|
|
| def analyze_lighting(self, image: Image.Image) -> Dict: |
| """Comprehensive lighting analysis using Places365 + CV""" |
|
|
| |
| cv_features = self._extract_cv_features(image) |
|
|
| |
| scene_info = self._analyze_scene_places365(image) |
|
|
| |
| lighting_condition, confidence = self._determine_lighting_adaptive( |
| cv_features, scene_info |
| ) |
|
|
| return { |
| 'lighting_type': lighting_condition, |
| 'confidence': confidence, |
| 'cv_features': cv_features, |
| 'scene_info': scene_info |
| } |
|
|
| def _extract_cv_features(self, image: Image.Image) -> Dict: |
| """Extract CV-based features including advanced gradient and color analysis""" |
| img_array = np.array(image) |
| img_bgr = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR) |
|
|
| |
| |
| lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB) |
| brightness = float(np.mean(lab[:, :, 0])) |
|
|
| |
| b_mean = np.mean(img_bgr[:, :, 0]) |
| r_mean = np.mean(img_bgr[:, :, 2]) |
| color_temp = float(r_mean / (b_mean + 1e-6)) |
|
|
| |
| gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) |
| contrast = float(np.std(gray)) |
|
|
| |
| _, shadow_mask = cv2.threshold(gray, 80, 255, cv2.THRESH_BINARY_INV) |
| shadow_ratio = float(np.sum(shadow_mask > 0) / shadow_mask.size) |
|
|
| |
| |
| |
| sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) |
| sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) |
| gradient_magnitude = np.sqrt(sobelx**2 + sobely**2) |
| gradient_strength = float(np.mean(gradient_magnitude)) |
|
|
| |
| |
| laplacian = cv2.Laplacian(gray, cv2.CV_64F) |
| laplacian_var = float(np.var(laplacian)) |
|
|
| |
| |
| a_std = float(np.std(lab[:, :, 1])) |
| b_std = float(np.std(lab[:, :, 2])) |
| color_variation = (a_std + b_std) / 2 |
|
|
| return { |
| |
| 'brightness': brightness, |
| 'color_temp': color_temp, |
| 'contrast': contrast, |
| 'shadow_ratio': shadow_ratio, |
| |
| 'gradient_strength': gradient_strength, |
| 'laplacian_variance': laplacian_var, |
| 'color_variation': color_variation |
| } |
|
|
| def _analyze_scene_places365(self, image: Image.Image) -> Dict: |
| """Analyze scene using Places365""" |
| if self.places_model is None: |
| return {'scene_category': 'unknown', 'confidence': 0.0} |
|
|
| try: |
| with torch.no_grad(): |
| img_tensor = self.places_transform(image).unsqueeze(0).to(self.device) |
| logits = self.places_model(img_tensor) |
| probs = torch.nn.functional.softmax(logits, dim=1) |
|
|
| |
| top_prob, top_idx = torch.max(probs, 1) |
|
|
| |
| |
| is_outdoor = top_idx.item() < 200 |
|
|
| return { |
| 'scene_category': 'outdoor' if is_outdoor else 'indoor', |
| 'confidence': float(top_prob.item()), |
| 'scene_idx': int(top_idx.item()) |
| } |
| except Exception as e: |
| print(f" Places365 inference failed: {e}") |
| return {'scene_category': 'unknown', 'confidence': 0.0} |
|
|
| def _detect_indoor_scene(self, cv_features: Dict, scene_info: Dict) -> bool: |
| """ |
| Detect if scene is indoor or outdoor using multiple signals |
| |
| Args: |
| cv_features: Computer vision features |
| scene_info: Places365 scene information |
| |
| Returns: |
| True if indoor, False if outdoor |
| """ |
| indoor_score = 0.0 |
|
|
| |
| if scene_info.get('scene_category') == 'indoor': |
| indoor_score += 0.5 |
| elif scene_info.get('scene_category') == 'outdoor': |
| indoor_score -= 0.3 |
|
|
| |
| |
| brightness = cv_features['brightness'] |
| if 60 < brightness < 220: |
| indoor_score += 0.15 |
| elif brightness > 230: |
| indoor_score -= 0.2 |
|
|
| |
| gradient = cv_features['gradient_strength'] |
| if gradient < 20: |
| indoor_score += 0.15 |
|
|
| |
| laplacian = cv_features['laplacian_variance'] |
| if laplacian < 400: |
| indoor_score += 0.10 |
|
|
| |
| shadow_ratio = cv_features['shadow_ratio'] |
| if shadow_ratio < 0.25: |
| indoor_score += 0.10 |
| elif shadow_ratio > 0.5: |
| indoor_score -= 0.15 |
|
|
| |
| return indoor_score > 0.15 |
|
|
| def _determine_indoor_lighting(self, cv_features: Dict) -> Tuple[str, float]: |
| """ |
| Determine lighting type for indoor scenes |
| |
| Returns indoor-specific lighting types with confidence |
| """ |
| brightness = cv_features['brightness'] |
| color_temp = cv_features['color_temp'] |
| contrast = cv_features['contrast'] |
| shadow_ratio = cv_features['shadow_ratio'] |
| gradient = cv_features['gradient_strength'] |
| laplacian = cv_features['laplacian_variance'] |
|
|
| |
| brightness_norm = min(brightness / 255.0, 1.0) |
| contrast_norm = min(contrast / 100.0, 1.0) |
| gradient_norm = min(gradient / 50.0, 1.0) |
| laplacian_norm = min(laplacian / 1000.0, 1.0) |
|
|
| scores = {} |
|
|
| |
| |
| studio_score = ( |
| 0.35 * (1.0 if brightness_norm > 0.6 else 0.5) + |
| 0.25 * (1.0 - shadow_ratio) + |
| 0.20 * (1.0 - gradient_norm) + |
| 0.15 * (1.0 - laplacian_norm) + |
| 0.05 * (1.0 - abs(color_temp - 1.0)) |
| ) |
| scores['studio lighting'] = studio_score |
|
|
| |
| |
| natural_indoor_score = ( |
| 0.30 * (1.0 if 0.5 < brightness_norm < 0.8 else 0.5) + |
| 0.25 * min(contrast_norm, 0.6) + |
| 0.20 * (1.0 if color_temp > 0.95 else 0.5) + |
| 0.15 * min(gradient_norm, 0.5) + |
| 0.10 * (1.0 if shadow_ratio < 0.3 else 0.5) |
| ) |
| scores['indoor natural light'] = natural_indoor_score |
|
|
| |
| |
| warm_artificial_score = ( |
| 0.35 * (1.0 if color_temp > 1.1 else 0.3) + |
| 0.25 * (1.0 - abs(brightness_norm - 0.5)) + |
| 0.20 * (1.0 - gradient_norm) + |
| 0.15 * (1.0 - shadow_ratio) + |
| 0.05 * (1.0 - laplacian_norm) |
| ) |
| scores['warm artificial lighting'] = warm_artificial_score |
|
|
| |
| |
| cool_artificial_score = ( |
| 0.35 * (1.0 if color_temp < 1.05 else 0.4) + |
| 0.25 * (1.0 if brightness_norm > 0.5 else 0.5) + |
| 0.20 * (1.0 - gradient_norm) + |
| 0.15 * (1.0 - shadow_ratio) + |
| 0.05 * (1.0 - laplacian_norm) |
| ) |
| scores['cool artificial lighting'] = cool_artificial_score |
|
|
| |
| |
| soft_indoor_score = ( |
| 0.30 * (1.0 - abs(brightness_norm - 0.5)) + |
| 0.30 * (1.0 - contrast_norm) + |
| 0.20 * (1.0 - gradient_norm) + |
| 0.15 * (1.0 - shadow_ratio) + |
| 0.05 * (1.0 - laplacian_norm) |
| ) |
| scores['soft indoor lighting'] = soft_indoor_score |
|
|
| |
| |
| dramatic_score = ( |
| 0.35 * contrast_norm + |
| 0.25 * gradient_norm + |
| 0.20 * shadow_ratio + |
| 0.15 * laplacian_norm + |
| 0.05 * (1.0 if brightness_norm < 0.6 else 0.5) |
| ) |
| scores['dramatic indoor lighting'] = dramatic_score |
|
|
| |
| best_condition = max(scores.items(), key=lambda x: x[1]) |
|
|
| |
| sorted_scores = sorted(scores.values(), reverse=True) |
| if len(sorted_scores) > 1: |
| score_gap = sorted_scores[0] - sorted_scores[1] |
| confidence = min(0.7 + score_gap * 0.3, 0.95) |
| else: |
| confidence = 0.7 |
|
|
| return best_condition[0], confidence |
|
|
| def _determine_lighting_adaptive(self, cv_features: Dict, scene_info: Dict) -> Tuple[str, float]: |
| """Determine lighting using adaptive thresholds with indoor/outdoor detection""" |
|
|
| |
| brightness = cv_features['brightness'] |
| color_temp = cv_features['color_temp'] |
| contrast = cv_features['contrast'] |
| shadow = cv_features['shadow_ratio'] |
| gradient = cv_features['gradient_strength'] |
| laplacian = cv_features['laplacian_variance'] |
| color_var = cv_features['color_variation'] |
|
|
| |
| is_indoor = self._detect_indoor_scene(cv_features, scene_info) |
| if is_indoor: |
| |
| return self._determine_indoor_lighting(cv_features) |
| |
|
|
| |
| brightness_norm = min(brightness / 255.0, 1.0) |
| contrast_norm = min(contrast / 100.0, 1.0) |
| gradient_norm = min(gradient / 50.0, 1.0) |
| laplacian_norm = min(laplacian / 1000.0, 1.0) |
| color_var_norm = min(color_var / 50.0, 1.0) |
|
|
| |
| scores = {} |
|
|
| |
| |
| |
| diffuse_score = ( |
| 0.40 * (1.0 - abs(brightness_norm - 0.5)) + |
| 0.25 * (1.0 - contrast_norm) + |
| 0.20 * (1.0 - abs(color_temp - 1.0)) + |
| 0.08 * (1.0 - gradient_norm) + |
| 0.05 * (1.0 - laplacian_norm) + |
| 0.02 * (1.0 - color_var_norm) |
| ) |
| scores['soft diffused light'] = diffuse_score |
|
|
| |
| |
| |
| daylight_score = ( |
| 0.40 * brightness_norm + |
| 0.25 * min(contrast_norm, 0.7) + |
| 0.20 * (1.0 - abs(color_temp - 1.0)) + |
| 0.08 * min(gradient_norm, 0.6) + |
| 0.05 * min(laplacian_norm, 0.6) + |
| 0.02 * min(color_var_norm, 0.5) |
| ) |
| scores['natural daylight'] = daylight_score |
|
|
| |
| |
| |
| overcast_score = ( |
| 0.35 * (1.0 - abs(brightness_norm - 0.45)) + |
| 0.25 * (1.0 - contrast_norm) + |
| 0.15 * (1.0 if color_temp < 1.05 else 0.5) + |
| 0.10 * (1.0 - shadow) + |
| 0.08 * (1.0 - gradient_norm) + |
| 0.05 * (1.0 - laplacian_norm) + |
| 0.02 * (1.0 - color_var_norm) |
| ) |
| scores['overcast atmosphere'] = overcast_score |
|
|
| |
| |
| |
| warm_score = ( |
| 0.40 * (1.0 - abs(brightness_norm - 0.5)) + |
| 0.30 * (1.0 if color_temp > 1.1 else 0.5) + |
| 0.15 * min(contrast_norm, 0.6) + |
| 0.08 * min(gradient_norm, 0.5) + |
| 0.05 * min(laplacian_norm, 0.5) + |
| 0.02 * color_var_norm |
| ) |
| scores['warm ambient light'] = warm_score |
|
|
| |
| |
| |
| evening_score = ( |
| 0.35 * (1.0 if brightness_norm < 0.6 else 0.5) + |
| 0.30 * (1.0 if color_temp > 1.05 else 0.5) + |
| 0.20 * contrast_norm + |
| 0.08 * min(gradient_norm, 0.7) + |
| 0.05 * laplacian_norm + |
| 0.02 * color_var_norm |
| ) |
| scores['evening light'] = evening_score |
|
|
| |
| |
| |
| sunlight_score = ( |
| 0.40 * (1.0 if brightness_norm > 0.7 else 0.3) + |
| 0.25 * contrast_norm + |
| 0.15 * shadow + |
| 0.10 * gradient_norm + |
| 0.08 * laplacian_norm + |
| 0.02 * color_var_norm |
| ) |
| scores['bright sunlight'] = sunlight_score |
|
|
| |
| best_condition = max(scores.items(), key=lambda x: x[1]) |
|
|
| |
| sorted_scores = sorted(scores.values(), reverse=True) |
| if len(sorted_scores) > 1: |
| score_gap = sorted_scores[0] - sorted_scores[1] |
| confidence = min(0.7 + score_gap * 0.3, 0.95) |
| else: |
| confidence = 0.7 |
|
|
| return best_condition[0], confidence |
|
|
| print("✓ LightingAnalysisManager (with Places365 + advanced CV features) defined") |
|
|