import logging import traceback from typing import Dict, List, Any, Optional logger = logging.getLogger(__name__) class FunctionalZoneIdentifier: """ 作為功能區域辨識的主要窗口 整合區域評估和場景特定的區域辨識邏輯,提供統一的功能區域辨識接口 """ def __init__(self, zone_evaluator=None, scene_zone_identifier=None, scene_viewpoint_analyzer=None, object_categories=None): """ 初始化功能區域識別器 Args: zone_evaluator: 區域評估器實例 scene_zone_identifier: 場景區域辨識器實例 scene_viewpoint_analyzer: 場景視角分析器 """ try: self.zone_evaluator = zone_evaluator self.scene_zone_identifier = scene_zone_identifier self.scene_viewpoint_analyzer = scene_viewpoint_analyzer self.viewpoint_detector = scene_viewpoint_analyzer self.OBJECT_CATEGORIES = object_categories or {} logger.info("FunctionalZoneIdentifier initialized successfully with SceneViewpointAnalyzer") except Exception as e: logger.error(f"Failed to initialize FunctionalZoneIdentifier: {str(e)}") logger.error(traceback.format_exc()) raise def identify_functional_zones(self, detected_objects: List[Dict], scene_type: str) -> Dict: """ 識別場景內的功能區域,具有針對不同視角和文化背景的改進檢測能力。 如果偵測到 is_landmark=True 的物件,則優先直接呼叫 identify_landmark_zones 並回傳結果。 """ try: # 1. 如果沒有啟用地標功能,就先把所有有 is_landmark=True 的物件過濾掉 if not getattr(self, 'enable_landmark', True): detected_objects = [obj for obj in detected_objects if not obj.get("is_landmark", False)] # 2. 只要檢測到任何 is_landmark=True 的物件,立即優先使用 identify_landmark_zones landmark_objects = [obj for obj in detected_objects if obj.get("is_landmark", False)] if landmark_objects and self.scene_zone_identifier: lm_zones = self.scene_zone_identifier.identify_landmark_zones(landmark_objects) return self._standardize_zone_keys_and_descriptions(lm_zones) # 3. city_street if scene_type in ["tourist_landmark", "natural_landmark", "historical_monument"]: scene_type = "city_street" # 4. 判斷與物件數量檢查 if self.zone_evaluator: should_identify = self.zone_evaluator.evaluate_zone_identification_feasibility( detected_objects, scene_type ) if not should_identify: logger.info(f"Zone identification not feasible for scene type '{scene_type}'") return {} else: if len(detected_objects) < 2: logger.info("Insufficient objects for zone identification") return {} # 5. 建立 category_regions category_regions = self._build_category_regions_mapping(detected_objects) zones = {} # 6. 檢測場景視角 viewpoint_info = {"viewpoint": "eye_level"} if self.scene_viewpoint_analyzer: viewpoint_info = self.scene_viewpoint_analyzer.detect_scene_viewpoint(detected_objects) # 7. 根據不同 scene_type 使用各種自己的區域辨識 if scene_type in ["living_room", "bedroom", "dining_area", "kitchen", "office_workspace", "meeting_room"]: if self.scene_zone_identifier: raw_zones = self.scene_zone_identifier.identify_indoor_zones( category_regions, detected_objects, scene_type ) zones.update(self._standardize_zone_keys_and_descriptions(raw_zones)) elif scene_type in ["city_street", "parking_lot", "park_area"]: if self.scene_zone_identifier: raw_zones = self.scene_zone_identifier.identify_outdoor_general_zones( category_regions, detected_objects, scene_type ) zones.update(self._standardize_zone_keys_and_descriptions(raw_zones)) elif "aerial" in scene_type or viewpoint_info.get("viewpoint") == "aerial": if self.scene_zone_identifier: raw_zones = self.scene_zone_identifier.identify_aerial_view_zones( category_regions, detected_objects, scene_type ) zones.update(self._standardize_zone_keys_and_descriptions(raw_zones)) elif "asian" in scene_type: if self.scene_zone_identifier: asian_zones = self.scene_zone_identifier.identify_asian_cultural_zones( category_regions, detected_objects, scene_type ) zones.update(self._standardize_zone_keys_and_descriptions(asian_zones)) elif scene_type == "urban_intersection": if self.scene_zone_identifier: raw_zones = self.scene_zone_identifier.identify_intersection_zones( category_regions, detected_objects, viewpoint_info.get("viewpoint") ) zones.update(self._standardize_zone_keys_and_descriptions(raw_zones)) used_tl_count_per_region = {} for zone_info in raw_zones.values(): obj_list = zone_info.get("objects", []) if "traffic light" in obj_list: rg = zone_info.get("region", "") count_in_zone = obj_list.count("traffic light") used_tl_count_per_region[rg] = used_tl_count_per_region.get(rg, 0) + count_in_zone signal_regions = {} for t in [obj for obj in detected_objects if obj.get("class_id") == 9]: region = t.get("region", "") signal_regions.setdefault(region, []).append(t) for idx, (region, signals) in enumerate(signal_regions.items()): total_in_region = len(signals) used_in_region = used_tl_count_per_region.get(region, 0) remaining_in_region = total_in_region - used_in_region if remaining_in_region > 0: direction = self._get_directional_description(region) if direction and direction != "central": zone_key = f"{direction} traffic control area" else: zone_key = "primary traffic control area" if idx == 0 else "auxiliary traffic control area" if zone_key in zones: suffix = 1 new_key = f"{zone_key} ({suffix})" while new_key in zones: suffix += 1 new_key = f"{zone_key} ({suffix})" zone_key = new_key zones[zone_key] = { "region": region, "objects": ["traffic light"] * remaining_in_region, "description": f"Traffic control area with {remaining_in_region} traffic lights in {region}" } for region, signals in signal_regions.items(): used = used_tl_count_per_region.get(region, 0) total = len(signals) remaining = total - used # print(f"[DEBUG] Region '{region}': Total TL = {total}, Used in crossing = {used}, Remaining = {remaining}") elif scene_type == "financial_district": if self.scene_zone_identifier: fd_zones = self.scene_zone_identifier.identify_financial_district_zones( category_regions, detected_objects ) zones.update(self._standardize_zone_keys_and_descriptions(fd_zones)) elif scene_type == "upscale_dining": if self.scene_zone_identifier: ud_zones = self.scene_zone_identifier.identify_upscale_dining_zones( category_regions, detected_objects ) zones.update(self._standardize_zone_keys_and_descriptions(ud_zones)) else: # 如果不是上述任何一種場景,就用「預設功能區」 default_zones = self._identify_default_zones(category_regions, detected_objects) zones.update(self._standardize_zone_keys_and_descriptions(default_zones)) # 8. 如果此時 zones 仍為空,就會變成 default → basic → fallback if not zones: default_zones = self._identify_default_zones(category_regions, detected_objects) if default_zones: zones.update(self._standardize_zone_keys_and_descriptions(default_zones)) else: basic_zones = self._create_basic_zones_from_objects(detected_objects, scene_type) zones.update(self._standardize_zone_keys_and_descriptions(basic_zones)) # 通用 fallback:把所有還沒被列出的 (class_name, region) 通通補進去 fallback_zones = self._generate_category_fallback_zones(detected_objects, zones) zones.update(fallback_zones) # Debug: 列印出各功能區的 traffic light 統計 total_tl_in_zones = 0 for zone_key, zone_info in zones.items(): if isinstance(zone_info, dict): sub_objs = zone_info.get("objects", []) else: sub_objs = [] t_in_zone = [obj for obj in sub_objs if obj == "traffic light"] # print(f"[DEBUG] identify_functional_zones - Zone '{zone_key}' has {len(t_in_zone)} traffic light(s).") total_tl_in_zones += len(t_in_zone) # print(f"[DEBUG] identify_functional_zones - Total traffic lights in zones: {total_tl_in_zones}") logger.info(f"Identified {len(zones)} functional zones for scene type '{scene_type}'") return zones except Exception as e: logger.error(f"Error identifying functional zones: {str(e)}") logger.error(traceback.format_exc()) return {} def _standardize_zone_keys_and_descriptions(self, raw_zones: Dict) -> Dict: """ 標準化區域鍵名和描述,將內部標識符轉換為描述性名稱 Args: raw_zones: 原始區域識別結果 Returns: Dict: 標準化後的區域字典 """ try: standardized_zones = {} for zone_key, zone_data in raw_zones.items(): # 生成描述性的區域鍵名 descriptive_key = self._generate_descriptive_zone_key(zone_key, zone_data) # 確保區域描述也經過標準化 if isinstance(zone_data, dict) and "description" in zone_data: zone_data["description"] = self._enhance_zone_description(zone_data["description"], zone_data) standardized_zones[descriptive_key] = zone_data return standardized_zones except Exception as e: logger.error(f"Error standardizing zone keys and descriptions: {str(e)}") return raw_zones def _generate_descriptive_zone_key(self, original_key: str, zone_data: Dict) -> str: """ 基於區域內容生成描述性的鍵名 核心修改:只要該區域內有任一個 'traffic light',就優先回傳 'traffic control zone', """ try: objects = zone_data.get("objects", []) region = zone_data.get("region", "") # 優先檢查是否含有 traffic light if any(obj == "traffic light" or "traffic light" in obj for obj in objects): return "traffic control zone" # 如果沒有 traffic light,才繼續分析「主要物件」順序 primary_objects = self._analyze_primary_objects(objects) # 依序檢查人、車、家具、紅綠燈等 if "person" in primary_objects: if len([o for o in objects if o == "person"]) > 1: return "pedestrian activity area" else: return "individual activity zone" elif any(vehicle in primary_objects for vehicle in ["car", "truck", "bus", "motorcycle"]): return "vehicle movement area" elif any(furniture in primary_objects for furniture in ["chair", "table", "sofa", "bed"]): return "furniture arrangement area" # 若上述都不符合,改用「基於位置」做 fallback position_descriptions = { "top_left": "upper left area", "top_center": "upper central area", "top_right": "upper right area", "middle_left": "left side area", "middle_center": "main crossing area", "middle_right": "right side area", "bottom_left": "lower left area", "bottom_center": "lower central area", "bottom_right": "lower right area" } if region in position_descriptions: return position_descriptions[region] # 再次檢查主要物件,給出另一種 fallback 命名 if primary_objects: if "traffic light" in primary_objects: return "traffic control zone" elif any(vehicle in primary_objects for vehicle in ["car", "truck", "bus"]): return "vehicle movement area" elif "person" in primary_objects: return "pedestrian activity area" # 最後最後的備用名稱 return "activity area" except Exception as e: logger.warning(f"Error generating descriptive key for '{original_key}': {str(e)}") return "activity area" def _analyze_primary_objects(self, objects: List[str]) -> List[str]: """ 分析區域中的主要物件類型 Args: objects: 物件名稱列表 Returns: List[str]: 主要物件類型列表 """ try: # 計算物件出現頻率 object_counts = {} for obj in objects: normalized_obj = obj.replace('_', ' ').lower().strip() object_counts[normalized_obj] = object_counts.get(normalized_obj, 0) + 1 # 按出現頻率排序,返回前三個主要物件 sorted_objects = sorted(object_counts.items(), key=lambda x: x[1], reverse=True) return [obj[0] for obj in sorted_objects[:3]] except Exception as e: logger.warning(f"Error analyzing primary objects: {str(e)}") return [] def _enhance_zone_description(self, original_description: str, zone_data: Dict) -> str: """ 增強區域描述的自然性和完整性 """ try: if not original_description or not original_description.strip(): return self._generate_fallback_description(zone_data) import re enhanced = original_description.strip() # 改善技術性表達為自然語言 enhanced = re.sub(r'\bin central direction\b', 'in the center', enhanced) enhanced = re.sub(r'\bin west area\b', 'on the left side', enhanced) enhanced = re.sub(r'\bin east direction\b', 'on the right side', enhanced) enhanced = re.sub(r'\bnear traffic signals\b', 'near the traffic lights', enhanced) enhanced = re.sub(r'\bwith (\d+) (\w+)\b', r'where \1 \2 can be seen', enhanced) # 移除重複和冗餘表達 enhanced = re.sub(r'\barea with.*?in.*?area\b', lambda m: m.group(0).split(' in ')[0], enhanced) enhanced = enhanced.replace('traffic area', 'area').replace('crossing area', 'crossing') # 標準化描述結構 if enhanced.startswith('Pedestrian'): enhanced = re.sub(r'^Pedestrian crossing area', 'The main pedestrian crossing', enhanced) elif enhanced.startswith('Vehicle'): enhanced = re.sub(r'^Vehicle traffic area', 'The vehicle movement area', enhanced) elif enhanced.startswith('Traffic control'): enhanced = re.sub(r'^Traffic control area', 'Traffic management elements', enhanced) # 移除內部標識符格式 enhanced = re.sub(r'\b\w+_\w+(?:_\w+)*\b', lambda m: m.group(0).replace('_', ' '), enhanced) # 確保描述的完整性 if not enhanced.endswith('.'): enhanced += '.' # 改善描述的自然性 enhanced = enhanced.replace('with with', 'with') enhanced = re.sub(r'\s{2,}', ' ', enhanced) return enhanced except Exception as e: logger.warning(f"Error enhancing zone description: {str(e)}") return original_description if original_description else "A functional area within the scene." def _generate_fallback_description(self, zone_data: Dict) -> str: """ 為缺少描述的區域生成備用描述 Args: zone_data: 區域數據 Returns: str: 備用描述 """ try: objects = zone_data.get("objects", []) region = zone_data.get("region", "") if objects: object_count = len(objects) unique_objects = list(set(objects)) if object_count == 1: return f"Area containing {unique_objects[0].replace('_', ' ')}." elif len(unique_objects) <= 3: obj_list = ", ".join([obj.replace('_', ' ') for obj in unique_objects]) return f"Area featuring {obj_list}." else: return f"Multi-functional area with {object_count} elements including various objects." return "Functional area within the scene." except Exception as e: logger.warning(f"Error generating fallback description: {str(e)}") return "Activity area." def _build_category_regions_mapping(self, detected_objects: List[Dict]) -> Dict: """ 建立物件按類別和區域的分組映射 Args: detected_objects: 檢測到的物件列表 Returns: 按類別和區域分組的物件字典 """ try: category_regions = {} for obj in detected_objects: category = self._categorize_object(obj) if not category: continue if category not in category_regions: category_regions[category] = {} region = obj.get("region", "center") if region not in category_regions[category]: category_regions[category][region] = [] category_regions[category][region].append(obj) logger.debug(f"Built category regions mapping with {len(category_regions)} categories") return category_regions except Exception as e: logger.error(f"Error building category regions mapping: {str(e)}") logger.error(traceback.format_exc()) return {} def _categorize_object(self, obj: Dict) -> str: """ 將檢測到的物件分類到功能類別中,用於區域識別 確保所有返回值都使用自然語言格式,避免底線或技術性標識符 """ try: class_id = obj.get("class_id", -1) class_name = obj.get("class_name", "").lower().strip() # 優先處理 traffic light # 只要 class_id == 9 或 class_name 包含 "traffic light",就分類為 "traffic light" if class_id == 9 or "traffic light" in class_name: return "traffic light" # 如果有自訂的 OBJECT_CATEGORIES 映射,優先使用它 if hasattr(self, 'OBJECT_CATEGORIES') and self.OBJECT_CATEGORIES: for category, ids in self.OBJECT_CATEGORIES.items(): if class_id in ids: # 確保返回的類別名稱使用自然語言格式 return self._clean_category_name(category) # COCO class default name furniture_items = ["chair", "couch", "bed", "dining table", "toilet"] plant_items = ["potted plant"] electronic_items = ["tv", "laptop", "mouse", "remote", "keyboard", "cell phone"] vehicle_items = ["bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat"] person_items = ["person"] kitchen_items = [ "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "refrigerator", "oven", "toaster", "sink", "microwave" ] sports_items = [ "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket" ] personal_items = ["handbag", "tie", "suitcase", "umbrella", "backpack"] # fallback natural language if any(item in class_name for item in furniture_items): return "furniture" elif any(item in class_name for item in plant_items): return "plant" elif any(item in class_name for item in electronic_items): return "electronics" elif any(item in class_name for item in vehicle_items): return "vehicle" elif any(item in class_name for item in person_items): return "person" elif any(item in class_name for item in kitchen_items): return "kitchen items" # 移除底線 elif any(item in class_name for item in sports_items): return "sports" elif any(item in class_name for item in personal_items): return "personal items" # 移除底線 else: return "misc" except Exception as e: logger.error(f"Error categorizing object: {str(e)}") logger.error(traceback.format_exc()) return "misc" def _clean_category_name(self, category: str) -> str: """ 清理類別名稱,移除底線並轉換為較自然的格式 Args: category: 原始類別名稱 Returns: str: 清理後的類別名稱 """ try: if not category: return "misc" # 將底線替換為空格 cleaned = category.replace('_', ' ') # 處理常見的技術性命名模式 replacements = { 'kitchen items': 'kitchen items', 'personal items': 'personal items', 'traffic light': 'traffic light', 'misc items': 'misc' } # 應用特定的替換規則 for old_term, new_term in replacements.items(): if cleaned == old_term: return new_term return cleaned.strip() except Exception as e: logger.warning(f"Error cleaning category name '{category}': {str(e)}") return "misc" def _identify_default_zones(self, category_regions: Dict, detected_objects: List[Dict]) -> Dict: """ 當沒有匹配到特定場景類型時的一般功能區域識別 Args: category_regions: 按類別和區域分組的物件字典 detected_objects: 檢測到的物件列表 Returns: 預設功能區域字典 """ try: zones = {} # 按類別分組物件並找到主要集中區域 for category, regions in category_regions.items(): if not regions: continue # 找到此類別中物件最多的區域 main_region = max(regions.items(), key=lambda x: len(x[1]), default=(None, [])) if main_region[0] is None or len(main_region[1]) < 2: continue # 創建基於物件類別的區域 zone_objects = [obj["class_name"] for obj in main_region[1]] # 如果物件太少,跳過 if len(zone_objects) < 2: continue # 根據類別創建區域名稱和描述 if category == "furniture": zones["furniture arrangement area"] = { "region": main_region[0], "objects": zone_objects, "description": f"Furniture arrangement area featuring {self._format_object_list_naturally(zone_objects[:3])}" } elif category == "electronics": zones["electronics area"] = { "region": main_region[0], "objects": zone_objects, "description": f"Electronics area containing {self._format_object_list_naturally(zone_objects[:3])}" } elif category == "kitchen_items": zones["dining_zone"] = { "region": main_region[0], "objects": zone_objects, "description": f"Dining or food area with {', '.join(zone_objects[:3])}" } elif category == "vehicle": zones["vehicle_zone"] = { "region": main_region[0], "objects": zone_objects, "description": f"Area with vehicles including {', '.join(zone_objects[:3])}" } elif category == "personal_items": zones["personal_items_zone"] = { "region": main_region[0], "objects": zone_objects, "description": f"Area with personal items including {', '.join(zone_objects[:3])}" } # 檢查人群聚集 people_objs = [obj for obj in detected_objects if obj["class_id"] == 0] if len(people_objs) >= 2: people_regions = {} for obj in people_objs: region = obj["region"] if region not in people_regions: people_regions[region] = [] people_regions[region].append(obj) if people_regions: main_people_region = max(people_regions.items(), key=lambda x: len(x[1]), default=(None, [])) if main_people_region[0] is not None: zones["people_zone"] = { "region": main_people_region[0], "objects": ["person"] * len(main_people_region[1]), "description": f"Area with {len(main_people_region[1])} people" } logger.debug(f"Identified {len(zones)} default zones") return zones except Exception as e: logger.error(f"Error identifying default zones: {str(e)}") logger.error(traceback.format_exc()) return {} def _format_object_list_naturally(self, object_list: List[str]) -> str: """ 將物件列表格式化為自然語言表達 Args: object_list: 物件名稱列表 Returns: str: 自然語言格式的物件列表 """ try: if not object_list: return "various items" # 標準化物件名稱 normalized_objects = [] for obj in object_list: normalized = obj.replace('_', ' ').strip() if normalized: normalized_objects.append(normalized) if not normalized_objects: return "various items" # 格式化列表 if len(normalized_objects) == 1: return normalized_objects[0] elif len(normalized_objects) == 2: return f"{normalized_objects[0]} and {normalized_objects[1]}" else: return ", ".join(normalized_objects[:-1]) + f", and {normalized_objects[-1]}" except Exception as e: logger.warning(f"Error formatting object list naturally: {str(e)}") return "various items" def _create_basic_zones_from_objects(self, detected_objects: List[Dict], scene_type: str) -> Dict: """ 從個別高置信度物件創建基本功能區域 這是標準區域識別失敗時的後備方案 Args: detected_objects: 檢測到的物件列表 scene_type: 場景類型 Returns: 基本區域字典 """ try: zones = {} # 專注於高置信度物件 high_conf_objects = [obj for obj in detected_objects if obj.get("confidence", 0) >= 0.6] if not high_conf_objects: high_conf_objects = detected_objects # 後備到所有物件 # 基於個別重要物件創建區域 processed_objects = set() # 避免重複處理相同類型的物件 for obj in high_conf_objects[:3]: # 限制為前3個物件 class_name = obj["class_name"] region = obj.get("region", "center") # 避免為同一類型物件創建多個區域 if class_name in processed_objects: continue processed_objects.add(class_name) # 基於物件類型創建描述性區域 zone_description = self._get_basic_zone_description(class_name, scene_type) descriptive_key = self._generate_object_based_zone_key(class_name, region) if zone_description and descriptive_key: zones[descriptive_key] = { "region": region, "objects": [class_name], "description": zone_description } logger.debug(f"Created {len(zones)} basic zones from high confidence objects") return zones except Exception as e: logger.error(f"Error creating basic zones from objects: {str(e)}") logger.error(traceback.format_exc()) return {} def _generate_object_based_zone_key(self, class_name: str, region: str) -> str: """ 基於物件類型和位置生成描述性的區域鍵名 Args: class_name: 物件類別名稱 region: 區域位置 Returns: str: 描述性區域鍵名 """ try: # 標準化物件名稱 normalized_class = class_name.replace('_', ' ').lower().strip() # 物件類型對應的區域描述 object_zone_mapping = { 'person': 'activity area', 'car': 'vehicle area', 'truck': 'vehicle area', 'bus': 'vehicle area', 'motorcycle': 'vehicle area', 'bicycle': 'cycling area', 'traffic light': 'traffic control area', 'chair': 'seating area', 'sofa': 'seating area', 'bed': 'rest area', 'dining table': 'dining area', 'tv': 'entertainment area', 'laptop': 'workspace area', 'potted plant': 'decorative area' } base_description = object_zone_mapping.get(normalized_class, f"{normalized_class} area") # 添加位置信息以提供更具體的描述 position_modifiers = { 'top_left': 'upper left', 'top_center': 'upper central', 'top_right': 'upper right', 'middle_left': 'left side', 'middle_center': 'central', 'middle_right': 'right side', 'bottom_left': 'lower left', 'bottom_center': 'lower central', 'bottom_right': 'lower right' } if region in position_modifiers: return f"{position_modifiers[region]} {base_description}" return base_description except Exception as e: logger.warning(f"Error generating object-based zone key for '{class_name}': {str(e)}") return "activity area" def _get_basic_zone_description(self, class_name: str, scene_type: str) -> str: """ 基於物件和場景類型生成基本區域描述 Args: class_name: 物件類別名稱 scene_type: 場景類型 Returns: 區域描述字串 """ try: # 物件特定描述 descriptions = { "bed": "Sleeping and rest area", "sofa": "Seating and relaxation area", "chair": "Seating area", "dining table": "Dining and meal area", "tv": "Entertainment and media area", "laptop": "Work and computing area", "potted plant": "Decorative and green space area", "refrigerator": "Food storage and kitchen area", "car": "Vehicle and transportation area", "person": "Activity and social area" } return descriptions.get(class_name, f"Functional area with {class_name}") except Exception as e: logger.error(f"Error getting basic zone description for '{class_name}': {str(e)}") return f"Functional area with {class_name}" def _generate_category_fallback_zones(self, all_detected_objects: List[Dict], current_zones: Dict) -> Dict: """ 通用 fallback:針對 all_detected_objects 裡,每一個 (class_name, region) 組合是否已經 在 current_zones 裡出現過。如果還沒,就為它們產生一個 fallback zone。 """ general_fallback = { 0: 'person', 1: 'bicycle', 2: 'car', 3: 'motorcycle', 4: 'airplane', 5: 'bus', 6: 'train', 7: 'truck', 8: 'boat', 9: 'traffic light', 10: 'fire hydrant', 11: 'stop sign', 12: 'parking meter', 13: 'bench', 14: 'bird', 15: 'cat', 16: 'dog', 17: 'horse', 18: 'sheep', 19: 'cow', 20: 'elephant', 21: 'bear', 22: 'zebra', 23: 'giraffe', 24: 'backpack', 25: 'umbrella', 26: 'handbag', 27: 'tie', 28: 'suitcase', 29: 'frisbee', 30: 'skis', 31: 'snowboard', 32: 'sports ball', 33: 'kite', 34: 'baseball bat', 35: 'baseball glove', 36: 'skateboard', 37: 'surfboard', 38: 'tennis racket', 39: 'bottle', 40: 'wine glass', 41: 'cup', 42: 'fork', 43: 'knife', 44: 'spoon', 45: 'bowl', 46: 'banana', 47: 'apple', 48: 'sandwich', 49: 'orange', 50: 'broccoli', 51: 'carrot', 52: 'hot dog', 53: 'pizza', 54: 'donut', 55: 'cake', 56: 'chair', 57: 'couch', 58: 'potted plant', 59: 'bed', 60: 'dining table', 61: 'toilet', 62: 'tv', 63: 'laptop', 64: 'mouse', 65: 'remote', 66: 'keyboard', 67: 'cell phone', 68: 'microwave', 69: 'oven', 70: 'toaster', 71: 'sink', 72: 'refrigerator', 73: 'book', 74: 'clock', 75: 'vase', 76: 'scissors', 77: 'teddy bear', 78: 'hair drier', 79: 'toothbrush' } # 1. 統計 current_zones 裡,已使用掉的 (class_name, region) 次數 used_count = {} for zone_info in current_zones.values(): rg = zone_info.get("region", "") for obj_name in zone_info.get("objects", []): key = (obj_name, rg) used_count[key] = used_count.get(key, 0) + 1 # 2. 統計 all_detected_objects 裡的 (class_name, region) 總次數 total_count = {} for obj in all_detected_objects: cname = obj.get("class_name", "") rg = obj.get("region", "") key = (cname, rg) total_count[key] = total_count.get(key, 0) + 1 # 3. 把 default_classes 轉換成「class_name → fallback 區域 type」的對照表 category_to_fallback = { # 行人與交通工具 "person": "pedestrian area", "bicycle": "vehicle movement area", "car": "vehicle movement area", "motorcycle": "vehicle movement area", "airplane": "vehicle movement area", "bus": "vehicle movement area", "train": "vehicle movement area", "truck": "vehicle movement area", "boat": "vehicle movement area", "traffic light": "traffic control area", "fire hydrant": "traffic control area", "stop sign": "traffic control area", "parking meter": "traffic control area", "bench": "public furniture area", # 動物類、鳥類 "bird": "animal area", "cat": "animal area", "dog": "animal area", "horse": "animal area", "sheep": "animal area", "cow": "animal area", "elephant": "animal area", "bear": "animal area", "zebra": "animal area", "giraffe": "animal area", # 托運與行李 "backpack": "personal items area", "umbrella": "personal items area", "handbag": "personal items area", "tie": "personal items area", "suitcase": "personal items area", # 運動器材 "frisbee": "sports area", "skis": "sports area", "snowboard": "sports area", "sports ball": "sports area", "kite": "sports area", "baseball bat": "sports area", "baseball glove":"sports area", "skateboard": "sports area", "surfboard": "sports area", "tennis racket": "sports area", # 廚房與食品(Kitchen) "bottle": "kitchen area", "wine glass": "kitchen area", "cup": "kitchen area", "fork": "kitchen area", "knife": "kitchen area", "spoon": "kitchen area", "bowl": "kitchen area", "banana": "kitchen area", "apple": "kitchen area", "sandwich": "kitchen area", "orange": "kitchen area", "broccoli": "kitchen area", "carrot": "kitchen area", "hot dog": "kitchen area", "pizza": "kitchen area", "donut": "kitchen area", "cake": "kitchen area", "dining table": "furniture arrangement area", "refrigerator": "kitchen area", "oven": "kitchen area", "microwave": "kitchen area", "toaster": "kitchen area", "sink": "kitchen area", "book": "miscellaneous area", "clock": "miscellaneous area", "vase": "decorative area", "scissors": "miscellaneous area", "teddy bear": "miscellaneous area", "hair drier": "miscellaneous area", "toothbrush": "miscellaneous area", # 電子產品 "tv": "electronics area", "laptop": "electronics area", "mouse": "electronics area", "remote": "electronics area", "keyboard": "electronics area", "cell phone": "electronics area", # 家具類 "chair": "furniture arrangement area", "couch": "furniture arrangement area", "bed": "furniture arrangement area", "toilet": "furniture arrangement area", # 植物(室內植物或戶外綠化) "potted plant": "decorative area", } # 4. 計算缺少的 (class_name, region) 並建立 fallback zone for (cname, rg), total in total_count.items(): used = used_count.get((cname, rg), 0) missing = total - used if missing <= 0: continue # (A) 決定這個 cname 在 fallback 裡屬於哪個大 class(zone_type) zone_type = category_to_fallback.get(cname, "miscellaneous area") # (B) 根據 region 與 zone_type 組合成 fallback_key fallback_key = f"{rg} {zone_type}" # (C) 如果名稱重複,就在後面加 (1),(2),… 避免掉衝突 if fallback_key in current_zones or fallback_key in general_fallback: suffix = 1 new_key = f"{fallback_key} ({suffix})" while new_key in current_zones or new_key in general_fallback: suffix += 1 new_key = f"{fallback_key} ({suffix})" fallback_key = new_key # (D) 建立這支 fallback zone,objects 裡放 missing 個 cname general_fallback[fallback_key] = { "region": rg, "objects": [cname] * missing, "description": f"{missing} {cname}(s) placed in fallback {zone_type} for region {rg}" } return general_fallback