File size: 17,747 Bytes
3172319
 
e6a18b7
 
3172319
 
e6a18b7
 
 
 
 
 
3172319
 
e6a18b7
 
 
 
 
 
3172319
 
 
 
e6a18b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3172319
 
e6a18b7
3172319
e6a18b7
 
 
 
 
 
 
 
3172319
e6a18b7
3172319
e6a18b7
 
 
 
3172319
 
4d1f920
e6a18b7
 
3172319
 
e6a18b7
 
 
 
 
 
 
 
 
 
3172319
 
e6a18b7
3172319
e6a18b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d1f920
3172319
 
 
e6a18b7
 
 
 
 
3172319
e6a18b7
3172319
e6a18b7
 
3172319
e6a18b7
 
4d1f920
e6a18b7
 
 
 
 
 
4d1f920
e6a18b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3172319
e6a18b7
 
 
 
 
 
 
 
 
 
 
 
 
3172319
e6a18b7
 
3172319
e6a18b7
 
 
4d1f920
e6a18b7
 
 
 
 
3172319
e6a18b7
 
 
 
3172319
e6a18b7
 
3172319
e6a18b7
 
 
 
 
 
 
 
 
 
 
 
 
4d1f920
e6a18b7
 
 
 
 
 
 
 
 
 
 
 
 
 
4d1f920
e6a18b7
 
 
 
 
 
4d1f920
e6a18b7
 
 
 
 
 
3172319
e6a18b7
 
3172319
e6a18b7
 
 
 
 
 
 
3172319
 
e6a18b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3172319
e6a18b7
 
 
 
 
 
 
3172319
 
e6a18b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3172319
e6a18b7
3172319
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
import numpy as np
import cv2
import logging
import traceback
from typing import Dict, Any, Optional

from configuration_manager import ConfigurationManager
from feature_extractor import FeatureExtractor
from indoor_outdoor_classifier import IndoorOutdoorClassifier
from lighting_condition_analyzer import LightingConditionAnalyzer


class LightingAnalyzer:
    """
    Comprehensive lighting analysis system facade that coordinates feature extraction,
    indoor/outdoor classification, and lighting condition determination.
    此class是一個總窗口,主要匯總各式光線分析相關的class

    This facade class maintains the original interface while internally delegating
    work to specialized components for improved maintainability and modularity.
    """

    def __init__(self, config: Optional[Dict[str, Any]] = None):
        """
        Initialize the lighting analyzer with configuration.

        Args:
            config: Optional configuration dictionary. If None, uses default configuration.
        """
        self.logger = self._setup_logger()

        try:
            # Initialize configuration manager
            self.config_manager = ConfigurationManager()

            # Override default configuration if provided
            if config is not None:
                self._update_configuration(config)

            # Initialize specialized components
            self.feature_extractor = FeatureExtractor(self.config_manager)
            self.indoor_outdoor_classifier = IndoorOutdoorClassifier(self.config_manager)
            self.lighting_condition_analyzer = LightingConditionAnalyzer(self.config_manager)

            # Legacy configuration access for backward compatibility
            self.config = self.config_manager.get_legacy_config_dict()

            self.logger.info("LightingAnalyzer initialized successfully")

        except Exception as e:
            self.logger.error(f"Error initializing LightingAnalyzer: {str(e)}")
            self.logger.error(f"Traceback: {traceback.format_exc()}")
            raise

    def _setup_logger(self) -> logging.Logger:
        """Set up logger for lighting analysis operations."""
        logger = logging.getLogger(f"{__name__}.LightingAnalyzer")
        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 _update_configuration(self, config: Dict[str, Any]) -> None:
        """
        Update configuration manager with provided configuration dictionary.

        Args:
            config: Configuration dictionary to update existing configuration.
        """
        try:
            # Update configuration through the manager's internal method
            self.config_manager._update_from_dict(config)
            self.logger.debug("Configuration updated successfully")

        except Exception as e:
            self.logger.warning(f"Error updating configuration: {str(e)}")
            self.logger.warning("Continuing with default configuration")

    def analyze(self, image, places365_info: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Analyze lighting conditions of an image.

        This is the main entry point that maintains compatibility with the original interface
        while leveraging the new modular architecture internally.

        Args:
            image: Input image (numpy array or PIL Image).
            places365_info: Optional Places365 classification information containing
                          scene type, confidence, attributes, and indoor/outdoor classification.

        Returns:
            Dictionary containing comprehensive lighting analysis results including:
            - time_of_day: Specific lighting condition classification
            - confidence: Confidence score for the classification
            - is_indoor: Boolean indicating indoor/outdoor classification
            - indoor_probability: Probability score for indoor classification
            - brightness: Brightness analysis metrics
            - color_info: Color characteristic analysis
            - texture_info: Texture and gradient analysis
            - structure_info: Structural feature analysis
            - diagnostics: Detailed diagnostic information (if enabled)
        """
        try:
            self.logger.debug("Starting comprehensive lighting analysis")

            # Step 1: Validate and preprocess input image
            processed_image = self._preprocess_image(image)
            if processed_image is None:
                return self._get_error_result("Invalid image input")

            # Step 2: Extract comprehensive features
            self.logger.debug("Extracting image features")
            features = self.feature_extractor.extract_features(processed_image)

            if not features or "avg_brightness" not in features:
                return self._get_error_result("Feature extraction failed")

            # Step 3: Classify indoor/outdoor with Places365 integration
            self.logger.debug("Performing indoor/outdoor classification")
            indoor_outdoor_result = self.indoor_outdoor_classifier.classify(
                features, places365_info
            )

            is_indoor = indoor_outdoor_result["is_indoor"]
            indoor_probability = indoor_outdoor_result["indoor_probability"]

            # Step 4: Determine specific lighting conditions
            self.logger.debug(f"Analyzing lighting conditions for {'indoor' if is_indoor else 'outdoor'} scene")
            lighting_result = self.lighting_condition_analyzer.analyze_lighting_conditions(
                features, is_indoor, places365_info
            )

            # Step 5: Consolidate comprehensive results
            result = self._consolidate_analysis_results(
                lighting_result, indoor_outdoor_result, features
            )

            self.logger.info(f"Analysis complete: {result['time_of_day']} "
                           f"({'indoor' if result['is_indoor'] else 'outdoor'}) "
                           f"confidence: {result['confidence']:.3f}")

            return result

        except Exception as e:
            self.logger.error(f"Error in lighting analysis: {str(e)}")
            self.logger.error(f"Traceback: {traceback.format_exc()}")
            return self._get_error_result(str(e))

    def _preprocess_image(self, image) -> Optional[np.ndarray]:
        """
        Preprocess input image to ensure consistent format for analysis.

        Args:
            image: Input image in various possible formats.

        Returns:
            Preprocessed image as RGB numpy array, or None if preprocessing failed.
        """
        try:
            # Convert to numpy array if needed
            if not isinstance(image, np.ndarray):
                image_np = np.array(image)
            else:
                image_np = image.copy()

            # Validate basic image properties
            if len(image_np.shape) < 2:
                self.logger.error("Image must have at least 2 dimensions")
                return None

            height, width = image_np.shape[:2]
            if height == 0 or width == 0:
                self.logger.error(f"Invalid image dimensions: {height}x{width}")
                return None

            # Handle different color formats and convert to RGB
            if len(image_np.shape) == 2:
                # 灰階 to RGB
                image_rgb = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
            elif image_np.shape[2] == 3:
                # Handle BGR vs RGB
                if not isinstance(image, np.ndarray):
                    # PIL images are typically RGB
                    image_rgb = image_np
                else:
                    # OpenCV arrays are typically BGR, convert to RGB
                    image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)
            elif image_np.shape[2] == 4:
                # RGBA to RGB
                if not isinstance(image, np.ndarray):
                    # PIL RGBA to RGB
                    image_rgb = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
                else:
                    # OpenCV BGRA to RGB
                    image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGRA2RGB)
            else:
                self.logger.error(f"Unsupported image format with shape: {image_np.shape}")
                return None

            # Ensure uint8 data type
            if image_rgb.dtype != np.uint8:
                if image_rgb.dtype in [np.float32, np.float64]:
                    # Assume normalized float values
                    if image_rgb.max() <= 1.0:
                        image_rgb = (image_rgb * 255).astype(np.uint8)
                    else:
                        image_rgb = image_rgb.astype(np.uint8)
                else:
                    image_rgb = image_rgb.astype(np.uint8)

            self.logger.debug(f"Preprocessed image: {image_rgb.shape}, dtype: {image_rgb.dtype}")
            return image_rgb

        except Exception as e:
            self.logger.error(f"Error preprocessing image: {str(e)}")
            return None

    def _consolidate_analysis_results(self, lighting_result: Dict[str, Any],
                                     indoor_outdoor_result: Dict[str, Any],
                                     features: Dict[str, Any]) -> Dict[str, Any]:
        """
        Consolidate results from all analysis components into final output format.

        Args:
            lighting_result: Results from lighting condition analysis.
            indoor_outdoor_result: Results from indoor/outdoor classification.
            features: Extracted image features.

        Returns:
            Consolidated analysis results in the expected output format.
        """
        # Extract core results
        time_of_day = lighting_result["time_of_day"]
        confidence = lighting_result["confidence"]
        is_indoor = indoor_outdoor_result["is_indoor"]
        indoor_probability = indoor_outdoor_result["indoor_probability"]

        # Organize brightness information
        brightness_info = {
            "average": float(features.get("avg_brightness", 0.0)),
            "std_dev": float(features.get("brightness_std", 0.0)),
            "dark_ratio": float(features.get("dark_pixel_ratio", 0.0)),
            "bright_ratio": float(features.get("bright_pixel_ratio", 0.0))
        }

        # Organize color information
        color_info = {
            "blue_ratio": float(features.get("blue_ratio", 0.0)),
            "sky_like_blue_ratio": float(features.get("sky_like_blue_ratio", 0.0)),
            "yellow_orange_ratio": float(features.get("yellow_orange_ratio", 0.0)),
            "gray_ratio": float(features.get("gray_ratio", 0.0)),
            "avg_saturation": float(features.get("avg_saturation", 0.0)),
            "sky_region_brightness_ratio": float(features.get("sky_region_brightness_ratio", 1.0)),
            "sky_region_saturation": float(features.get("sky_region_saturation", 0.0)),
            "sky_region_blue_dominance": float(features.get("sky_region_blue_dominance", 0.0)),
            "color_atmosphere": features.get("color_atmosphere", "neutral"),
            "warm_ratio": float(features.get("warm_ratio", 0.0)),
            "cool_ratio": float(features.get("cool_ratio", 0.0))
        }

        # Organize texture information
        texture_info = {
            "gradient_ratio_vertical_horizontal": float(features.get("gradient_ratio_vertical_horizontal", 0.0)),
            "top_region_texture_complexity": float(features.get("top_region_texture_complexity", 0.0)),
            "shadow_clarity_score": float(features.get("shadow_clarity_score", 0.5))
        }

        # Organize structure information
        structure_info = {
            "ceiling_likelihood": float(features.get("ceiling_likelihood", 0.0)),
            "boundary_clarity": float(features.get("boundary_clarity", 0.0)),
            "openness_top_edge": float(features.get("openness_top_edge", 0.5))
        }

        # Compile final result
        result = {
            "time_of_day": time_of_day,
            "confidence": float(confidence),
            "is_indoor": is_indoor,
            "indoor_probability": float(indoor_probability),
            "brightness": brightness_info,
            "color_info": color_info,
            "texture_info": texture_info,
            "structure_info": structure_info
        }

        # Add diagnostic information if enabled
        if self.config_manager.algorithm_parameters.include_diagnostics:
            diagnostics = {}

            # Combine diagnostics from all components
            if "diagnostics" in lighting_result:
                diagnostics["lighting_diagnostics"] = lighting_result["diagnostics"]

            if "diagnostics" in indoor_outdoor_result:
                diagnostics["indoor_outdoor_diagnostics"] = indoor_outdoor_result["diagnostics"]

            if "feature_contributions" in indoor_outdoor_result:
                diagnostics["feature_contributions"] = indoor_outdoor_result["feature_contributions"]

            result["diagnostics"] = diagnostics

        return result

    def _get_error_result(self, error_message: str) -> Dict[str, Any]:
        """
        Generate standardized error result format.

        Args:
            error_message: Description of the error that occurred.

        Returns:
            Dictionary containing error result with safe default values.
        """
        return {
            "time_of_day": "unknown",
            "confidence": 0.0,
            "is_indoor": False,
            "indoor_probability": 0.5,
            "brightness": {
                "average": 100.0,
                "std_dev": 50.0,
                "dark_ratio": 0.0,
                "bright_ratio": 0.0
            },
            "color_info": {
                "blue_ratio": 0.0,
                "sky_like_blue_ratio": 0.0,
                "yellow_orange_ratio": 0.0,
                "gray_ratio": 0.0,
                "avg_saturation": 100.0,
                "sky_region_brightness_ratio": 1.0,
                "sky_region_saturation": 0.0,
                "sky_region_blue_dominance": 0.0,
                "color_atmosphere": "neutral",
                "warm_ratio": 0.0,
                "cool_ratio": 0.0
            },
            "texture_info": {
                "gradient_ratio_vertical_horizontal": 1.0,
                "top_region_texture_complexity": 0.5,
                "shadow_clarity_score": 0.5
            },
            "structure_info": {
                "ceiling_likelihood": 0.0,
                "boundary_clarity": 0.0,
                "openness_top_edge": 0.5
            },
            "error": error_message
        }

    def get_configuration(self) -> Dict[str, Any]:
        """
        Get current configuration as dictionary for backward compatibility.

        Returns:
            Dictionary containing all current configuration parameters.
        """
        return self.config_manager.get_legacy_config_dict()

    def update_configuration(self, config_updates: Dict[str, Any]) -> None:
        """
        Update configuration parameters.

        Args:
            config_updates: Dictionary containing configuration parameters to update.
        """
        try:
            self.config_manager._update_from_dict(config_updates)
            # Update legacy config reference
            self.config = self.config_manager.get_legacy_config_dict()
            self.logger.info("Configuration updated successfully")

        except Exception as e:
            self.logger.error(f"Error updating configuration: {str(e)}")
            raise

    def validate_configuration(self) -> bool:
        """
        Validate current configuration for logical consistency.

        Returns:
            True if configuration is valid, False otherwise.
        """
        try:
            validation_errors = self.config_manager.validate_configuration()

            if validation_errors:
                self.logger.error("Configuration validation failed:")
                for error in validation_errors:
                    self.logger.error(f"  - {error}")
                return False

            self.logger.info("Configuration validation passed")
            return True

        except Exception as e:
            self.logger.error(f"Error during configuration validation: {str(e)}")
            return False

    def save_configuration(self, filepath: str) -> None:
        """
        Save current configuration to file.

        Args:
            filepath: Path where to save the configuration file.
        """
        try:
            self.config_manager.save_to_file(filepath)
            self.logger.info(f"Configuration saved to {filepath}")

        except Exception as e:
            self.logger.error(f"Error saving configuration: {str(e)}")
            raise

    def load_configuration(self, filepath: str) -> None:
        """
        Load configuration from file.

        Args:
            filepath: Path to the configuration file to load.
        """
        try:
            self.config_manager.load_from_file(filepath)
            # Update legacy config reference
            self.config = self.config_manager.get_legacy_config_dict()
            self.logger.info(f"Configuration loaded from {filepath}")

        except Exception as e:
            self.logger.error(f"Error loading configuration: {str(e)}")
            raise