VisionScout / lighting_analyzer.py
DawnC's picture
Upload 31 files
4d1f920 verified
raw
history blame
82.5 kB
import numpy as np
import cv2
from typing import Dict, Any, Optional
class LightingAnalyzer:
"""
Analyzes lighting conditions of an image, providing enhanced indoor/outdoor
determination and light type classification, with a focus on lighting analysis.
"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""
Initializes the LightingAnalyzer.
Args:
config: Optional configuration dictionary for custom analysis parameters.
"""
self.config = config or self._get_default_config()
def analyze(self, image, places365_info: Optional[Dict] = None):
"""
Analyzes the lighting conditions of an image.
Main entry point for analysis, computes basic features, determines
indoor/outdoor, and identifies lighting conditions.
Args:
image: Input image (numpy array or PIL Image).
Returns:
Dict: Dictionary containing lighting analysis results.
"""
try:
# Convert image format
if not isinstance(image, np.ndarray):
image_np = np.array(image) # Convert PIL Image to numpy array
else:
image_np = image.copy()
# Ensure image is in BGR for OpenCV if it's from PIL (RGB)
if image_np.shape[2] == 3 and not isinstance(image, np.ndarray): # PIL images are typically RGB
image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
elif image_np.shape[2] == 3 and image.shape[2] == 3: # Already a numpy array, assume BGR from cv2.imread
image_bgr = image_np
elif image_np.shape[2] == 4: # RGBA
image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGBA2BGR)
else: # Grayscale or other
# If grayscale, convert to BGR for consistency, though feature extraction will mostly use grayscale/HSV
if len(image_np.shape) == 2:
image_bgr = cv2.cvtColor(image_np, cv2.COLOR_GRAY2BGR)
else: # Fallback for other unexpected formats
print(f"Warning: Unexpected image format with shape {image_np.shape}. Attempting to proceed.")
image_bgr = image_np
# Ensure RGB format for internal processing (some functions expect RGB)
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
features = self._compute_basic_features(image_rgb) # features 字典現在也包含由 P365 間接影響的預計算值
# 將 places365_info 傳遞給室內/室外判斷
indoor_result = self._analyze_indoor_outdoor(features, places365_info=places365_info)
is_indoor = indoor_result["is_indoor"]
indoor_probability = indoor_result["indoor_probability"]
# 將 places365_info 和已修正的 is_indoor 傳遞給光線類型判斷
lighting_conditions = self._determine_lighting_conditions(features, is_indoor, places365_info=places365_info)
# Consolidate results
result = {
"time_of_day": lighting_conditions["time_of_day"],
"confidence": float(lighting_conditions["confidence"]),
"is_indoor": is_indoor,
"indoor_probability": float(indoor_probability),
"brightness": {
"average": float(features["avg_brightness"]),
"std_dev": float(features["brightness_std"]),
"dark_ratio": float(features["dark_pixel_ratio"]),
"bright_ratio": float(features.get("bright_pixel_ratio", 0)) # Added
},
"color_info": {
"blue_ratio": float(features["blue_ratio"]),
"sky_like_blue_ratio": float(features.get("sky_like_blue_ratio",0)), # More specific sky blue
"yellow_orange_ratio": float(features["yellow_orange_ratio"]),
"gray_ratio": float(features["gray_ratio"]),
"avg_saturation": float(features["avg_saturation"]),
"sky_region_brightness_ratio": float(features.get("sky_region_brightness_ratio", 1.0)), # Renamed and clarified
"sky_region_saturation": float(features.get("sky_region_saturation", 0)),
"sky_region_blue_dominance": float(features.get("sky_region_blue_dominance", 0)),
"color_atmosphere": features["color_atmosphere"],
"warm_ratio": float(features["warm_ratio"]),
"cool_ratio": float(features["cool_ratio"]),
},
"texture_info": { # New category for texture/gradient features
"gradient_ratio_vertical_horizontal": float(features.get("gradient_ratio_vertical_horizontal", 0)), # Renamed
"top_region_texture_complexity": float(features.get("top_region_texture_complexity", 0)),
"shadow_clarity_score": float(features.get("shadow_clarity_score",0.5)), # Default to neutral
},
"structure_info": { # New category for structural features
"ceiling_likelihood": float(features.get("ceiling_likelihood",0)),
"boundary_clarity": float(features.get("boundary_clarity",0)),
"openness_top_edge": float(features.get("openness_top_edge", 0.5)), # Default to neutral
}
}
# Add diagnostic information
if self.config.get("include_diagnostics", False): # Use .get for safety
result["diagnostics"] = {
"feature_contributions": indoor_result.get("feature_contributions", {}),
"lighting_diagnostics": lighting_conditions.get("diagnostics", {})
}
if self.config.get("include_diagnostics", False):
# indoor_result["diagnostics"] 現在會包含 P365 的影響
result["diagnostics"]["feature_contributions"] = indoor_result.get("feature_contributions", {})
result["diagnostics"]["lighting_diagnostics"] = lighting_conditions.get("diagnostics", {})
result["diagnostics"]["indoor_outdoor_diagnostics"] = indoor_result.get("diagnostics", {})
return result
except Exception as e:
print(f"Error in lighting analysis: {str(e)}")
import traceback
traceback.print_exc()
return {
"time_of_day": "unknown",
"confidence": 0,
"error": str(e)
}
def _compute_basic_features(self, image_rgb: np.ndarray) -> Dict[str, Any]:
"""
Computes basic lighting features from an RGB image.
This version includes enhancements for sky, ceiling, and boundary detection.
"""
# Get image dimensions
height, width = image_rgb.shape[:2]
if height == 0 or width == 0:
print("Error: Image has zero height or width.")
# Return a dictionary of zeros or default values for all expected features
return {feature: 0.0 for feature in [ # Ensure all keys expected by other methods are present
"avg_brightness", "brightness_std", "dark_pixel_ratio", "bright_pixel_ratio",
"blue_ratio", "sky_like_blue_ratio", "yellow_orange_ratio", "gray_ratio",
"avg_saturation", "sky_region_brightness_ratio", "sky_region_saturation", "sky_region_blue_dominance",
"color_atmosphere", "warm_ratio", "cool_ratio", "gradient_ratio_vertical_horizontal",
"top_region_texture_complexity", "shadow_clarity_score", "ceiling_likelihood",
"boundary_clarity", "openness_top_edge", "ceiling_uniformity", "horizontal_line_ratio", # Old keys kept for compatibility if still used
"indoor_light_score", "circular_light_count", "light_distribution_uniformity",
"boundary_edge_score", "top_region_std", "edges_density", "street_line_score",
"sky_brightness", "vertical_strength", "horizontal_strength", "brightness_uniformity", "bright_spot_count"
]}
# Adaptive scaling factor based on image size for performance
base_scale = 4
# Protect against zero division if height or width is tiny
scale_factor = base_scale + min(8, max(0, int((height * width) / (1000 * 1000)) if height * width > 0 else 0))
scale_factor = max(1, scale_factor) # Ensure scale_factor is at least 1
# Create a smaller version of the image for faster processing of some features
small_rgb = cv2.resize(image_rgb, (width // scale_factor, height // scale_factor), interpolation=cv2.INTER_AREA)
# Convert to HSV and Grayscale once
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) # Grayscale of the small image
# Separate HSV channels
h_channel, s_channel, v_channel = cv2.split(hsv_img)
# --- Brightness Features ---
avg_brightness = np.mean(v_channel)
brightness_std = np.std(v_channel)
dark_pixel_ratio = np.sum(v_channel < self.config.get("dark_pixel_threshold", 50)) / (height * width) # 使用配置閾值
bright_pixel_ratio = np.sum(v_channel > self.config.get("bright_pixel_threshold", 220)) / (height * width) # 新增:亮部像素比例
# --- Color Features ---
# Yellow-Orange Ratio
yellow_orange_mask = ((h_channel >= 15) & (h_channel <= 45)) # Adjusted range slightly
yellow_orange_ratio = np.sum(yellow_orange_mask) / (height * width)
# General Blue Ratio
blue_mask = ((h_channel >= 90) & (h_channel <= 140)) # Slightly wider blue range
blue_ratio = np.sum(blue_mask) / (height * width)
# More specific "Sky-Like Blue" Ratio - for clearer skies
# 中文備註:更精確地定義「天空藍」,排除室內常見的深藍或青色。
sky_like_blue_hue_min = self.config.get("sky_blue_hue_min", 100)
sky_like_blue_hue_max = self.config.get("sky_blue_hue_max", 130) # Typical sky blue Hues in HSV
sky_like_blue_sat_min = self.config.get("sky_blue_sat_min", 60) # Sky is usually somewhat saturated
sky_like_blue_val_min = self.config.get("sky_blue_val_min", 120) # Sky is usually bright
sky_like_blue_mask = ((h_channel >= sky_like_blue_hue_min) & (h_channel <= sky_like_blue_hue_max) &
(s_channel > sky_like_blue_sat_min) & (v_channel > sky_like_blue_val_min))
sky_like_blue_ratio = np.sum(sky_like_blue_mask) / (height * width)
# Gray Ratio (low saturation, mid-high brightness)
gray_sat_max = self.config.get("gray_sat_max", 50)
gray_val_min = self.config.get("gray_val_min", 80) # Adjusted to avoid very dark grays
gray_val_max = self.config.get("gray_val_max", 200) # Avoid pure white being too gray
gray_mask = (s_channel < gray_sat_max) & (v_channel > gray_val_min) & (v_channel < gray_val_max)
gray_ratio = np.sum(gray_mask) / (height * width)
avg_saturation = np.mean(s_channel)
# --- Sky Region Analysis (Top 1/3 of image) ---
# 中文備註:專門分析圖像頂部區域,這是判斷天空的關鍵。
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, :]
sky_region_avg_brightness = np.mean(sky_region_v) if sky_region_v.size > 0 else 0
sky_region_brightness_ratio = sky_region_avg_brightness / max(avg_brightness, 1e-5) # Ratio to overall brightness
sky_region_saturation = np.mean(sky_region_s) if sky_region_s.size > 0 else 0
# Blue dominance in sky region
sky_region_blue_pixels = np.sum(
(sky_region_h >= sky_like_blue_hue_min) & (sky_region_h <= sky_like_blue_hue_max) &
(sky_region_s > sky_like_blue_sat_min) & (sky_region_v > sky_like_blue_val_min)
)
sky_region_blue_dominance = sky_region_blue_pixels / max(1, sky_region_v.size)
# --- Color Atmosphere ---
warm_hue_ranges = self.config.get("warm_hue_ranges", [(0, 50), (330, 360)]) # Red, Orange, Yellow, some Magentas
cool_hue_ranges = self.config.get("cool_hue_ranges", [(90, 270)]) # Cyan, Blue, Purple, Green
warm_mask = np.zeros_like(h_channel, dtype=bool)
for h_min, h_max in warm_hue_ranges:
warm_mask |= ((h_channel >= h_min) & (h_channel <= h_max))
warm_ratio = np.sum(warm_mask & (s_channel > 30)) / (height * width) # Consider saturation for warmth
cool_mask = np.zeros_like(h_channel, dtype=bool)
for h_min, h_max in cool_hue_ranges:
cool_mask |= ((h_channel >= h_min) & (h_channel <= h_max))
cool_ratio = np.sum(cool_mask & (s_channel > 30)) / (height * width) # Consider saturation for coolness
if warm_ratio > cool_ratio and warm_ratio > 0.3: # Increased threshold
color_atmosphere = "warm"
elif cool_ratio > warm_ratio and cool_ratio > 0.3: # Increased threshold
color_atmosphere = "cool"
else:
color_atmosphere = "neutral"
# --- Gradient and Texture Features (on small image for speed) ---
# 中文備註:在縮小的灰階圖像上計算梯度,以提高效率。
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 = np.mean(np.abs(gx))
avg_abs_gy = np.mean(np.abs(gy))
# Renamed for clarity: ratio of vertical to horizontal gradients
gradient_ratio_vertical_horizontal = avg_abs_gy / max(avg_abs_gx, 1e-5)
# Texture complexity of the top region (potential ceiling or sky)
# 中文備註:分析頂部區域的紋理複雜度,天空通常紋理簡單,天花板可能複雜。
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()
# Normalize, though this might need scene-adaptive normalization or defined bins
top_region_texture_complexity = min(1.0, laplacian_var_sky / 1000.0) # Example normalization
else:
top_region_texture_complexity = 0.5 # Neutral if no top region
# 先簡單的估計陰影清晰度。清晰陰影通常表示強烈又單一的光源(像是太陽)。
# High brightness std dev might indicate strong highlights and shadows.
# Low dark_pixel_ratio with high brightness_std could imply sharp shadows.
if brightness_std > 60 and dark_pixel_ratio < 0.15 and avg_brightness > 100:
shadow_clarity_score = 0.7 # Potential for clear shadows (more outdoor-like)
elif brightness_std < 30 and dark_pixel_ratio > 0.1:
shadow_clarity_score = 0.3 # Potential for diffuse shadows (more indoor/cloudy-like)
else:
shadow_clarity_score = 0.5 # Neutral
# Structural Features (Ceiling, Boundary, Openness)
# 判斷天花板的可能性。
ceiling_likelihood = 0.0
# 條件1: 頂部區域紋理簡單且亮度適中 (表明可能是平坦的天花板)
if top_region_texture_complexity < self.config.get("ceiling_texture_thresh", 0.4) and \
self.config.get("ceiling_brightness_min", 60) < sky_region_avg_brightness < self.config.get("ceiling_brightness_max", 230): # 放寬亮度上限
ceiling_likelihood += 0.45 # 稍微提高基礎分
# 條件2: 頂部區域存在水平線條 (可能是天花板邊緣或結構)
top_horizontal_lines_strength = np.mean(np.abs(gx[:small_gray.shape[0]//3, :]))
if top_horizontal_lines_strength > avg_abs_gx * self.config.get("ceiling_horizontal_line_factor", 1.15): # 稍微降低因子
ceiling_likelihood += 0.35 # 稍微提高貢獻
# 條件3: 中央區域比周圍亮 (可能是吊燈,暗示天花板) - 針對室內光源
# 這個條件對於 room_02.jpg 可能比較重要,因為它有一個中央吊燈
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_bright_spot_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_bright_spot_region.size > 0 and np.mean(center_bright_spot_region) > avg_brightness * self.config.get("ceiling_center_bright_factor", 1.25): # 提高中心亮度要求
ceiling_likelihood += 0.30 # 顯著提高吊燈對天花板的貢獻
# 條件4: 如果頂部區域藍色成分不高,且不是特別亮(排除天空),則增加天花板可能性
# 這個條件有助於區分多雲天空和室內天花板
if sky_region_blue_dominance < self.config.get("ceiling_max_sky_blue_thresh", 0.08) and \
sky_region_brightness_ratio < self.config.get("ceiling_max_sky_brightness_ratio", 1.15): # 頂部不能太亮
ceiling_likelihood += 0.15
# 懲罰項: 如果有強烈天空信號,大幅降低天花板可能性
if sky_region_blue_dominance > self.config.get("sky_blue_dominance_strong_thresh", 0.25) and \
sky_region_brightness_ratio > self.config.get("sky_brightness_strong_thresh", 1.25):
ceiling_likelihood *= self.config.get("ceiling_sky_override_factor", 0.1) # 大幅降低
ceiling_likelihood = min(1.0, ceiling_likelihood)
# 邊界感的,通常室內邊界較強
# Using Sobel on edges of the small_gray image
edge_width_sm = max(1, small_gray.shape[1] // 10) # 10% for edge
edge_height_sm = max(1, small_gray.shape[0] // 10)
left_edge_grad_x = np.mean(np.abs(gx[:, :edge_width_sm])) if small_gray.shape[1] > edge_width_sm else 0
right_edge_grad_x = np.mean(np.abs(gx[:, -edge_width_sm:])) if small_gray.shape[1] > edge_width_sm else 0
top_edge_grad_y = np.mean(np.abs(gy[:edge_height_sm, :])) if small_gray.shape[0] > edge_height_sm else 0
# Normalize these gradients (e.g. against average gradient)
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) # Normalize, 1.5 is a heuristic factor
# 判斷頂部邊緣是否開放(例如天空),室外的特徵比較明顯
# Low vertical gradient at the very top edge suggests openness (sky)
top_edge_strip_gy = np.mean(np.abs(gy[:max(1,small_gray.shape[0]//20), :])) # Very top 5%
openness_top_edge = 1.0 - min(1.0, top_edge_strip_gy / max(avg_abs_gy, 1e-5) / 0.5 ) # Normalize, 0.5 factor, less grad = more open
top_region = v_channel[:height//4, :] # Full res top region
top_region_std_fullres = np.std(top_region) if top_region.size > 0 else 0
ceiling_uniformity_old = 1.0 - min(1, top_region_std_fullres / max(np.mean(top_region) if top_region.size >0 else 1e-5, 1e-5))
top_gradients_old = np.abs(cv2.Sobel(gray_img[:height//4, :], cv2.CV_32F, 0, 1, ksize=3)) # Full res top gradients for gy
horizontal_lines_strength_old = np.mean(top_gradients_old) if top_gradients_old.size > 0 else 0
horizontal_line_ratio_old = min(1, horizontal_lines_strength_old / 40) # Original normalization
# Light source detection (simplified, as in original)
sampled_v = v_channel[::scale_factor*2, ::scale_factor*2] # Already calculated
light_threshold = min(self.config.get("light_source_abs_thresh", 220), avg_brightness + 2*brightness_std)
is_bright_spots = sampled_v > light_threshold
bright_spot_count_old = np.sum(is_bright_spots)
circular_light_score_old = 0
indoor_light_score_old = 0.0 # Default to float
light_distribution_uniformity_old = 0.5
if 1 < bright_spot_count_old < 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): # Concentrated
circular_light_score_old = min(3, len(bright_y) // 2)
light_distribution_uniformity_old = 0.7
if np.mean(bright_y) < sampled_v.shape[0] / 2: # Lights in upper half
indoor_light_score_old = 0.6
else:
indoor_light_score_old = 0.3
# Boundary edge score
# Using small_gray for consistency with other gradient features
left_edge_sm = small_gray[:, :small_gray.shape[1]//6]
right_edge_sm = small_gray[:, 5*small_gray.shape[1]//6:]
top_edge_sm = small_gray[:small_gray.shape[0]//6, :]
left_gradient_old = 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_old = 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_old = np.mean(np.abs(cv2.Sobel(top_edge_sm, cv2.CV_32F, 0, 1, ksize=3))) if top_edge_sm.size >0 else 0
boundary_edge_score_old = (min(1, left_gradient_old/50) + min(1, right_gradient_old/50) + min(1, top_gradient_old/50)) / 3
edges_density_old = min(1, (avg_abs_gx + avg_abs_gy) / 100) # Using already computed avg_abs_gx, avg_abs_gy
# Street line score (original)
street_line_score_old = 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_old = 0.7
features = {
# Brightness
"avg_brightness": avg_brightness,
"brightness_std": brightness_std,
"dark_pixel_ratio": dark_pixel_ratio,
"bright_pixel_ratio": bright_pixel_ratio,
# Color
"blue_ratio": blue_ratio,
"sky_like_blue_ratio": sky_like_blue_ratio,
"yellow_orange_ratio": yellow_orange_ratio,
"gray_ratio": gray_ratio,
"avg_saturation": avg_saturation,
"color_atmosphere": color_atmosphere,
"warm_ratio": warm_ratio,
"cool_ratio": cool_ratio,
# Sky Region Specific
"sky_region_brightness_ratio": sky_region_brightness_ratio,
"sky_region_saturation": sky_region_saturation,
"sky_region_blue_dominance": sky_region_blue_dominance,
# Texture / Gradient
"gradient_ratio_vertical_horizontal": gradient_ratio_vertical_horizontal,
"top_region_texture_complexity": top_region_texture_complexity,
"shadow_clarity_score": shadow_clarity_score,
# Structure
"ceiling_likelihood": ceiling_likelihood,
"boundary_clarity": boundary_clarity,
"openness_top_edge": openness_top_edge,
# color distribution
"sky_blue_ratio": sky_like_blue_ratio,
"sky_brightness": sky_region_avg_brightness,
"gradient_ratio": gradient_ratio_vertical_horizontal,
"brightness_uniformity": 1 - min(1, brightness_std / max(avg_brightness, 1e-5)),
"vertical_strength": avg_abs_gy,
"horizontal_strength": avg_abs_gx,
"ceiling_uniformity": ceiling_uniformity_old,
"horizontal_line_ratio": horizontal_line_ratio_old,
"bright_spot_count": bright_spot_count_old,
"indoor_light_score": indoor_light_score_old,
"circular_light_count": circular_light_score_old,
"light_distribution_uniformity": light_distribution_uniformity_old,
"boundary_edge_score": boundary_edge_score_old,
"top_region_std": top_region_std_fullres,
"edges_density": edges_density_old,
"street_line_score": street_line_score_old,
}
return features
def _analyze_indoor_outdoor(self, features: Dict[str, Any], places365_info: Optional[Dict] = None) -> Dict[str, Any]:
"""
Analyzes features and Places365 info to determine if the scene is indoor or outdoor.
Places365 info is used to strongly influence the decision if its confidence is high.
"""
# Use a copy of weights if they might be modified, otherwise direct access is fine
weights = self.config.get("indoor_outdoor_weights", {})
visual_indoor_score = 0.0 # Score based purely on visual features
feature_contributions = {}
diagnostics = {}
# Internal Thresholds and Definitions for this function
P365_HIGH_CONF_THRESHOLD = 0.65 # Confidence threshold for P365 to strongly influence/override
P365_MODERATE_CONF_THRESHOLD = 0.4 # Confidence threshold for P365 to moderately influence
# Simplified internal lists for definitely indoor/outdoor based on P365 mapped_scene_type
DEFINITELY_OUTDOOR_KEYWORDS_P365 = [
"street", "road", "highway", "park", "beach", "mountain", "forest", "field",
"outdoor", "sky", "coast", "courtyard", "square", "plaza", "bridge",
"parking_lot", "playground", "stadium", "construction_site", "river", "ocean", "desert", "garden", "trail"
]
DEFINITELY_INDOOR_KEYWORDS_P365 = [
"bedroom", "office", "kitchen", "library", "classroom", "conference_room", "living_room",
"bathroom", "hospital", "hotel_room", "cabin", "interior", "museum", "gallery",
"mall", "market_indoor", "basement", "corridor", "lobby", "restaurant_indoor", "bar_indoor", "shop_indoor", "gym_indoor"
]
# Extract key info from places365_info
p365_mapped_scene = "unknown"
p365_is_indoor_from_classification = None
p365_attributes = []
p365_confidence = 0.0
if places365_info:
p365_mapped_scene = places365_info.get('mapped_scene_type', 'unknown').lower()
p365_attributes = [attr.lower() for attr in places365_info.get('attributes', [])]
p365_confidence = places365_info.get('confidence', 0.0)
p365_is_indoor_from_classification = places365_info.get('is_indoor_from_classification', None)
diagnostics["p365_context_received"] = (
f"P365 Scene: {p365_mapped_scene}, P365 SceneConf: {p365_confidence:.2f}, "
f"P365 DirectIndoor: {p365_is_indoor_from_classification}, P365 Attrs: {p365_attributes}"
)
# Step 1: Calculate visual_indoor_score based on its own features
sky_evidence_score_visual = 0.0
strong_sky_signal_visual = False
sky_blue_dominance_val = features.get("sky_region_blue_dominance", 0.0)
sky_region_brightness_ratio_val = features.get("sky_region_brightness_ratio", 1.0)
top_texture_complexity_val = features.get("top_region_texture_complexity", 0.5)
openness_top_edge_val = features.get("openness_top_edge", 0.5)
# Condition 1: Visual Strong blue sky signal
if sky_blue_dominance_val > self.config.get("sky_blue_dominance_strong_thresh", 0.35):
sky_evidence_score_visual -= weights.get("sky_blue_dominance_w", 3.5) * sky_blue_dominance_val
diagnostics["sky_detection_reason_visual"] = f"Visual: Strong sky-like blue ({sky_blue_dominance_val:.2f})"
strong_sky_signal_visual = True
elif sky_region_brightness_ratio_val > self.config.get("sky_brightness_ratio_strong_thresh", 1.35) and \
top_texture_complexity_val < self.config.get("sky_texture_complexity_clear_thresh", 0.25):
outdoor_push = weights.get("sky_brightness_ratio_w", 3.0) * (sky_region_brightness_ratio_val - 1.0)
sky_evidence_score_visual -= outdoor_push
sky_evidence_score_visual -= weights.get("sky_texture_w", 2.0)
diagnostics["sky_detection_reason_visual"] = f"Visual: Top brighter (ratio:{sky_region_brightness_ratio_val:.2f}) & low texture."
strong_sky_signal_visual = True
elif openness_top_edge_val > self.config.get("openness_top_strong_thresh", 0.80):
sky_evidence_score_visual -= weights.get("openness_top_w", 2.8) * openness_top_edge_val
diagnostics["sky_detection_reason_visual"] = f"Visual: Very high top edge openness ({openness_top_edge_val:.2f})."
strong_sky_signal_visual = True
elif not strong_sky_signal_visual and \
top_texture_complexity_val < self.config.get("sky_texture_complexity_cloudy_thresh", 0.20) and \
sky_region_brightness_ratio_val > self.config.get("sky_brightness_ratio_cloudy_thresh", 0.95):
sky_evidence_score_visual -= weights.get("sky_texture_w", 2.0) * (1.0 - top_texture_complexity_val) * 0.5
diagnostics["sky_detection_reason_visual"] = f"Visual: Weak sky signal (low texture, brightish top: {top_texture_complexity_val:.2f}), less weight."
if abs(sky_evidence_score_visual) > 0.01:
visual_indoor_score += sky_evidence_score_visual
feature_contributions["sky_openness_features_visual"] = round(sky_evidence_score_visual, 2)
if strong_sky_signal_visual:
diagnostics["strong_sky_signal_visual_detected"] = True
# Indoor Indicators (Visual): Ceiling, Enclosure
enclosure_evidence_score_visual = 0.0
ceiling_likelihood_val = features.get("ceiling_likelihood", 0.0)
boundary_clarity_val = features.get("boundary_clarity", 0.0)
# Get base weights for modification
effective_ceiling_weight = weights.get("ceiling_likelihood_w", 1.5)
effective_boundary_weight = weights.get("boundary_clarity_w", 1.2)
if ceiling_likelihood_val > self.config.get("ceiling_likelihood_thresh", 0.38):
current_ceiling_score = effective_ceiling_weight * ceiling_likelihood_val
if strong_sky_signal_visual:
current_ceiling_score *= self.config.get("sky_override_factor_ceiling", 0.1)
enclosure_evidence_score_visual += current_ceiling_score
diagnostics["indoor_reason_ceiling_visual"] = f"Visual Ceiling: {ceiling_likelihood_val:.2f}, ScoreCont: {current_ceiling_score:.2f}"
if boundary_clarity_val > self.config.get("boundary_clarity_thresh", 0.38):
current_boundary_score = effective_boundary_weight * boundary_clarity_val
if strong_sky_signal_visual:
current_boundary_score *= self.config.get("sky_override_factor_boundary", 0.2)
enclosure_evidence_score_visual += current_boundary_score
diagnostics["indoor_reason_boundary_visual"] = f"Visual Boundary: {boundary_clarity_val:.2f}, ScoreCont: {current_boundary_score:.2f}"
if not strong_sky_signal_visual and top_texture_complexity_val > 0.7 and \
openness_top_edge_val < 0.3 and ceiling_likelihood_val < 0.35:
diagnostics["complex_urban_top_visual"] = True
if boundary_clarity_val > 0.5:
enclosure_evidence_score_visual *= 0.5
diagnostics["reduced_enclosure_for_urban_top_visual"] = True
if abs(enclosure_evidence_score_visual) > 0.01:
visual_indoor_score += enclosure_evidence_score_visual
feature_contributions["enclosure_features"] = round(enclosure_evidence_score_visual, 2)
# Brightness Uniformity (Visual)
brightness_uniformity_val = 1.0 - min(1.0, features.get("brightness_std", 50.0) / max(features.get("avg_brightness", 100.0), 1e-5))
uniformity_contribution_visual = 0.0
if brightness_uniformity_val > self.config.get("brightness_uniformity_thresh_indoor", 0.6):
uniformity_contribution_visual = weights.get("brightness_uniformity_w", 0.6) * brightness_uniformity_val
if strong_sky_signal_visual: uniformity_contribution_visual *= self.config.get("sky_override_factor_uniformity", 0.15)
elif brightness_uniformity_val < self.config.get("brightness_uniformity_thresh_outdoor", 0.40):
if features.get("shadow_clarity_score", 0.5) > 0.65:
uniformity_contribution_visual = -weights.get("brightness_non_uniformity_outdoor_w", 1.0) * (1.0 - brightness_uniformity_val)
elif not strong_sky_signal_visual:
uniformity_contribution_visual = weights.get("brightness_non_uniformity_indoor_penalty_w", 0.1) * (1.0 - brightness_uniformity_val)
if abs(uniformity_contribution_visual) > 0.01:
visual_indoor_score += uniformity_contribution_visual
feature_contributions["brightness_uniformity_contribution"] = round(uniformity_contribution_visual, 2)
# Light Sources (Visual)
indoor_light_score_val = features.get("indoor_light_score", 0.0)
circular_light_count_val = features.get("circular_light_count", 0)
bright_spot_count_val = features.get("bright_spot_count", 0)
avg_brightness_val = features.get("avg_brightness", 100.0)
light_source_contribution_visual = 0.0
if circular_light_count_val >= 1 and not strong_sky_signal_visual:
light_source_contribution_visual += weights.get("circular_lights_w", 1.2) * circular_light_count_val
elif indoor_light_score_val > 0.55 and not strong_sky_signal_visual:
light_source_contribution_visual += weights.get("indoor_light_score_w", 0.8) * indoor_light_score_val
elif bright_spot_count_val > self.config.get("many_bright_spots_thresh", 6) and \
avg_brightness_val < self.config.get("dim_scene_for_spots_thresh", 115) and \
not strong_sky_signal_visual:
light_source_contribution_visual += weights.get("many_bright_spots_indoor_w", 0.3) * min(bright_spot_count_val / 10.0, 1.5)
grad_ratio_val = features.get("gradient_ratio_vertical_horizontal", 1.0)
is_likely_street_structure_visual = (0.7 < grad_ratio_val < 1.5) and features.get("edges_density", 0.0) > 0.15
if is_likely_street_structure_visual and bright_spot_count_val > 3 and not strong_sky_signal_visual:
light_source_contribution_visual *= 0.2
diagnostics["street_lights_heuristic_visual"] = True
elif strong_sky_signal_visual:
light_source_contribution_visual *= self.config.get("sky_override_factor_lights", 0.05)
if abs(light_source_contribution_visual) > 0.01:
visual_indoor_score += light_source_contribution_visual
feature_contributions["light_source_features"] = round(light_source_contribution_visual, 2)
# Color Atmosphere (Visual)
color_atmosphere_contribution_visual = 0.0
if features.get("color_atmosphere") == "warm" and \
avg_brightness_val < self.config.get("warm_indoor_max_brightness_thresh", 135):
if not strong_sky_signal_visual and \
not diagnostics.get("complex_urban_top_visual", False) and \
not (is_likely_street_structure_visual and avg_brightness_val > 80) and \
features.get("avg_saturation", 100.0) < 160:
if light_source_contribution_visual > 0.05:
color_atmosphere_contribution_visual = weights.get("warm_atmosphere_indoor_w", 0.15)
visual_indoor_score += color_atmosphere_contribution_visual
if abs(color_atmosphere_contribution_visual) > 0.01:
feature_contributions["warm_atmosphere_indoor_visual_contrib"] = round(color_atmosphere_contribution_visual, 2) # New key
# Home Environment Pattern (Visual)
home_env_score_contribution_visual = 0.0
if not strong_sky_signal_visual:
bedroom_indicators = 0
if features.get("brightness_uniformity",0.0) > 0.65 and features.get("boundary_clarity",0.0) > 0.40 : bedroom_indicators+=1.1
if features.get("ceiling_likelihood",0.0) > 0.35 and (bright_spot_count_val > 0 or circular_light_count_val > 0) : bedroom_indicators+=1.1
if features.get("warm_ratio", 0.0) > 0.55 and features.get("brightness_uniformity",0.0) > 0.65 : bedroom_indicators+=1.0
if features.get("brightness_uniformity",0.0) > 0.70 and features.get("avg_saturation",100.0) < 60 : bedroom_indicators+=0.7
if bedroom_indicators >= self.config.get("home_pattern_thresh_strong", 2.0) :
home_env_score_contribution_visual = weights.get("home_env_strong_w", 1.5)
elif bedroom_indicators >= self.config.get("home_pattern_thresh_moderate", 1.0):
home_env_score_contribution_visual = weights.get("home_env_moderate_w", 0.7)
if bedroom_indicators > 0:
diagnostics["home_environment_pattern_visual_indicators"] = round(bedroom_indicators,1)
else:
diagnostics["skipped_home_env_visual_due_to_sky"] = True
if abs(home_env_score_contribution_visual) > 0.01:
visual_indoor_score += home_env_score_contribution_visual
feature_contributions["home_environment_pattern_visual"] = round(home_env_score_contribution_visual, 2)
# Aerial View of Streets (Visual Heuristic)
if features.get("sky_region_brightness_ratio", 1.0) < self.config.get("aerial_top_dark_ratio_thresh", 0.9) and \
top_texture_complexity_val > self.config.get("aerial_top_complex_thresh", 0.60) and \
avg_brightness_val > self.config.get("aerial_min_avg_brightness_thresh", 65) and \
not strong_sky_signal_visual:
aerial_street_outdoor_push_visual = -weights.get("aerial_street_w", 2.5)
visual_indoor_score += aerial_street_outdoor_push_visual
feature_contributions["aerial_street_pattern_visual"] = round(aerial_street_outdoor_push_visual, 2)
diagnostics["aerial_street_pattern_visual_detected"] = True
if "enclosure_features" in feature_contributions and feature_contributions["enclosure_features"] > 0: # Check if positive
reduction_factor = self.config.get("aerial_enclosure_reduction_factor", 0.75)
# Only reduce the positive part of enclosure_evidence_score_visual
positive_enclosure_score = max(0, enclosure_evidence_score_visual)
reduction_amount = positive_enclosure_score * reduction_factor
visual_indoor_score -= reduction_amount
feature_contributions["enclosure_features_reduced_by_aerial"] = round(-reduction_amount, 2)
# Update the main enclosure_features contribution
feature_contributions["enclosure_features"] = round(enclosure_evidence_score_visual - reduction_amount, 2)
diagnostics["visual_indoor_score_subtotal"] = round(visual_indoor_score, 3)
# Step 2: Incorporate Places365 Influence
final_indoor_score = visual_indoor_score # Start with the visual score
p365_influence_score = 0.0 # Score component specifically from P365
# 處理所有Places365資訊
if places365_info:
# Define internal (non-config) weights for P365 influence to keep it self-contained
P365_DIRECT_INDOOR_WEIGHT = 3.5 # Strong influence for P365's direct classification
P365_DIRECT_OUTDOOR_WEIGHT = 4.0 # Slightly stronger for outdoor to counter visual enclosure bias
P365_SCENE_CONTEXT_INDOOR_WEIGHT = 2.0
P365_SCENE_CONTEXT_OUTDOOR_WEIGHT = 2.5
P365_ATTRIBUTE_INDOOR_WEIGHT = 1.0
P365_ATTRIBUTE_OUTDOOR_WEIGHT = 1.5
# 場景關鍵字定義,包含十字路口相關詞彙
DEFINITELY_OUTDOOR_KEYWORDS_P365 = [
"street", "road", "highway", "park", "beach", "mountain", "forest", "field",
"outdoor", "sky", "coast", "courtyard", "square", "plaza", "bridge",
"parking_lot", "playground", "stadium", "construction_site", "river", "ocean",
"desert", "garden", "trail", "intersection", "crosswalk", "sidewalk", "pathway",
"avenue", "boulevard", "downtown", "city_center", "market_outdoor"
]
DEFINITELY_INDOOR_KEYWORDS_P365 = [
"bedroom", "office", "kitchen", "library", "classroom", "conference_room", "living_room",
"bathroom", "hospital", "hotel_room", "cabin", "interior", "museum", "gallery",
"mall", "market_indoor", "basement", "corridor", "lobby", "restaurant_indoor",
"bar_indoor", "shop_indoor", "gym_indoor"
]
# A. Influence from P365's direct indoor/outdoor classification (is_indoor_from_classification)
if p365_is_indoor_from_classification is not None and \
p365_confidence >= P365_MODERATE_CONF_THRESHOLD:
current_p365_direct_contrib = 0.0
if p365_is_indoor_from_classification is True:
current_p365_direct_contrib = P365_DIRECT_INDOOR_WEIGHT * p365_confidence
diagnostics["p365_influence_source"] = f"P365_DirectIndoor(True,Conf:{p365_confidence:.2f},Scene:{p365_mapped_scene})"
else: # P365 says outdoor
current_p365_direct_contrib = -P365_DIRECT_OUTDOOR_WEIGHT * p365_confidence
diagnostics["p365_influence_source"] = f"P365_DirectIndoor(False,Conf:{p365_confidence:.2f},Scene:{p365_mapped_scene})"
# Modulate P365's indoor push if strong VISUAL sky signal exists from LA
if strong_sky_signal_visual and current_p365_direct_contrib > 0:
sky_override_factor = self.config.get("sky_override_factor_p365_indoor_decision", 0.3)
current_p365_direct_contrib *= sky_override_factor
diagnostics["p365_indoor_push_reduced_by_visual_sky"] = f"Reduced to {current_p365_direct_contrib:.2f}"
p365_influence_score += current_p365_direct_contrib
# B. Influence from P365's mapped scene type context (修改:適用於所有信心度情況)
elif p365_confidence >= P365_MODERATE_CONF_THRESHOLD:
current_p365_context_contrib = 0.0
is_def_indoor = any(kw in p365_mapped_scene for kw in DEFINITELY_INDOOR_KEYWORDS_P365)
is_def_outdoor = any(kw in p365_mapped_scene for kw in DEFINITELY_OUTDOOR_KEYWORDS_P365)
if is_def_indoor and not is_def_outdoor: # Clearly an indoor scene type from P365
current_p365_context_contrib = P365_SCENE_CONTEXT_INDOOR_WEIGHT * p365_confidence
diagnostics["p365_influence_source"] = f"P365_SceneContext(Indoor: {p365_mapped_scene}, Conf:{p365_confidence:.2f})"
elif is_def_outdoor and not is_def_indoor: # Clearly an outdoor scene type from P365
current_p365_context_contrib = -P365_SCENE_CONTEXT_OUTDOOR_WEIGHT * p365_confidence
diagnostics["p365_influence_source"] = f"P365_SceneContext(Outdoor: {p365_mapped_scene}, Conf:{p365_confidence:.2f})"
if strong_sky_signal_visual and current_p365_context_contrib > 0:
sky_override_factor = self.config.get("sky_override_factor_p365_indoor_decision", 0.3)
current_p365_context_contrib *= sky_override_factor
diagnostics["p365_context_indoor_push_reduced_by_visual_sky"] = f"Reduced to {current_p365_context_contrib:.2f}"
p365_influence_score += current_p365_context_contrib
# C. Influence from P365 attributes
if p365_attributes and p365_confidence > self.config.get("places365_attribute_confidence_thresh", 0.5):
attr_contrib = 0.0
if "indoor" in p365_attributes and "outdoor" not in p365_attributes: # Prioritize "indoor" if both somehow appear
attr_contrib += P365_ATTRIBUTE_INDOOR_WEIGHT * (p365_confidence * 0.5) # Attributes usually less direct
diagnostics["p365_attr_influence"] = f"+{attr_contrib:.2f} (indoor attr)"
elif "outdoor" in p365_attributes and "indoor" not in p365_attributes:
attr_contrib -= P365_ATTRIBUTE_OUTDOOR_WEIGHT * (p365_confidence * 0.5)
diagnostics["p365_attr_influence"] = f"{attr_contrib:.2f} (outdoor attr)"
if strong_sky_signal_visual and attr_contrib > 0:
attr_contrib *= self.config.get("sky_override_factor_p365_indoor_decision", 0.3) # Reduce if LA sees sky
p365_influence_score += attr_contrib
# 針對高信心度戶外場景的額外處理
if p365_confidence >= 0.85 and any(kw in p365_mapped_scene for kw in ["intersection", "crosswalk", "street", "road"]):
# 當Places365強烈指示戶外街道場景時,額外增加戶外影響分數
additional_outdoor_push = -3.0 * p365_confidence
p365_influence_score += additional_outdoor_push
diagnostics["p365_street_scene_boost"] = f"Additional outdoor push: {additional_outdoor_push:.2f} for street scene: {p365_mapped_scene}"
print(f"DEBUG: High confidence street scene detected - {p365_mapped_scene} with confidence {p365_confidence:.3f}")
if abs(p365_influence_score) > 0.01:
feature_contributions["places365_influence_score"] = round(p365_influence_score, 2)
final_indoor_score = visual_indoor_score + p365_influence_score
diagnostics["final_indoor_score_value"] = round(final_indoor_score, 3)
diagnostics["final_score_breakdown"] = f"VisualScore: {visual_indoor_score:.2f}, P365Influence: {p365_influence_score:.2f}"
# Step 3: Final probability and decision
sigmoid_scale = self.config.get("indoor_score_sigmoid_scale", 0.30)
indoor_probability = 1 / (1 + np.exp(-final_indoor_score * sigmoid_scale))
decision_threshold = self.config.get("indoor_decision_threshold", 0.5)
is_indoor = indoor_probability > decision_threshold
# Places365 高信心度強制覆蓋(在 sigmoid 計算之後才執行)
print(f"DEBUG_OVERRIDE: Pre-override -> is_indoor: {is_indoor} (type: {type(is_indoor)}), p365_conf: {p365_confidence}, p365_raw_is_indoor: {places365_info.get('is_indoor', 'N/A') if places365_info else 'N/A'}")
# Places365 Model 信心大於0.5時候直接覆蓋結果
if places365_info and p365_confidence >= 0.5:
p365_is_indoor_decision = places365_info.get('is_indoor', None) # 這應該是 Python bool (True, False) 或 None
print(f"DEBUG_OVERRIDE: Override condition p365_conf >= 0.8 MET. p365_is_indoor_decision: {p365_is_indoor_decision} (type: {type(p365_is_indoor_decision)})")
# 使用 '==' 進行比較以增加對 NumPy bool 型別的兼容性
# 並且明確檢查 p365_is_indoor_decision 不是 None
if p365_is_indoor_decision == False and p365_is_indoor_decision is not None:
print(f"DEBUG_OVERRIDE: Path for p365_is_indoor_decision == False taken. Original is_indoor: {is_indoor}")
original_decision_str = f"Indoor:{is_indoor}, Prob:{indoor_probability:.3f}, Score:{final_indoor_score:.2f}"
is_indoor = False
indoor_probability = 0.02 # 強制設定為極低的室內機率,基本上就是變成室外
final_indoor_score = -8.0 # 強制設定為極低的室內分數
feature_contributions["places365_influence_score"] = final_indoor_score # 更新貢獻分數
diagnostics["p365_force_override_applied"] = f"P365 FORCED OUTDOOR (is_indoor: {p365_is_indoor_decision}, Conf: {p365_confidence:.3f})"
diagnostics["p365_override_original_decision"] = original_decision_str
print(f"INFO: Places365 FORCED OUTDOOR override applied. New is_indoor: {is_indoor}")
elif p365_is_indoor_decision == True and p365_is_indoor_decision is not None:
print(f"DEBUG_OVERRIDE: Path for p365_is_indoor_decision == True taken. Original is_indoor: {is_indoor}")
original_decision_str = f"Indoor:{is_indoor}, Prob:{indoor_probability:.3f}, Score:{final_indoor_score:.2f}"
is_indoor = True
indoor_probability = 0.98 # 強制設定為極高的室內機率,基本上就是變成室內
final_indoor_score = 8.0 # 強制設定為極高的室內分數
feature_contributions["places365_influence_score"] = final_indoor_score # 更新貢獻分數
diagnostics["p365_force_override_applied"] = f"P365 FORCED INDOOR (is_indoor: {p365_is_indoor_decision}, Conf: {p365_confidence:.3f})"
diagnostics["p365_override_original_decision"] = original_decision_str
print(f"INFO: Places365 FORCED INDOOR override applied. New is_indoor: {is_indoor}")
else:
print(f"DEBUG_OVERRIDE: No P365 True/False override. p365_is_indoor_decision was: {p365_is_indoor_decision}")
# 確保 diagnostics 反映的是覆蓋後的 is_indoor 值
diagnostics["final_indoor_probability_calculated"] = round(indoor_probability, 3) # 使用可能已被覆蓋的 indoor_probability
diagnostics["final_is_indoor_decision"] = bool(is_indoor) # 避免 np.True_
print(f"DEBUG_OVERRIDE: Returning from _analyze_indoor_outdoor -> is_indoor: {is_indoor} (type: {type(is_indoor)}), final_indoor_score: {final_indoor_score}, indoor_probability: {indoor_probability}")
for key in ["sky_openness_features", "enclosure_features", "brightness_uniformity_contribution", "light_source_features"]:
if key not in feature_contributions:
feature_contributions[key] = 0.0 # Default to 0 if not specifically calculated by visual or P365 parts
return {
"is_indoor": is_indoor,
"indoor_probability": indoor_probability,
"indoor_score_raw": final_indoor_score,
"feature_contributions": feature_contributions, # Contains visual contributions and P365 influence
"diagnostics": diagnostics
}
def _determine_lighting_conditions(self, features: Dict[str, Any], is_indoor: bool, places365_info: Optional[Dict] = None) -> Dict[str, Any]:
"""
Determines specific lighting conditions based on features, the (Places365-influenced) is_indoor status,
and Places365 scene context.
"""
time_of_day = "unknown"
confidence = 0.5 # Base confidence for visual feature analysis
diagnostics = {}
# Internal Thresholds and Definitions for this function
P365_ATTRIBUTE_CONF_THRESHOLD = 0.60 # Min P365 scene confidence to trust its attributes for lighting
P365_SCENE_MODERATE_CONF_THRESHOLD = 0.45 # Min P365 scene confidence for its type to influence lighting
P365_SCENE_HIGH_CONF_THRESHOLD = 0.70 # Min P365 scene confidence for strong influence
# Keywords for P365 mapped scene types (lowercase)
P365_OUTDOOR_SCENE_KEYWORDS = [
"street", "road", "highway", "park", "beach", "mountain", "forest", "field",
"outdoor", "sky", "coast", "courtyard", "square", "plaza", "bridge",
"parking", "playground", "stadium", "construction", "river", "ocean", "desert", "garden", "trail",
"natural_landmark", "airport_outdoor", "train_station_outdoor", "bus_station_outdoor", "intersection", "crosswalk", "sidewalk", "pathway"
]
P365_INDOOR_RESTAURANT_KEYWORDS = ["restaurant", "bar", "cafe", "dining_room", "pub", "bistro", "eatery"]
# Extract key info from places365_info - Initialize all variables first
p365_mapped_scene = "unknown"
p365_attributes = []
p365_confidence = 0.0
if places365_info:
p365_mapped_scene = places365_info.get('mapped_scene_type', 'unknown').lower()
p365_attributes = [attr.lower() for attr in places365_info.get('attributes', [])]
p365_confidence = places365_info.get('confidence', 0.0)
diagnostics["p365_context_for_lighting"] = (
f"P365 Scene: {p365_mapped_scene}, Attrs: {p365_attributes}, Conf: {p365_confidence:.2f}"
)
# Extract visual features (using .get with defaults for safety)
avg_brightness = features.get("avg_brightness", 128.0)
yellow_orange_ratio = features.get("yellow_orange_ratio", 0.0)
gray_ratio = features.get("gray_ratio", 0.0)
sky_like_blue_in_sky_region = features.get("sky_region_blue_dominance", 0.0)
sky_region_brightness_ratio = features.get("sky_region_brightness_ratio", 1.0)
sky_region_is_brighter = sky_region_brightness_ratio > 1.05
top_texture_complexity_val = features.get("top_region_texture_complexity", 0.5)
bright_spots_overall = features.get("bright_spot_count", 0)
circular_lights = features.get("circular_light_count", 0)
is_likely_home_environment = features.get("home_environment_pattern", 0.0) > self.config.get("home_pattern_thresh_moderate", 1.0) * 0.7
light_dist_uniformity = features.get("light_distribution_uniformity", 0.5)
# Config thresholds
config_thresholds = self.config
# Priority 1: Use Places365 Attributes if highly confident and consistent with `is_indoor`
determined_by_p365_attr = False
if p365_attributes and p365_confidence > P365_ATTRIBUTE_CONF_THRESHOLD:
if not is_indoor: # Apply outdoor attributes only if current `is_indoor` decision is False
if "sunny" in p365_attributes or "clear sky" in p365_attributes:
time_of_day = "day_clear"
confidence = 0.85 + (p365_confidence - P365_ATTRIBUTE_CONF_THRESHOLD) * 0.25
diagnostics["reason"] = "P365 attribute: sunny/clear sky (Outdoor)."
determined_by_p365_attr = True
elif ("nighttime" in p365_attributes or "night" in p365_attributes):
# Further refine based on lights if P365 confirms it's a typically lit outdoor night scene
if ("artificial lighting" in p365_attributes or "man-made lighting" in p365_attributes or \
any(kw in p365_mapped_scene for kw in ["street", "city", "road", "urban", "downtown"])):
time_of_day = "night_with_lights"
confidence = 0.82 + (p365_confidence - P365_ATTRIBUTE_CONF_THRESHOLD) * 0.20
diagnostics["reason"] = "P365 attribute: nighttime with artificial/street lights (Outdoor)."
else: # General dark night
time_of_day = "night_dark"
confidence = 0.78 + (p365_confidence - P365_ATTRIBUTE_CONF_THRESHOLD) * 0.20
diagnostics["reason"] = "P365 attribute: nighttime, dark (Outdoor)."
determined_by_p365_attr = True
elif "cloudy" in p365_attributes or "overcast" in p365_attributes:
time_of_day = "day_cloudy_overcast"
confidence = 0.80 + (p365_confidence - P365_ATTRIBUTE_CONF_THRESHOLD) * 0.25
diagnostics["reason"] = "P365 attribute: cloudy/overcast (Outdoor)."
determined_by_p365_attr = True
elif is_indoor: # Apply indoor attributes only if current `is_indoor` decision is True
if "artificial lighting" in p365_attributes or "man-made lighting" in p365_attributes:
base_indoor_conf = 0.70 + (p365_confidence - P365_ATTRIBUTE_CONF_THRESHOLD) * 0.20
if avg_brightness > config_thresholds.get("indoor_bright_thresh", 130):
time_of_day = "indoor_bright_artificial"
confidence = base_indoor_conf + 0.10
elif avg_brightness > config_thresholds.get("indoor_moderate_thresh", 95):
time_of_day = "indoor_moderate_artificial"
confidence = base_indoor_conf
else:
time_of_day = "indoor_dim_artificial" # More specific than _general
confidence = base_indoor_conf - 0.05
diagnostics["reason"] = f"P365 attribute: artificial lighting (Indoor), brightness based category: {time_of_day}."
determined_by_p365_attr = True
elif "natural lighting" in p365_attributes and \
(is_likely_home_environment or any(kw in p365_mapped_scene for kw in ["living_room", "bedroom", "sunroom"])):
time_of_day = "indoor_residential_natural"
confidence = 0.80 + (p365_confidence - P365_ATTRIBUTE_CONF_THRESHOLD) * 0.20
diagnostics["reason"] = "P365 attribute: natural lighting in residential/applicable indoor scene."
determined_by_p365_attr = True
# Step 2: If P365 attributes didn't make a high-confidence decision
# proceed with visual feature analysis, but now refined by P365 scene context.
if not determined_by_p365_attr or confidence < 0.75: # If P365 attributes didn't strongly decide
# Store the initial P365-attribute based tod and conf if they existed
initial_tod_by_attr = time_of_day if determined_by_p365_attr else "unknown"
initial_conf_by_attr = confidence if determined_by_p365_attr else 0.5
# Reset for visual analysis, but keep P365 context in diagnostics
time_of_day = "unknown"
confidence = 0.5 # Base for visual
current_visual_reason = "" # For diagnostics from visual features
if is_indoor: # `is_indoor` is already P365-influenced from _analyze_indoor_outdoor
natural_light_hints = 0
if sky_like_blue_in_sky_region > 0.05 and sky_region_is_brighter: natural_light_hints += 1.0
if features.get("brightness_uniformity", 0.0) > 0.65 and features.get("brightness_std", 100.0) < 70: natural_light_hints += 1.0
if features.get("warm_ratio", 0.0) > 0.15 and avg_brightness > 110: natural_light_hints += 0.5
is_designer_lit_flag = (circular_lights > 0 or bright_spots_overall > 2) and \
features.get("brightness_uniformity", 0.0) > 0.6 and \
features.get("warm_ratio", 0.0) > 0.2 and \
avg_brightness > 90
if avg_brightness > config_thresholds.get("indoor_bright_thresh", 130):
if natural_light_hints >= 1.5 and (is_likely_home_environment or any(kw in p365_mapped_scene for kw in ["home", "residential", "living", "bedroom"])):
time_of_day = "indoor_residential_natural"
confidence = 0.82
current_visual_reason = "Visual: Bright residential, natural window light hints."
elif is_designer_lit_flag and (is_likely_home_environment or any(kw in p365_mapped_scene for kw in ["home", "designer", "modern_interior"])):
time_of_day = "indoor_designer_residential"
confidence = 0.85
current_visual_reason = "Visual: Bright, designer-lit residential."
elif sky_like_blue_in_sky_region > 0.03 and sky_region_is_brighter:
time_of_day = "indoor_bright_natural_mix"
confidence = 0.78
current_visual_reason = "Visual: Bright indoor, mixed natural/artificial (window)."
else:
time_of_day = "indoor_bright_artificial"
confidence = 0.75
current_visual_reason = "Visual: High brightness, artificial indoor."
elif avg_brightness > config_thresholds.get("indoor_moderate_thresh", 95):
if is_designer_lit_flag and (is_likely_home_environment or any(kw in p365_mapped_scene for kw in ["home", "designer"])):
time_of_day = "indoor_designer_residential"
confidence = 0.78
current_visual_reason = "Visual: Moderately bright, designer-lit residential."
elif features.get("warm_ratio", 0.0) > 0.35 and yellow_orange_ratio > 0.1:
if any(kw in p365_mapped_scene for kw in P365_INDOOR_RESTAURANT_KEYWORDS) and \
p365_confidence > P365_SCENE_MODERATE_CONF_THRESHOLD :
time_of_day = "indoor_restaurant_bar"
confidence = 0.80 + p365_confidence * 0.15 # Boost with P365 context
current_visual_reason = "Visual: Moderate warm tones. P365 context confirms restaurant/bar."
elif any(kw in p365_mapped_scene for kw in P365_OUTDOOR_SCENE_KEYWORDS) and \
p365_confidence > P365_SCENE_MODERATE_CONF_THRESHOLD :
# This shouldn't happen if `is_indoor` was correctly set to False by P365 in `_analyze_indoor_outdoor`
# But as a fallback, if is_indoor=True but P365 scene context says strongly outdoor
time_of_day = "indoor_moderate_artificial" # Fallback to general indoor
confidence = 0.55 # Lower confidence due to strong conflict
current_visual_reason = "Visual: Moderate warm. CONFLICT: LA says indoor but P365 scene is outdoor. Defaulting to general indoor artificial."
diagnostics["conflict_is_indoor_vs_p365_scene_for_restaurant_bar"] = True
else: # P365 context is neutral, or not strongly conflicting restaurant/bar
time_of_day = "indoor_restaurant_bar"
confidence = 0.70 # Standard confidence without strong P365 confirmation
current_visual_reason = "Visual: Moderate warm tones, typical of restaurant/bar. P365 context neutral or weak."
else:
time_of_day = "indoor_moderate_artificial"
confidence = 0.70
current_visual_reason = "Visual: Moderate brightness, standard artificial indoor."
else: # Dimmer indoor
if features.get("warm_ratio", 0.0) > 0.45 and yellow_orange_ratio > 0.15:
time_of_day = "indoor_dim_warm"
confidence = 0.75
current_visual_reason = "Visual: Dim indoor with very warm tones."
else:
time_of_day = "indoor_dim_general"
confidence = 0.70
current_visual_reason = "Visual: Low brightness indoor."
# Refined commercial check (indoor)
if "residential" not in time_of_day and "restaurant" not in time_of_day and "bar" not in time_of_day and \
not (any(kw in p365_mapped_scene for kw in P365_INDOOR_RESTAURANT_KEYWORDS)): # Avoid reclassifying if P365 already said restaurant/bar
if avg_brightness > config_thresholds.get("commercial_min_brightness_thresh", 105) and \
bright_spots_overall > config_thresholds.get("commercial_min_spots_thresh", 3) and \
(light_dist_uniformity > 0.5 or features.get("ceiling_likelihood",0) > 0.4):
if not (any(kw in p365_mapped_scene for kw in ["home", "residential"])): # Don't call commercial if P365 suggests home
time_of_day = "indoor_commercial"
confidence = 0.70 + min(0.2, bright_spots_overall * 0.02)
current_visual_reason = "Visual: Multiple/structured light sources in non-residential/restaurant setting."
diagnostics["visual_analysis_reason"] = current_visual_reason
else: # Outdoor (is_indoor is False, influenced by P365 in the previous step)
current_visual_reason = ""
if (any(kw in p365_mapped_scene for kw in P365_OUTDOOR_SCENE_KEYWORDS) and any(kw in p365_mapped_scene for kw in ["street", "city", "road", "urban", "downtown", "intersection"])) and \
p365_confidence > P365_SCENE_MODERATE_CONF_THRESHOLD and \
features.get("color_atmosphere") == "warm" and \
avg_brightness < config_thresholds.get("outdoor_dusk_dawn_thresh_brightness", 135): # Not bright daytime
if avg_brightness < config_thresholds.get("outdoor_night_thresh_brightness", 85) and \
bright_spots_overall > config_thresholds.get("outdoor_night_lights_thresh", 2):
time_of_day = "night_with_lights"
confidence = 0.88 + p365_confidence * 0.1 # High confidence
current_visual_reason = f"P365 outdoor scene '{p365_mapped_scene}' + visual low-warm light with spots -> night_with_lights."
elif avg_brightness >= config_thresholds.get("outdoor_night_thresh_brightness", 85) : # Dusk/Dawn range
time_of_day = "sunset_sunrise"
confidence = 0.88 + p365_confidence * 0.1 # High confidence
current_visual_reason = f"P365 outdoor scene '{p365_mapped_scene}' + visual moderate-warm light -> sunset/sunrise."
else: # Too dark for sunset, but not enough spots for "night_with_lights" based on pure visual
time_of_day = "night_dark" # Fallback if P365 indicates night but visual light spots are few
confidence = 0.75 + p365_confidence * 0.1
current_visual_reason = f"P365 outdoor scene '{p365_mapped_scene}' + visual very low light -> night_dark."
# Fallback to your original visual logic if P365 street context isn't strong enough for above
elif avg_brightness < config_thresholds.get("outdoor_night_thresh_brightness", 85):
if bright_spots_overall > config_thresholds.get("outdoor_night_lights_thresh", 2):
time_of_day = "night_with_lights"
confidence = 0.82 + min(0.13, features.get("dark_pixel_ratio", 0.0) / 2.5)
current_visual_reason = "Visual: Low brightness with light sources (street/car lights)."
else:
time_of_day = "night_dark"
confidence = 0.78 + min(0.17, features.get("dark_pixel_ratio", 0.0) / 1.8)
current_visual_reason = "Visual: Very low brightness outdoor, deep night."
elif avg_brightness < config_thresholds.get("outdoor_dusk_dawn_thresh_brightness", 135) and \
yellow_orange_ratio > config_thresholds.get("outdoor_dusk_dawn_color_thresh", 0.10) and \
features.get("color_atmosphere") == "warm" and \
sky_region_brightness_ratio < 1.5 :
time_of_day = "sunset_sunrise"
confidence = 0.75 + min(0.20, yellow_orange_ratio / 1.5)
current_visual_reason = "Visual: Moderate brightness, warm tones -> sunset/sunrise."
if any(kw in p365_mapped_scene for kw in ["beach", "mountain", "lake", "ocean", "desert", "field", "natural_landmark", "sky"]) and \
p365_confidence > P365_SCENE_MODERATE_CONF_THRESHOLD:
confidence = min(0.95, confidence + 0.15)
current_visual_reason += f" P365 natural scene '{p365_mapped_scene}' supports."
elif avg_brightness > config_thresholds.get("outdoor_day_bright_thresh", 140) and \
(sky_like_blue_in_sky_region > config_thresholds.get("outdoor_day_blue_thresh", 0.05) or \
(sky_region_is_brighter and top_texture_complexity_val < 0.4) ):
time_of_day = "day_clear"
confidence = 0.80 + min(0.15, sky_like_blue_in_sky_region * 2 + (sky_like_blue_in_sky_region*1.5 if sky_region_is_brighter else 0) ) # Corrected feature name
current_visual_reason = "Visual: High brightness with blue/sky tones or bright smooth top."
elif avg_brightness > config_thresholds.get("outdoor_day_cloudy_thresh", 120):
if sky_region_is_brighter and top_texture_complexity_val < 0.45 and features.get("avg_saturation", 100) < 70:
time_of_day = "day_cloudy_overcast"
confidence = 0.75 + min(0.20, gray_ratio / 1.5 + (features.get("brightness_uniformity",0.0)-0.5)/1.5)
current_visual_reason = "Visual: Good brightness, uniform bright top, lower saturation -> overcast."
elif gray_ratio > config_thresholds.get("outdoor_day_gray_thresh", 0.18):
time_of_day = "day_cloudy_gray"
confidence = 0.72 + min(0.23, gray_ratio / 1.8)
current_visual_reason = "Visual: Good brightness with higher gray tones."
else:
time_of_day = "day_bright_general"
confidence = 0.68
current_visual_reason = "Visual: Bright outdoor, specific type less clear."
else: # Fallback for outdoor
if features.get("color_atmosphere") == "warm" and yellow_orange_ratio > 0.08:
time_of_day = "sunset_sunrise_low_confidence"
confidence = 0.62
elif sky_like_blue_in_sky_region > 0.02 or features.get("sky_region_blue_dominance",0) > 0.03 :
time_of_day = "day_hazy_or_partly_cloudy"
confidence = 0.62
else:
time_of_day = "outdoor_unknown_daylight"
confidence = 0.58
current_visual_reason = "Visual: Outdoor, specific conditions less clear; broader visual cues."
# Visual check for stadium/floodlit (only if is_indoor is false)
if avg_brightness > 150 and \
features.get("brightness_uniformity",0.0) > 0.70 and \
bright_spots_overall > config_thresholds.get("stadium_min_spots_thresh", 6):
time_of_day = "stadium_or_floodlit_area"
confidence = 0.78
current_visual_reason = "Visual: Very bright, uniform lighting with multiple sources, suggests floodlights (Outdoor)."
diagnostics["visual_analysis_reason"] = current_visual_reason
# If P365 attributes made a decision, and visual analysis refined it or provided a different one,
# we need to decide which one to trust or how to blend.
# If P365 attributes were strong (determined_by_p365_attr=True and initial_p365_confidence >=0.8), we stick with it.
# Otherwise, the visual analysis (now also P365 scene-context-aware) takes over.
if determined_by_p365_attr and initial_conf_by_attr >= 0.80 and initial_tod_by_attr != "unknown":
# time_of_day and confidence are already set from P365 attributes.
diagnostics["final_decision_source"] = "High-confidence P365 attribute."
else:
# If P365 attribute was not decisive, or visual analysis provided a different and
# potentially more nuanced result (especially if P365 scene context was used in visual path),
diagnostics["final_decision_source"] = "Visual features (potentially P365-context-refined)."
if initial_tod_by_attr != "unknown" and initial_tod_by_attr != time_of_day:
diagnostics["p365_attr_overridden_by_visual"] = f"P365 Attr ToD {initial_tod_by_attr} (Conf {initial_conf_by_attr:.2f}) was less certain or overridden by visual logic result {time_of_day} (Conf {confidence:.2f})."
# Neon/Sodium Vapor Night (can apply to either indoor if bar-like, or outdoor street)
# This refinement can apply *after* the main decision.
is_current_night_or_dim_warm = "night" in time_of_day or time_of_day == "indoor_dim_warm"
# Define these thresholds here if not in self.config or use self.config.get()
neon_yellow_orange_thresh = self.config.get("neon_yellow_orange_thresh", 0.12)
neon_bright_spots_thresh = self.config.get("neon_bright_spots_thresh", 4)
neon_avg_saturation_thresh = self.config.get("neon_avg_saturation_thresh", 60)
if is_current_night_or_dim_warm and \
yellow_orange_ratio > neon_yellow_orange_thresh and \
bright_spots_overall > neon_bright_spots_thresh and \
features.get("color_atmosphere") == "warm" and \
features.get("avg_saturation",0) > neon_avg_saturation_thresh:
old_time_of_day_for_neon_check = time_of_day
old_confidence_for_neon_check = confidence
# Check P365 context for "neon" related scenes
is_p365_neon_context = any(kw in p365_mapped_scene for kw in ["neon", "nightclub", "bar_neon"]) or \
"neon" in p365_attributes
if is_indoor:
if is_p365_neon_context or any(kw in p365_mapped_scene for kw in P365_INDOOR_RESTAURANT_KEYWORDS): # e.g. bar with neon
time_of_day = "indoor_neon_lit"
confidence = max(confidence, 0.80) # Boost confidence if P365 supports
else: # Generic indoor dim warm with neon characteristics
time_of_day = "indoor_dim_warm_neon_accent" # A more nuanced category
confidence = max(confidence, 0.77)
else: # outdoor street neon
if is_p365_neon_context or any(kw in p365_mapped_scene for kw in ["street_night", "city_night", "downtown_night"]):
time_of_day = "neon_or_sodium_vapor_night"
confidence = max(confidence, 0.82) # Boost confidence
else: # Generic outdoor night with neon characteristics
time_of_day = "night_with_neon_lights" # A more nuanced category
confidence = max(confidence, 0.79)
diagnostics["special_lighting_detected"] = (
f"Refined from {old_time_of_day_for_neon_check} (Conf:{old_confidence_for_neon_check:.2f}) "
f"to {time_of_day} (Conf:{confidence:.2f}) due to neon/sodium vapor light characteristics. "
f"P365 Context: {p365_mapped_scene if is_p365_neon_context else 'N/A'}."
)
# Final confidence clamp
confidence = min(0.95, max(0.50, confidence))
diagnostics["final_lighting_time_of_day"] = time_of_day
diagnostics["final_lighting_confidence"] = round(confidence,3)
return {
"time_of_day": time_of_day,
"confidence": confidence,
"diagnostics": diagnostics
}
def _get_default_config(self) -> Dict[str, Any]:
"""
Returns default configuration parameters, with adjustments for better balance.
"""
return {
# Thresholds for feature calculation (from _compute_basic_features)
"dark_pixel_threshold": 50,
"bright_pixel_threshold": 220,
"sky_blue_hue_min": 95,
"sky_blue_hue_max": 135,
"sky_blue_sat_min": 40,
"sky_blue_val_min": 90,
"gray_sat_max": 70,
"gray_val_min": 60,
"gray_val_max": 220,
"light_source_abs_thresh": 220, # For old bright_spot_count compatibility if used
"warm_hue_ranges": [(0, 50), (330, 360)],
"cool_hue_ranges": [(90, 270)],
# Thresholds for _analyze_indoor_outdoor logic
"sky_blue_dominance_thresh": 0.18,
"sky_brightness_ratio_thresh": 1.25,
"openness_top_thresh": 0.68,
"sky_texture_complexity_thresh": 0.35,
"ceiling_likelihood_thresh": 0.4,
"boundary_clarity_thresh": 0.38,
"brightness_uniformity_thresh_indoor": 0.6,
"brightness_uniformity_thresh_outdoor": 0.40,
"many_bright_spots_thresh": 6,
"dim_scene_for_spots_thresh": 115,
"home_pattern_thresh_strong": 2.0,
"home_pattern_thresh_moderate": 1.0,
"warm_indoor_max_brightness_thresh": 135,
"aerial_top_dark_ratio_thresh": 0.9,
"aerial_top_complex_thresh": 0.60,
"aerial_min_avg_brightness_thresh": 65,
# Factors to reduce indoor cues if strong sky signal
"sky_override_factor_ceiling": 0.1,
"sky_override_factor_boundary": 0.2,
"sky_override_factor_uniformity": 0.15,
"sky_override_factor_lights": 0.05,
# Factor to reduce enclosure score if aerial street pattern detected
"aerial_enclosure_reduction_factor": 0.75,
# Weights for _analyze_indoor_outdoor scoring (positive = indoor, negative = outdoor)
"indoor_outdoor_weights": {
# Sky/Openness (Negative values push towards outdoor)
"sky_blue_dominance_w": 3.5,
"sky_brightness_ratio_w": 3,
"openness_top_w": 2.8,
"sky_texture_w": 2,
# Ceiling/Enclosure (Positive values push towards indoor)
"ceiling_likelihood_w": 1.5,
"boundary_clarity_w": 1.2,
# Brightness
"brightness_uniformity_w": 0.6,
"brightness_non_uniformity_outdoor_w": 1.0,
"brightness_non_uniformity_indoor_penalty_w": 0.1,
# Light Sources
"circular_lights_w": 1.2,
"indoor_light_score_w": 0.8,
"many_bright_spots_indoor_w": 0.3,
# Color Atmosphere
"warm_atmosphere_indoor_w": 0.15,
# Home Environment Pattern (structural cues for indoor)
"home_env_strong_w": 1.5,
"home_env_moderate_w": 0.7,
# Aerial street pattern (negative pushes to outdoor)
"aerial_street_w": 2.5,
"places365_outdoor_scene_w": 4.0, # Places365 明確判斷為室外場景時的強烈負(室外)權重
"places365_indoor_scene_w": 3.0, # Places365 明確判斷為室內場景時的正面(室內)權重
"places365_attribute_w": 1.5,
"blue_ratio": 0.0, "gradient_ratio": 0.0, "bright_spots": 0.0,
"color_tone": 0.0, "sky_brightness": 0.0, "ceiling_features": 0.0,
"light_features": 0.0, "boundary_features": 0.0,
"street_features": 0.0, "building_features": 0.0,
},
"indoor_score_sigmoid_scale": 0.3,
"indoor_decision_threshold": 0.5,
# Places365 相關閾值
"places365_high_confidence_thresh": 0.75, # Places365 判斷結果被視為高信心度的閾值
"places365_moderate_confidence_thresh": 0.5, # Places365 中等信心度閾值
"places365_attribute_confidence_thresh": 0.6, # Places365 屬性判斷的置信度閾值
"p365_outdoor_reduces_enclosure_factor": 0.3, # 如果P365認為是室外,圍合特徵的影響降低到30%
"p365_indoor_boosts_ceiling_factor": 1.5,
# Thresholds for _determine_lighting_conditions (outdoor)
"outdoor_night_thresh_brightness": 80,
"outdoor_night_lights_thresh": 2,
"outdoor_dusk_dawn_thresh_brightness": 130,
"outdoor_dusk_dawn_color_thresh": 0.10,
"outdoor_day_bright_thresh": 140,
"outdoor_day_blue_thresh": 0.05,
"outdoor_day_cloudy_thresh": 120,
"outdoor_day_gray_thresh": 0.18,
"include_diagnostics": True,
"ceiling_likelihood_thresh_indoor": 0.38,
"sky_blue_dominance_strong_thresh": 0.35,
"sky_brightness_ratio_strong_thresh": 1.35,
"sky_texture_complexity_clear_thresh": 0.25,
"openness_top_strong_thresh": 0.80,
"sky_texture_complexity_cloudy_thresh": 0.20,
"sky_brightness_ratio_cloudy_thresh": 0.95,
"ceiling_texture_thresh": 0.4,
"ceiling_brightness_min": 60,
"ceiling_brightness_max": 230,
"ceiling_horizontal_line_factor": 1.15,
"ceiling_center_bright_factor": 1.25,
"ceiling_max_sky_blue_thresh": 0.08,
"ceiling_max_sky_brightness_ratio": 1.15,
"ceiling_sky_override_factor": 0.1,
"stadium_min_spots_thresh": 6
}