File size: 20,764 Bytes
6a3bd1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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...")

        # Places365 ResNet18
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self._load_places365_model()

        # CV feature weights (Places365 gets higher weight)
        self.feature_weights = {
            'places365': 0.50,     # Primary weight to Places365
            'brightness': 0.15,
            'color_temp': 0.15,
            'contrast': 0.08,
            'gradient': 0.05,      # Auxiliary features
            '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:
            # Use ResNet18 pretrained on Places365
            model = models.resnet18(weights=None)
            model.fc = nn.Linear(model.fc.in_features, 365)

            # Load Places365 weights (if available, otherwise use ImageNet as fallback)
            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

            # Image preprocessing for Places365
            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]
                )
            ])

            # Scene categories related to lighting
            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"""

        # 1. CV-based physical features (including advanced features)
        cv_features = self._extract_cv_features(image)

        # 2. Places365 scene understanding (if available)
        scene_info = self._analyze_scene_places365(image)

        # 3. Determine lighting condition (adaptive with auxiliary features)
        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)

        # Basic Features (Primary)
        # Brightness (LAB L-channel)
        lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
        brightness = float(np.mean(lab[:, :, 0]))

        # Color temperature (R/B ratio)
        b_mean = np.mean(img_bgr[:, :, 0])
        r_mean = np.mean(img_bgr[:, :, 2])
        color_temp = float(r_mean / (b_mean + 1e-6))

        # Contrast (std of grayscale)
        gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
        contrast = float(np.std(gray))

        # Shadow ratio
        _, shadow_mask = cv2.threshold(gray, 80, 255, cv2.THRESH_BINARY_INV)
        shadow_ratio = float(np.sum(shadow_mask > 0) / shadow_mask.size)

        # Advanced Features
        # 1. First derivative: Sobel gradient magnitude (edge strength)
        # Strong gradients suggest directional lighting, weak suggest diffused
        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))

        # 2. Second derivative: Laplacian variance (lighting change detection)
        # High variance indicates complex lighting with many transitions
        laplacian = cv2.Laplacian(gray, cv2.CV_64F)
        laplacian_var = float(np.var(laplacian))

        # 3. Color difference in LAB space (color uniformity)
        # Low variation suggests overcast/diffused, high suggests mixed lighting
        a_std = float(np.std(lab[:, :, 1]))  # a* channel (green-red)
        b_std = float(np.std(lab[:, :, 2]))  # b* channel (blue-yellow)
        color_variation = (a_std + b_std) / 2

        return {
            # Primary features
            'brightness': brightness,
            'color_temp': color_temp,
            'contrast': contrast,
            'shadow_ratio': shadow_ratio,
            # Advanced auxiliary features (to assist Places365)
            '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)

                # Get top prediction
                top_prob, top_idx = torch.max(probs, 1)

                # Simple scene categories
                # Using index ranges for common outdoor scenes
                is_outdoor = top_idx.item() < 200  # Rough heuristic

                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

        # Signal 1: Places365 scene category (strongest signal)
        if scene_info.get('scene_category') == 'indoor':
            indoor_score += 0.5
        elif scene_info.get('scene_category') == 'outdoor':
            indoor_score -= 0.3

        # Signal 2: Brightness patterns
        # Indoor scenes typically have controlled brightness (not too bright, not too dark)
        brightness = cv_features['brightness']
        if 60 < brightness < 220:  # 放寬範圍,包含更多室內場景
            indoor_score += 0.15
        elif brightness > 230:  # Very bright suggests outdoor
            indoor_score -= 0.2

        # Signal 3: Low gradient suggests controlled/diffused indoor lighting
        gradient = cv_features['gradient_strength']
        if gradient < 20:  # 放寬閾值,更多室內場景符合
            indoor_score += 0.15

        # Signal 4: Low laplacian variance suggests smooth indoor lighting
        laplacian = cv_features['laplacian_variance']
        if laplacian < 400:  # 放寬閾值,包含更多室內場景
            indoor_score += 0.10

        # Signal 5: Shadow ratio - indoor scenes have less harsh shadows
        shadow_ratio = cv_features['shadow_ratio']
        if shadow_ratio < 0.25:  # 放寬閾值,包含更多室內場景
            indoor_score += 0.10
        elif shadow_ratio > 0.5:  # Strong shadows suggest outdoor sunlight
            indoor_score -= 0.15

        # Threshold: indoor if 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']

        # Normalize 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)

        scores = {}

        # Studio/Product Lighting (工作室/產品攝影燈光)
        # Very controlled, bright, minimal shadows, low gradient
        studio_score = (
            0.35 * (1.0 if brightness_norm > 0.6 else 0.5) +  # Bright
            0.25 * (1.0 - shadow_ratio) +                      # Minimal shadows
            0.20 * (1.0 - gradient_norm) +                     # Smooth, even
            0.15 * (1.0 - laplacian_norm) +                   # Very smooth
            0.05 * (1.0 - abs(color_temp - 1.0))              # Neutral temp
        )
        scores['studio lighting'] = studio_score

        # Indoor Natural Light (室內自然光 - 窗光)
        # Medium-bright, some contrast, neutral to warm temp
        natural_indoor_score = (
            0.30 * (1.0 if 0.5 < brightness_norm < 0.8 else 0.5) +  # Medium-bright
            0.25 * min(contrast_norm, 0.6) +                         # Some contrast
            0.20 * (1.0 if color_temp > 0.95 else 0.5) +            # Neutral to warm
            0.15 * min(gradient_norm, 0.5) +                         # Some direction
            0.10 * (1.0 if shadow_ratio < 0.3 else 0.5)             # Some shadows
        )
        scores['indoor natural light'] = natural_indoor_score

        # Warm Artificial Lighting (溫暖人工照明)
        # Warm color temp, medium brightness, soft
        warm_artificial_score = (
            0.35 * (1.0 if color_temp > 1.1 else 0.3) +              # Warm temp
            0.25 * (1.0 - abs(brightness_norm - 0.5)) +              # Medium brightness
            0.20 * (1.0 - gradient_norm) +                           # Soft
            0.15 * (1.0 - shadow_ratio) +                            # Minimal shadows
            0.05 * (1.0 - laplacian_norm)                            # Smooth
        )
        scores['warm artificial lighting'] = warm_artificial_score

        # Cool Artificial Lighting (冷色人工照明)
        # Cool/neutral temp, medium-bright
        cool_artificial_score = (
            0.35 * (1.0 if color_temp < 1.05 else 0.4) +            # Cool/neutral temp
            0.25 * (1.0 if brightness_norm > 0.5 else 0.5) +        # Medium-bright
            0.20 * (1.0 - gradient_norm) +                           # Smooth
            0.15 * (1.0 - shadow_ratio) +                            # Minimal shadows
            0.05 * (1.0 - laplacian_norm)                            # Even
        )
        scores['cool artificial lighting'] = cool_artificial_score

        # Soft Indoor Lighting (柔和室內光線)
        # Low contrast, diffused, medium brightness
        soft_indoor_score = (
            0.30 * (1.0 - abs(brightness_norm - 0.5)) +             # Medium brightness
            0.30 * (1.0 - contrast_norm) +                           # Low contrast
            0.20 * (1.0 - gradient_norm) +                           # Very soft
            0.15 * (1.0 - shadow_ratio) +                            # Minimal shadows
            0.05 * (1.0 - laplacian_norm)                            # Smooth
        )
        scores['soft indoor lighting'] = soft_indoor_score

        # Dramatic Indoor Lighting (戲劇性室內光線)
        # High contrast, directional, some shadows
        dramatic_score = (
            0.35 * contrast_norm +                                   # High contrast
            0.25 * gradient_norm +                                   # Directional
            0.20 * shadow_ratio +                                    # Shadows present
            0.15 * laplacian_norm +                                  # Sharp transitions
            0.05 * (1.0 if brightness_norm < 0.6 else 0.5)          # Can be darker
        )
        scores['dramatic indoor lighting'] = dramatic_score

        # Get best match
        best_condition = max(scores.items(), key=lambda x: x[1])

        # Calculate confidence
        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"""

        # Extract all features
        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']

        # NEW: Detect indoor vs outdoor
        is_indoor = self._detect_indoor_scene(cv_features, scene_info)
        if is_indoor:
            # 室內場景優先使用室內光線類型
            return self._determine_indoor_lighting(cv_features)
        # 否則使用原有邏輯

        # Normalize features to 0-1 scale
        brightness_norm = min(brightness / 255.0, 1.0)
        contrast_norm = min(contrast / 100.0, 1.0)
        gradient_norm = min(gradient / 50.0, 1.0)  # Typical range 0-50
        laplacian_norm = min(laplacian / 1000.0, 1.0)  # Typical range 0-1000
        color_var_norm = min(color_var / 50.0, 1.0)  # Typical range 0-50

        # Adaptive scoring (Places365 dominant, CV features assist)
        scores = {}

        # Soft diffused light (柔和漫射光)
        # Characteristics: medium brightness, low contrast, neutral temp
        # Auxiliary: low gradient (no strong edges), low laplacian (smooth transitions)
        diffuse_score = (
            0.40 * (1.0 - abs(brightness_norm - 0.5)) +  # Medium brightness
            0.25 * (1.0 - contrast_norm) +                # Low contrast
            0.20 * (1.0 - abs(color_temp - 1.0)) +       # Neutral temp
            0.08 * (1.0 - gradient_norm) +                # Weak edges (diffused)
            0.05 * (1.0 - laplacian_norm) +              # Smooth transitions
            0.02 * (1.0 - color_var_norm)                # Uniform color
        )
        scores['soft diffused light'] = diffuse_score

        # Natural daylight (自然光)
        # Characteristics: bright, moderate contrast
        # Auxiliary: moderate gradient, moderate color variation
        daylight_score = (
            0.40 * brightness_norm +                      # Bright
            0.25 * min(contrast_norm, 0.7) +             # Moderate contrast
            0.20 * (1.0 - abs(color_temp - 1.0)) +       # Neutral temp
            0.08 * min(gradient_norm, 0.6) +             # Moderate edges
            0.05 * min(laplacian_norm, 0.6) +            # Some detail
            0.02 * min(color_var_norm, 0.5)              # Some color variation
        )
        scores['natural daylight'] = daylight_score

        # Overcast atmosphere (陰天氛圍)
        # Characteristics: medium-low brightness, very low contrast, cool temp, minimal shadow
        # Auxiliary: very low gradient (flat), low laplacian, low color variation
        overcast_score = (
            0.35 * (1.0 - abs(brightness_norm - 0.45)) + # Medium-low brightness
            0.25 * (1.0 - contrast_norm) +                # Very low contrast
            0.15 * (1.0 if color_temp < 1.05 else 0.5) + # Cool temp
            0.10 * (1.0 - shadow) +                       # Minimal shadows
            0.08 * (1.0 - gradient_norm) +                # Flat appearance
            0.05 * (1.0 - laplacian_norm) +              # Smooth lighting
            0.02 * (1.0 - color_var_norm)                # Uniform color
        )
        scores['overcast atmosphere'] = overcast_score

        # Warm ambient light (溫暖環境光)
        # Characteristics: medium brightness, warm temp
        # Auxiliary: moderate gradient, warm color bias
        warm_score = (
            0.40 * (1.0 - abs(brightness_norm - 0.5)) +  # Medium brightness
            0.30 * (1.0 if color_temp > 1.1 else 0.5) +  # Warm temp
            0.15 * min(contrast_norm, 0.6) +             # Moderate contrast
            0.08 * min(gradient_norm, 0.5) +             # Soft edges
            0.05 * min(laplacian_norm, 0.5) +            # Soft transitions
            0.02 * color_var_norm                        # Some color variation (warmth)
        )
        scores['warm ambient light'] = warm_score

        # Evening light (傍晚光線)
        # Characteristics: medium-low brightness, warm temp, medium contrast
        # Auxiliary: moderate gradient (directional), some color variation
        evening_score = (
            0.35 * (1.0 if brightness_norm < 0.6 else 0.5) +  # Lower brightness
            0.30 * (1.0 if color_temp > 1.05 else 0.5) +      # Slightly warm
            0.20 * contrast_norm +                             # Some contrast
            0.08 * min(gradient_norm, 0.7) +                   # Directional light
            0.05 * laplacian_norm +                            # Detail present
            0.02 * color_var_norm                              # Color variation
        )
        scores['evening light'] = evening_score

        # Bright sunlight (明亮陽光)
        # Characteristics: high brightness, high contrast, strong shadows
        # Auxiliary: high gradient (strong edges), high laplacian (sharp transitions)
        sunlight_score = (
            0.40 * (1.0 if brightness_norm > 0.7 else 0.3) +  # High brightness
            0.25 * contrast_norm +                             # High contrast
            0.15 * shadow +                                    # Strong shadows
            0.10 * gradient_norm +                             # Strong edges
            0.08 * laplacian_norm +                            # Sharp detail
            0.02 * color_var_norm                              # Color variation
        )
        scores['bright sunlight'] = sunlight_score

        # Get top scoring condition
        best_condition = max(scores.items(), key=lambda x: x[1])

        # Calculate confidence based on score separation
        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")