|
|
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") |
|
|
|