File size: 35,403 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
import numpy as np
import cv2
import logging
import traceback
from typing import Dict, Any, Optional
from configuration_manager import ConfigurationManager


class FeatureExtractor:
    """
    Extracts comprehensive lighting and scene features from images.(主要從圖片提取光線資訊)

    This class handles all basic feature computation including brightness analysis,
    color characteristics, texture complexity, and structural features for
    lighting analysis and scene understanding.
    """

    def __init__(self, config_manager: ConfigurationManager):
        """
        Initialize the feature extractor.

        Args:
            config_manager: Configuration manager instance for accessing thresholds.
        """
        self.config_manager = config_manager
        self.logger = self._setup_logger()

    def _setup_logger(self) -> logging.Logger:
        """Set up logger for feature extraction operations."""
        logger = logging.getLogger(f"{__name__}.FeatureExtractor")
        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 extract_features(self, image_rgb: np.ndarray) -> Dict[str, Any]:
        """
        Extract all features from an RGB image.

        Args:
            image_rgb: Input image as numpy array in RGB format.

        Returns:
            Dictionary containing all extracted features.
        """
        try:
            # Validate input image
            if not self._validate_image(image_rgb):
                return self._get_default_features()

            # Get image dimensions and prepare processing parameters
            height, width = image_rgb.shape[:2]
            scale_factor = self._calculate_scale_factor(height, width)

            # Create processed image versions
            small_rgb = cv2.resize(
                image_rgb,
                (width // scale_factor, height // scale_factor),
                interpolation=cv2.INTER_AREA
            )
            hsv_img = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2HSV)
            gray_img = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2GRAY)
            small_gray = cv2.cvtColor(small_rgb, cv2.COLOR_RGB2GRAY)

            # Extract features by category
            brightness_features = self.compute_brightness_features(hsv_img, height, width)
            color_features = self.compute_color_features(hsv_img, height, width)
            texture_features = self.compute_texture_features(small_gray, gray_img, height, width)
            structure_features = self.compute_structure_features(
                small_gray, gray_img, hsv_img, height, width, scale_factor
            )

            # Combine all features
            features = {**brightness_features, **color_features, **texture_features, **structure_features}

            # Add compatibility features for legacy code
            legacy_features = self._compute_legacy_compatibility_features(
                hsv_img, small_gray, features, scale_factor
            )
            features.update(legacy_features)

            self.logger.debug(f"Successfully extracted {len(features)} features from image")
            return features

        except Exception as e:
            self.logger.error(f"Error in feature extraction: {str(e)}")
            self.logger.error(f"Traceback: {traceback.format_exc()}")
            return self._get_default_features()

    def compute_brightness_features(self, hsv_img: np.ndarray, height: int, width: int) -> Dict[str, float]:
        """
        Compute brightness-related features from HSV image.

        Args:
            hsv_img: Image in HSV color space.
            height: Image height.
            width: Image width.

        Returns:
            Dictionary containing brightness features.
        """
        try:
            v_channel = hsv_img[:, :, 2]  # Value channel represents brightness

            # 基本的亮度統計
            avg_brightness = float(np.mean(v_channel))
            brightness_std = float(np.std(v_channel))

            # Pixel ratio calculations
            dark_threshold = self.config_manager.feature_thresholds.dark_pixel_threshold
            bright_threshold = self.config_manager.feature_thresholds.bright_pixel_threshold

            total_pixels = height * width
            dark_pixel_ratio = float(np.sum(v_channel < dark_threshold) / total_pixels)
            bright_pixel_ratio = float(np.sum(v_channel > bright_threshold) / total_pixels)

            # Brightness uniformity
            brightness_uniformity = 1.0 - min(1.0, brightness_std / max(avg_brightness, 1e-5))

            return {
                "avg_brightness": avg_brightness,
                "brightness_std": brightness_std,
                "dark_pixel_ratio": dark_pixel_ratio,
                "bright_pixel_ratio": bright_pixel_ratio,
                "brightness_uniformity": brightness_uniformity
            }

        except Exception as e:
            self.logger.error(f"Error computing brightness features: {str(e)}")
            return {
                "avg_brightness": 100.0,
                "brightness_std": 50.0,
                "dark_pixel_ratio": 0.0,
                "bright_pixel_ratio": 0.0,
                "brightness_uniformity": 0.5
            }

    def compute_color_features(self, hsv_img: np.ndarray, height: int, width: int) -> Dict[str, Any]:
        """
        Compute color-related features from HSV image.

        Args:
            hsv_img: Image in HSV color space.
            height: Image height.
            width: Image width.

        Returns:
            Dictionary containing color features.
        """
        try:
            h_channel, s_channel, v_channel = cv2.split(hsv_img)
            total_pixels = height * width

            # Color ratio calculations
            color_features = {}

            # Blue color detection (general and sky-specific)
            blue_mask = ((h_channel >= 90) & (h_channel <= 140))
            color_features["blue_ratio"] = float(np.sum(blue_mask) / total_pixels)

            # Sky-like blue detection
            ft = self.config_manager.feature_thresholds
            sky_blue_mask = (
                (h_channel >= ft.sky_blue_hue_min) & (h_channel <= ft.sky_blue_hue_max) &
                (s_channel > ft.sky_blue_sat_min) & (v_channel > ft.sky_blue_val_min)
            )
            color_features["sky_like_blue_ratio"] = float(np.sum(sky_blue_mask) / total_pixels)

            # Yellow-orange detection
            yellow_orange_mask = ((h_channel >= 15) & (h_channel <= 45))
            color_features["yellow_orange_ratio"] = float(np.sum(yellow_orange_mask) / total_pixels)

            # Gray detection
            gray_mask = (
                (s_channel < ft.gray_sat_max) &
                (v_channel > ft.gray_val_min) &
                (v_channel < ft.gray_val_max)
            )
            color_features["gray_ratio"] = float(np.sum(gray_mask) / total_pixels)

            # Saturation statistics
            color_features["avg_saturation"] = float(np.mean(s_channel))

            # Sky region analysis
            sky_region_features = self._analyze_sky_region(h_channel, s_channel, v_channel, height)
            color_features.update(sky_region_features)

            # Color atmosphere analysis
            atmosphere_features = self._analyze_color_atmosphere(h_channel, s_channel, total_pixels)
            color_features.update(atmosphere_features)

            return color_features

        except Exception as e:
            self.logger.error(f"Error computing color features: {str(e)}")
            return self._get_default_color_features()

    def compute_texture_features(self, small_gray: np.ndarray, gray_img: np.ndarray,
                                height: int, width: int) -> Dict[str, float]:
        """
        Compute texture and gradient features.

        Args:
            small_gray: Downscaled grayscale image for efficient processing.
            gray_img: Full-resolution grayscale image.
            height: Original image height.
            width: Original image width.

        Returns:
            Dictionary containing texture features.
        """
        try:
            # Compute gradients on small image for efficiency
            gx = cv2.Sobel(small_gray, cv2.CV_32F, 1, 0, ksize=3)
            gy = cv2.Sobel(small_gray, cv2.CV_32F, 0, 1, ksize=3)

            avg_abs_gx = float(np.mean(np.abs(gx)))
            avg_abs_gy = float(np.mean(np.abs(gy)))

            # Gradient ratio (vertical to horizontal)
            gradient_ratio_vertical_horizontal = avg_abs_gy / max(avg_abs_gx, 1e-5)

            # Top region texture complexity
            small_top_third_height = small_gray.shape[0] // 3
            small_sky_region_gray = small_gray[:small_top_third_height, :]

            if small_sky_region_gray.size > 0:
                laplacian_var_sky = cv2.Laplacian(small_sky_region_gray, cv2.CV_64F).var()
                top_region_texture_complexity = min(1.0, laplacian_var_sky / 1000.0)
            else:
                top_region_texture_complexity = 0.5

            # Shadow clarity estimation
            brightness_std = float(np.std(gray_img))
            avg_brightness = float(np.mean(gray_img))
            dark_pixel_ratio = float(np.sum(gray_img < 50) / (height * width))

            if brightness_std > 60 and dark_pixel_ratio < 0.15 and avg_brightness > 100:
                shadow_clarity_score = 0.7
            elif brightness_std < 30 and dark_pixel_ratio > 0.1:
                shadow_clarity_score = 0.3
            else:
                shadow_clarity_score = 0.5

            # Edge density
            edges_density = min(1.0, (avg_abs_gx + avg_abs_gy) / 100.0)

            return {
                "gradient_ratio_vertical_horizontal": gradient_ratio_vertical_horizontal,
                "top_region_texture_complexity": top_region_texture_complexity,
                "shadow_clarity_score": shadow_clarity_score,
                "vertical_strength": avg_abs_gy,
                "horizontal_strength": avg_abs_gx,
                "edges_density": edges_density
            }

        except Exception as e:
            self.logger.error(f"Error computing texture features: {str(e)}")
            return {
                "gradient_ratio_vertical_horizontal": 1.0,
                "top_region_texture_complexity": 0.5,
                "shadow_clarity_score": 0.5,
                "vertical_strength": 0.0,
                "horizontal_strength": 0.0,
                "edges_density": 0.0
            }

    def compute_structure_features(self, small_gray: np.ndarray, gray_img: np.ndarray,
                                  hsv_img: np.ndarray, height: int, width: int,
                                  scale_factor: int) -> Dict[str, float]:
        """
        Compute structural features including ceiling likelihood and boundary clarity.

        Args:
            small_gray: Downscaled grayscale image.
            gray_img: Full-resolution grayscale image.
            hsv_img: HSV image for brightness analysis.
            height: Original image height.
            width: Original image width.
            scale_factor: Downscaling factor used.

        Returns:
            Dictionary containing structural features.
        """
        try:
            # Compute gradients
            gx = cv2.Sobel(small_gray, cv2.CV_32F, 1, 0, ksize=3)
            gy = cv2.Sobel(small_gray, cv2.CV_32F, 0, 1, ksize=3)
            avg_abs_gx = float(np.mean(np.abs(gx)))
            avg_abs_gy = float(np.mean(np.abs(gy)))

            # Ceiling likelihood analysis
            ceiling_features = self._analyze_ceiling_likelihood(
                small_gray, hsv_img, gx, avg_abs_gx, height, scale_factor
            )

            # Boundary clarity analysis
            boundary_clarity = self._compute_boundary_clarity(small_gray, avg_abs_gx, avg_abs_gy)

            # Openness analysis
            openness_top_edge = self._compute_openness_top_edge(gy, avg_abs_gy)

            # Legacy compatibility features
            legacy_structure = self._compute_legacy_structure_features(gray_img, height)

            structure_features = {
                "ceiling_likelihood": ceiling_features["ceiling_likelihood"],
                "boundary_clarity": boundary_clarity,
                "openness_top_edge": openness_top_edge,
                **legacy_structure
            }

            return structure_features

        except Exception as e:
            self.logger.error(f"Error computing structure features: {str(e)}")
            return {
                "ceiling_likelihood": 0.0,
                "boundary_clarity": 0.0,
                "openness_top_edge": 0.5,
                "ceiling_uniformity": 0.5,
                "horizontal_line_ratio": 0.0
            }

    def _analyze_sky_region(self, h_channel: np.ndarray, s_channel: np.ndarray,
                           v_channel: np.ndarray, height: int) -> Dict[str, float]:
        """Analyze features specific to the sky region (top third of image)."""
        try:
            top_third_height = height // 3
            sky_region_v = v_channel[:top_third_height, :]
            sky_region_s = s_channel[:top_third_height, :]
            sky_region_h = h_channel[:top_third_height, :]

            if sky_region_v.size == 0:
                return self._get_default_sky_features()

            # Sky region brightness analysis
            sky_region_avg_brightness = float(np.mean(sky_region_v))
            overall_avg_brightness = float(np.mean(v_channel))
            sky_region_brightness_ratio = sky_region_avg_brightness / max(overall_avg_brightness, 1e-5)
            sky_region_saturation = float(np.mean(sky_region_s))

            # Sky blue dominance in sky region
            ft = self.config_manager.feature_thresholds
            sky_region_blue_pixels = np.sum(
                (sky_region_h >= ft.sky_blue_hue_min) & (sky_region_h <= ft.sky_blue_hue_max) &
                (sky_region_s > ft.sky_blue_sat_min) & (sky_region_v > ft.sky_blue_val_min)
            )
            sky_region_blue_dominance = float(sky_region_blue_pixels / max(1, sky_region_v.size))

            return {
                "sky_region_brightness_ratio": sky_region_brightness_ratio,
                "sky_region_saturation": sky_region_saturation,
                "sky_region_blue_dominance": sky_region_blue_dominance,
                "sky_brightness": sky_region_avg_brightness
            }

        except Exception as e:
            self.logger.error(f"Error analyzing sky region: {str(e)}")
            return self._get_default_sky_features()

    def _analyze_color_atmosphere(self, h_channel: np.ndarray, s_channel: np.ndarray,
                                 total_pixels: int) -> Dict[str, Any]:
        """Analyze warm/cool color atmosphere."""
        try:
            cr = self.config_manager.color_ranges

            # Warm colors detection
            warm_mask = np.zeros_like(h_channel, dtype=bool)
            for h_min, h_max in cr.warm_hue_ranges:
                warm_mask |= ((h_channel >= h_min) & (h_channel <= h_max))
            warm_ratio = float(np.sum(warm_mask & (s_channel > 30)) / total_pixels)

            # Cool colors detection
            cool_mask = np.zeros_like(h_channel, dtype=bool)
            for h_min, h_max in cr.cool_hue_ranges:
                cool_mask |= ((h_channel >= h_min) & (h_channel <= h_max))
            cool_ratio = float(np.sum(cool_mask & (s_channel > 30)) / total_pixels)

            # Determine overall atmosphere
            if warm_ratio > cool_ratio and warm_ratio > 0.3:
                color_atmosphere = "warm"
            elif cool_ratio > warm_ratio and cool_ratio > 0.3:
                color_atmosphere = "cool"
            else:
                color_atmosphere = "neutral"

            return {
                "warm_ratio": warm_ratio,
                "cool_ratio": cool_ratio,
                "color_atmosphere": color_atmosphere
            }

        except Exception as e:
            self.logger.error(f"Error analyzing color atmosphere: {str(e)}")
            return {
                "warm_ratio": 0.0,
                "cool_ratio": 0.0,
                "color_atmosphere": "neutral"
            }

    def _analyze_ceiling_likelihood(self, small_gray: np.ndarray, hsv_img: np.ndarray,
                                   gx: np.ndarray, avg_abs_gx: float, height: int,
                                   scale_factor: int) -> Dict[str, float]:
        """Analyze likelihood of ceiling presence."""
        try:
            ceiling_likelihood = 0.0
            config = self.config_manager.indoor_outdoor_thresholds

            # Get sky region brightness for analysis
            v_channel = hsv_img[:, :, 2]
            top_third_height = height // 3
            sky_region_v = v_channel[:top_third_height, :]
            sky_region_avg_brightness = float(np.mean(sky_region_v)) if sky_region_v.size > 0 else 0

            # Get top region texture complexity
            small_top_third_height = small_gray.shape[0] // 3
            small_sky_region_gray = small_gray[:small_top_third_height, :]

            if small_sky_region_gray.size > 0:
                laplacian_var = cv2.Laplacian(small_sky_region_gray, cv2.CV_64F).var()
                top_region_texture_complexity = min(1.0, laplacian_var / 1000.0)
            else:
                top_region_texture_complexity = 0.5

            # Condition 1: Simple texture and moderate brightness
            ceiling_texture_thresh = getattr(config, 'ceiling_texture_thresh', 0.4)
            ceiling_brightness_min = getattr(config, 'ceiling_brightness_min', 60)
            ceiling_brightness_max = getattr(config, 'ceiling_brightness_max', 230)

            if (top_region_texture_complexity < ceiling_texture_thresh and
                ceiling_brightness_min < sky_region_avg_brightness < ceiling_brightness_max):
                ceiling_likelihood += 0.45

            # Condition 2: Horizontal line strength
            top_horizontal_lines_strength = float(np.mean(np.abs(gx[:small_gray.shape[0]//3, :])))
            ceiling_horizontal_line_factor = getattr(config, 'ceiling_horizontal_line_factor', 1.15)

            if top_horizontal_lines_strength > avg_abs_gx * ceiling_horizontal_line_factor:
                ceiling_likelihood += 0.35

            # Condition 3: Central bright spot (lamp detection)
            center_y_sm, center_x_sm = small_gray.shape[0]//2, small_gray.shape[1]//2
            lamp_check_radius_y = small_gray.shape[0] // 8
            lamp_check_radius_x = small_gray.shape[1] // 8

            center_region = small_gray[
                max(0, center_y_sm - lamp_check_radius_y):min(small_gray.shape[0], center_y_sm + lamp_check_radius_y),
                max(0, center_x_sm - lamp_check_radius_x):min(small_gray.shape[1], center_x_sm + lamp_check_radius_x)
            ]

            if center_region.size > 0:
                avg_brightness = float(np.mean(small_gray))
                center_brightness = float(np.mean(center_region))
                ceiling_center_bright_factor = getattr(config, 'ceiling_center_bright_factor', 1.25)

                if center_brightness > avg_brightness * ceiling_center_bright_factor:
                    ceiling_likelihood += 0.30

            # Sky dominance analysis for penalty
            sky_region_blue_dominance = self._compute_sky_blue_dominance(hsv_img, height)
            sky_region_brightness_ratio = sky_region_avg_brightness / max(float(np.mean(v_channel)), 1e-5)

            # Penalties for strong sky signals
            ceiling_max_sky_blue_thresh = getattr(config, 'ceiling_max_sky_blue_thresh', 0.08)
            ceiling_max_sky_brightness_ratio = getattr(config, 'ceiling_max_sky_brightness_ratio', 1.15)

            if (sky_region_blue_dominance < ceiling_max_sky_blue_thresh and
                sky_region_brightness_ratio < ceiling_max_sky_brightness_ratio):
                ceiling_likelihood += 0.15

            # Strong sky override
            sky_blue_dominance_strong_thresh = getattr(config, 'sky_blue_dominance_strong_thresh', 0.25)
            sky_brightness_strong_thresh = getattr(config, 'sky_brightness_strong_thresh', 1.25)
            ceiling_sky_override_factor = getattr(config, 'ceiling_sky_override_factor', 0.1)

            if (sky_region_blue_dominance > sky_blue_dominance_strong_thresh and
                sky_region_brightness_ratio > sky_brightness_strong_thresh):
                ceiling_likelihood *= ceiling_sky_override_factor

            ceiling_likelihood = min(1.0, ceiling_likelihood)

            return {"ceiling_likelihood": ceiling_likelihood}

        except Exception as e:
            self.logger.error(f"Error analyzing ceiling likelihood: {str(e)}")
            return {"ceiling_likelihood": 0.0}

    def _compute_sky_blue_dominance(self, hsv_img: np.ndarray, height: int) -> float:
        """Compute blue dominance in sky region."""
        try:
            h_channel, s_channel, v_channel = cv2.split(hsv_img)
            top_third_height = height // 3
            sky_region_h = h_channel[:top_third_height, :]
            sky_region_s = s_channel[:top_third_height, :]
            sky_region_v = v_channel[:top_third_height, :]

            if sky_region_h.size == 0:
                return 0.0

            ft = self.config_manager.feature_thresholds
            sky_region_blue_pixels = np.sum(
                (sky_region_h >= ft.sky_blue_hue_min) & (sky_region_h <= ft.sky_blue_hue_max) &
                (sky_region_s > ft.sky_blue_sat_min) & (sky_region_v > ft.sky_blue_val_min)
            )

            return float(sky_region_blue_pixels / max(1, sky_region_h.size))

        except Exception as e:
            self.logger.error(f"Error computing sky blue dominance: {str(e)}")
            return 0.0

    def _compute_boundary_clarity(self, small_gray: np.ndarray, avg_abs_gx: float,
                                 avg_abs_gy: float) -> float:
        """Compute boundary clarity score."""
        try:
            edge_width_sm = max(1, small_gray.shape[1] // 10)
            edge_height_sm = max(1, small_gray.shape[0] // 10)

            # Edge gradients
            left_edge_grad_x = 0.0
            right_edge_grad_x = 0.0
            top_edge_grad_y = 0.0

            if small_gray.shape[1] > edge_width_sm:
                left_edge = small_gray[:, :edge_width_sm]
                right_edge = small_gray[:, -edge_width_sm:]
                left_edge_grad_x = float(np.mean(np.abs(cv2.Sobel(left_edge, cv2.CV_32F, 1, 0, ksize=3))))
                right_edge_grad_x = float(np.mean(np.abs(cv2.Sobel(right_edge, cv2.CV_32F, 1, 0, ksize=3))))

            if small_gray.shape[0] > edge_height_sm:
                top_edge = small_gray[:edge_height_sm, :]
                top_edge_grad_y = float(np.mean(np.abs(cv2.Sobel(top_edge, cv2.CV_32F, 0, 1, ksize=3))))

            # Normalize against average gradients
            boundary_clarity = (left_edge_grad_x + right_edge_grad_x + top_edge_grad_y) / (
                3 * max(avg_abs_gx, avg_abs_gy, 1e-5)
            )
            boundary_clarity = min(1.0, boundary_clarity / 1.5)

            return boundary_clarity

        except Exception as e:
            self.logger.error(f"Error computing boundary clarity: {str(e)}")
            return 0.0

    def _compute_openness_top_edge(self, gy: np.ndarray, avg_abs_gy: float) -> float:
        """Compute openness of top edge."""
        try:
            top_edge_strip_gy = float(np.mean(np.abs(gy[:max(1, gy.shape[0]//20), :])))
            openness_top_edge = 1.0 - min(1.0, top_edge_strip_gy / max(avg_abs_gy, 1e-5) / 0.5)
            return openness_top_edge
        except Exception as e:
            self.logger.error(f"Error computing top edge openness: {str(e)}")
            return 0.5

    def _compute_legacy_compatibility_features(self, hsv_img: np.ndarray, small_gray: np.ndarray,
                                             features: Dict[str, Any], scale_factor: int) -> Dict[str, Any]:
        """Compute additional features for backward compatibility."""
        try:
            v_channel = hsv_img[:, :, 2]

            # Light source detection
            light_features = self._detect_light_sources(v_channel, features["avg_brightness"],
                                                       features["brightness_std"], scale_factor)

            # Street line detection
            street_score = self._compute_street_line_score(small_gray)

            # Additional legacy features
            legacy_features = {
                **light_features,
                "street_line_score": street_score,
                "sky_blue_ratio": features.get("sky_like_blue_ratio", 0.0),  # Alias
                "gradient_ratio": features.get("gradient_ratio_vertical_horizontal", 1.0)  # Alias
            }

            return legacy_features

        except Exception as e:
            self.logger.error(f"Error computing legacy compatibility features: {str(e)}")
            return {}

    def _detect_light_sources(self, v_channel: np.ndarray, avg_brightness: float,
                             brightness_std: float, scale_factor: int) -> Dict[str, float]:
        """Detect artificial light sources in the image."""
        try:
            # Sample pixels for efficiency
            sampled_v = v_channel[::scale_factor*2, ::scale_factor*2]

            # Light threshold
            light_threshold = min(
                self.config_manager.feature_thresholds.light_source_abs_thresh,
                avg_brightness + 2 * brightness_std
            )

            is_bright_spots = sampled_v > light_threshold
            bright_spot_count = int(np.sum(is_bright_spots))

            # Initialize light features
            circular_light_count = 0
            indoor_light_score = 0.0
            light_distribution_uniformity = 0.5

            # Analyze light distribution if spots are found
            if 1 < bright_spot_count < 20:
                bright_y, bright_x = np.where(is_bright_spots)
                if len(bright_y) > 1:
                    mean_x, mean_y = np.mean(bright_x), np.mean(bright_y)
                    dist_from_center = np.sqrt((bright_x - mean_x)**2 + (bright_y - mean_y)**2)

                    if np.std(dist_from_center) < np.mean(dist_from_center):
                        circular_light_count = min(3, len(bright_y) // 2)
                        light_distribution_uniformity = 0.7

                    if np.mean(bright_y) < sampled_v.shape[0] / 2:
                        indoor_light_score = 0.6
                    else:
                        indoor_light_score = 0.3

            return {
                "bright_spot_count": bright_spot_count,
                "circular_light_count": circular_light_count,
                "indoor_light_score": indoor_light_score,
                "light_distribution_uniformity": light_distribution_uniformity
            }

        except Exception as e:
            self.logger.error(f"Error detecting light sources: {str(e)}")
            return {
                "bright_spot_count": 0,
                "circular_light_count": 0,
                "indoor_light_score": 0.0,
                "light_distribution_uniformity": 0.5
            }

    def _compute_street_line_score(self, small_gray: np.ndarray) -> float:
        """Compute street line detection score."""
        try:
            street_line_score = 0.0
            bottom_half_sm = small_gray[small_gray.shape[0]//2:, :]

            if bottom_half_sm.size > 0:
                bottom_vert_gradient = cv2.Sobel(bottom_half_sm, cv2.CV_32F, 0, 1, ksize=3)
                strong_vert_lines = np.abs(bottom_vert_gradient) > 50

                if np.sum(strong_vert_lines) > (bottom_half_sm.size * 0.05):
                    street_line_score = 0.7

            return street_line_score

        except Exception as e:
            self.logger.error(f"Error computing street line score: {str(e)}")
            return 0.0

    def _compute_legacy_structure_features(self, gray_img: np.ndarray, height: int) -> Dict[str, float]:
        """Compute legacy structure features for backward compatibility."""
        try:
            # Top region analysis for ceiling uniformity
            top_region = gray_img[:height//4, :]
            top_region_std = float(np.std(top_region)) if top_region.size > 0 else 0.0
            ceiling_uniformity = 1.0 - min(1.0, top_region_std / max(float(np.mean(top_region)) if top_region.size > 0 else 1e-5, 1e-5))

            # Horizontal line detection in top region
            if top_region.size > 0:
                top_gradients = np.abs(cv2.Sobel(top_region, cv2.CV_32F, 0, 1, ksize=3))
                horizontal_lines_strength = float(np.mean(top_gradients))
                horizontal_line_ratio = min(1.0, horizontal_lines_strength / 40.0)
            else:
                horizontal_line_ratio = 0.0

            # Boundary edge score computation
            boundary_edge_score = self._compute_legacy_boundary_score(gray_img)

            return {
                "ceiling_uniformity": ceiling_uniformity,
                "horizontal_line_ratio": horizontal_line_ratio,
                "top_region_std": top_region_std,
                "boundary_edge_score": boundary_edge_score
            }

        except Exception as e:
            self.logger.error(f"Error computing legacy structure features: {str(e)}")
            return {
                "ceiling_uniformity": 0.5,
                "horizontal_line_ratio": 0.0,
                "top_region_std": 0.0,
                "boundary_edge_score": 0.0
            }

    def _compute_legacy_boundary_score(self, gray_img: np.ndarray) -> float:
        """Compute legacy boundary edge score."""
        try:
            height, width = gray_img.shape

            # Create small version for boundary analysis
            small_height, small_width = height // 4, width // 4
            small_gray = cv2.resize(gray_img, (small_width, small_height), interpolation=cv2.INTER_AREA)

            # Edge regions
            left_edge_sm = small_gray[:, :small_width//6] if small_width > 6 else small_gray
            right_edge_sm = small_gray[:, 5*small_width//6:] if small_width > 6 else small_gray
            top_edge_sm = small_gray[:small_height//6, :] if small_height > 6 else small_gray

            # Compute gradients for each edge
            left_gradient = float(np.mean(np.abs(cv2.Sobel(left_edge_sm, cv2.CV_32F, 1, 0, ksize=3)))) if left_edge_sm.size > 0 else 0
            right_gradient = float(np.mean(np.abs(cv2.Sobel(right_edge_sm, cv2.CV_32F, 1, 0, ksize=3)))) if right_edge_sm.size > 0 else 0
            top_gradient = float(np.mean(np.abs(cv2.Sobel(top_edge_sm, cv2.CV_32F, 0, 1, ksize=3)))) if top_edge_sm.size > 0 else 0

            # Combine and normalize
            boundary_edge_score = (min(1.0, left_gradient/50) + min(1.0, right_gradient/50) + min(1.0, top_gradient/50)) / 3

            return boundary_edge_score

        except Exception as e:
            self.logger.error(f"Error computing legacy boundary score: {str(e)}")
            return 0.0

    def _validate_image(self, image_rgb: np.ndarray) -> bool:
        """Validate input image format and dimensions."""
        try:
            if not isinstance(image_rgb, np.ndarray):
                self.logger.error("Input is not a numpy array")
                return False

            if len(image_rgb.shape) != 3 or image_rgb.shape[2] != 3:
                self.logger.error(f"Invalid image shape: {image_rgb.shape}. Expected (H, W, 3)")
                return False

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

            return True

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

    def _calculate_scale_factor(self, height: int, width: int) -> int:
        """Calculate appropriate scale factor for image processing efficiency."""
        try:
            base_scale = 4
            scale_factor = base_scale + min(8, max(0, int((height * width) / (1000 * 1000)) if height * width > 0 else 0))
            return max(1, scale_factor)
        except Exception as e:
            self.logger.error(f"Error calculating scale factor: {str(e)}")
            return 4

    def _get_default_features(self) -> Dict[str, Any]:
        """Return default feature values in case of processing errors."""
        return {
            "avg_brightness": 100.0,
            "brightness_std": 50.0,
            "dark_pixel_ratio": 0.0,
            "bright_pixel_ratio": 0.0,
            "brightness_uniformity": 0.5,
            "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,
            "sky_brightness": 100.0,
            "warm_ratio": 0.0,
            "cool_ratio": 0.0,
            "color_atmosphere": "neutral",
            "gradient_ratio_vertical_horizontal": 1.0,
            "top_region_texture_complexity": 0.5,
            "shadow_clarity_score": 0.5,
            "vertical_strength": 0.0,
            "horizontal_strength": 0.0,
            "edges_density": 0.0,
            "ceiling_likelihood": 0.0,
            "boundary_clarity": 0.0,
            "openness_top_edge": 0.5,
            "ceiling_uniformity": 0.5,
            "horizontal_line_ratio": 0.0,
            "top_region_std": 0.0,
            "boundary_edge_score": 0.0,
            "bright_spot_count": 0,
            "circular_light_count": 0,
            "indoor_light_score": 0.0,
            "light_distribution_uniformity": 0.5,
            "street_line_score": 0.0,
            "sky_blue_ratio": 0.0,
            "gradient_ratio": 1.0
        }

    def _get_default_color_features(self) -> Dict[str, Any]:
        """Return default color feature values."""
        return {
            "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,
            "sky_brightness": 100.0,
            "warm_ratio": 0.0,
            "cool_ratio": 0.0,
            "color_atmosphere": "neutral"
        }

    def _get_default_sky_features(self) -> Dict[str, float]:
        """Return default sky region feature values."""
        return {
            "sky_region_brightness_ratio": 1.0,
            "sky_region_saturation": 0.0,
            "sky_region_blue_dominance": 0.0,
            "sky_brightness": 100.0
        }