| |
| """ |
| NavMesh解析工具 |
| |
| 用于解析HM3D数据集的NavMesh文件,提取可导航区域(islands) |
| """ |
|
|
| import os |
| import struct |
| import numpy as np |
| from typing import List, Dict, Optional, Tuple |
| from collections import defaultdict |
|
|
|
|
| def parse_navmesh_binary(navmesh_path: str) -> Optional[Dict]: |
| """ |
| 解析Recast Navigation格式的NavMesh二进制文件 |
| |
| NavMesh格式(基于分析): |
| - 文件头:'TESM' (4字节) |
| - 版本/标志:uint32, uint32 |
| - 边界框:float32x3 (min), float32x3 (max) |
| - 顶点数据、三角形数据、区域数据等 |
| |
| Args: |
| navmesh_path: NavMesh文件路径 |
| |
| Returns: |
| navmesh_data: 包含顶点、三角形、区域信息的字典,如果解析失败返回None |
| """ |
| if not os.path.exists(navmesh_path): |
| return None |
| |
| try: |
| with open(navmesh_path, 'rb') as f: |
| |
| header = f.read(4) |
| if header != b'TESM': |
| print(f"[WARN] NavMesh文件头不匹配: {header}") |
| return None |
| |
| |
| version = struct.unpack('<I', f.read(4))[0] |
| flags = struct.unpack('<I', f.read(4))[0] |
| |
| |
| bbox_min = struct.unpack('<fff', f.read(12)) |
| bbox_max = struct.unpack('<fff', f.read(12)) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| return { |
| 'header': header, |
| 'version': version, |
| 'flags': flags, |
| 'bbox_min': np.array(bbox_min), |
| 'bbox_max': np.array(bbox_max), |
| 'raw_data': f.read() |
| } |
| |
| except Exception as e: |
| print(f"[ERROR] 解析NavMesh文件失败: {e}") |
| return None |
|
|
|
|
| def extract_spaces_from_semantic_glb(semantic_glb_path: str, |
| room_to_objects: Dict[int, List[int]] = None, |
| use_clustering: bool = True, |
| cluster_eps: float = 0.8, |
| cluster_min_samples: int = 2) -> List[Dict]: |
| """ |
| 从semantic.glb中提取所有空间,使用空间聚类方法 |
| |
| 策略: |
| 1. 提取所有对象的中心点 |
| 2. 使用DBSCAN聚类识别空间 |
| 3. 如果提供了语义标注,可以结合使用 |
| |
| Args: |
| semantic_glb_path: semantic.glb文件路径 |
| room_to_objects: 房间到对象的映射 {room_id: [object_ids]}(可选) |
| use_clustering: 是否使用空间聚类(默认True) |
| cluster_eps: DBSCAN聚类半径(米),默认2.0 |
| cluster_min_samples: DBSCAN最小样本数,默认3 |
| |
| Returns: |
| spaces: 空间列表,每个元素包含空间信息 |
| """ |
| spaces = [] |
| |
| if not os.path.exists(semantic_glb_path): |
| return spaces |
| |
| try: |
| import trimesh |
| |
| scene = trimesh.load(semantic_glb_path, process=False) |
| if not isinstance(scene, trimesh.Scene): |
| return spaces |
| |
| |
| obj_info_list = [] |
| obj_index_to_info = {} |
| |
| for idx, (name, geom) in enumerate(scene.geometry.items(), 1): |
| if isinstance(geom, trimesh.Trimesh): |
| |
| transform = np.eye(4) |
| for node_name in scene.graph.nodes_geometry: |
| if scene.graph[node_name][1] == name: |
| transform = scene.graph[node_name][0] |
| break |
| |
| |
| local_center = geom.bounds.mean(axis=0) |
| world_center = np.dot(transform[:3, :3], local_center) + transform[:3, 3] |
| |
| |
| world_vertices = np.dot(geom.vertices, transform[:3, :3].T) + transform[:3, 3] |
| |
| obj_info = { |
| 'index': idx, |
| 'name': name, |
| 'geom': geom, |
| 'center': world_center, |
| 'vertices': world_vertices, |
| 'transform': transform |
| } |
| obj_info_list.append(obj_info) |
| obj_index_to_info[idx] = obj_info |
| |
| if not obj_info_list: |
| return spaces |
| |
| |
| if use_clustering: |
| try: |
| from sklearn.cluster import DBSCAN |
| |
| |
| centers = np.array([info['center'] for info in obj_info_list]) |
| |
| |
| |
| |
| |
| use_3d = len(obj_info_list) > 50 |
| |
| if use_3d: |
| |
| centers_3d = centers.copy() |
| centers_3d[:, 2] = centers_3d[:, 2] * 0.5 |
| clustering = DBSCAN(eps=cluster_eps, min_samples=cluster_min_samples) |
| cluster_labels = clustering.fit_predict(centers_3d) |
| else: |
| |
| centers_2d = centers[:, :2] |
| clustering = DBSCAN(eps=cluster_eps, min_samples=cluster_min_samples) |
| cluster_labels = clustering.fit_predict(centers_2d) |
| |
| |
| unique_labels = set(cluster_labels) |
| if -1 in unique_labels: |
| unique_labels.remove(-1) |
| |
| print(f"[INFO] DBSCAN聚类识别到 {len(unique_labels)} 个空间(eps={cluster_eps}, min_samples={cluster_min_samples})") |
| |
| |
| for cluster_id in sorted(unique_labels): |
| |
| cluster_objects = [obj_info_list[i] for i in range(len(obj_info_list)) |
| if cluster_labels[i] == cluster_id] |
| |
| if not cluster_objects: |
| continue |
| |
| |
| all_vertices = np.vstack([obj['vertices'] for obj in cluster_objects]) |
| |
| bounds_min = all_vertices.min(axis=0) |
| bounds_max = all_vertices.max(axis=0) |
| center = (bounds_min + bounds_max) / 2 |
| size = bounds_max - bounds_min |
| |
| |
| |
| if np.all(size > 0.2): |
| spaces.append({ |
| "space_id": cluster_id, |
| "bounds_min": bounds_min, |
| "bounds_max": bounds_max, |
| "center": center, |
| "object_count": len(cluster_objects), |
| "source": "spatial_clustering" |
| }) |
| |
| except ImportError: |
| print("[WARN] sklearn未安装,无法使用空间聚类,回退到语义标注方法") |
| use_clustering = False |
| |
| |
| if not use_clustering and room_to_objects: |
| |
| for room_id in sorted(room_to_objects.keys()): |
| obj_ids = room_to_objects[room_id] |
| |
| valid_obj_ids = [obj_id for obj_id in obj_ids if obj_id in obj_index_to_info] |
| |
| if valid_obj_ids: |
| |
| all_vertices = [] |
| for obj_id in valid_obj_ids: |
| info = obj_index_to_info[obj_id] |
| all_vertices.append(info['vertices']) |
| |
| if all_vertices: |
| all_vertices = np.vstack(all_vertices) |
| bounds_min = all_vertices.min(axis=0) |
| bounds_max = all_vertices.max(axis=0) |
| center = (bounds_min + bounds_max) / 2 |
| size = bounds_max - bounds_min |
| |
| |
| if np.all(size > 0.1): |
| spaces.append({ |
| "space_id": room_id, |
| "bounds_min": bounds_min, |
| "bounds_max": bounds_max, |
| "center": center, |
| "object_count": len(valid_obj_ids), |
| "total_objects_in_annotation": len(obj_ids), |
| "source": "semantic_glb" |
| }) |
| |
| except Exception as e: |
| print(f"[WARN] 从semantic.glb提取空间失败: {e}") |
| import traceback |
| traceback.print_exc() |
| |
| return spaces |
|
|
|
|
| def extract_spaces_from_glb(glb_path: str, |
| room_to_objects: Dict[int, List[int]] = None, |
| use_clustering: bool = True, |
| cluster_eps: float = 0.8, |
| cluster_min_samples: int = 2) -> List[Dict]: |
| """ |
| 从原始GLB文件中提取所有空间,使用空间聚类方法 |
| |
| 当没有semantic.glb文件时,使用此方法作为回退方案 |
| |
| Args: |
| glb_path: GLB文件路径 |
| room_to_objects: 房间到对象的映射 {room_id: [object_ids]}(可选) |
| use_clustering: 是否使用空间聚类(默认True) |
| cluster_eps: DBSCAN聚类半径(米),默认0.8 |
| cluster_min_samples: DBSCAN最小样本数,默认2 |
| |
| Returns: |
| spaces: 空间列表,每个元素包含空间信息 |
| """ |
| spaces = [] |
| |
| if not os.path.exists(glb_path): |
| return spaces |
| |
| try: |
| import trimesh |
| |
| scene = trimesh.load(glb_path, process=False) |
| if not isinstance(scene, trimesh.Scene): |
| return spaces |
| |
| |
| obj_info_list = [] |
| obj_index_to_info = {} |
| |
| for idx, (name, geom) in enumerate(scene.geometry.items(), 1): |
| if isinstance(geom, trimesh.Trimesh): |
| |
| transform = np.eye(4) |
| for node_name in scene.graph.nodes_geometry: |
| if scene.graph[node_name][1] == name: |
| transform = scene.graph[node_name][0] |
| break |
| |
| |
| local_center = geom.bounds.mean(axis=0) |
| world_center = np.dot(transform[:3, :3], local_center) + transform[:3, 3] |
| |
| |
| world_vertices = np.dot(geom.vertices, transform[:3, :3].T) + transform[:3, 3] |
| |
| obj_info = { |
| 'index': idx, |
| 'name': name, |
| 'geom': geom, |
| 'center': world_center, |
| 'vertices': world_vertices, |
| 'transform': transform |
| } |
| obj_info_list.append(obj_info) |
| obj_index_to_info[idx] = obj_info |
| |
| if not obj_info_list: |
| return spaces |
| |
| |
| if use_clustering: |
| try: |
| from sklearn.cluster import DBSCAN |
| |
| |
| centers = np.array([info['center'] for info in obj_info_list]) |
| |
| |
| |
| use_3d = len(obj_info_list) > 50 |
| |
| if use_3d: |
| |
| centers_3d = centers.copy() |
| centers_3d[:, 2] = centers_3d[:, 2] * 0.5 |
| clustering = DBSCAN(eps=cluster_eps, min_samples=cluster_min_samples) |
| cluster_labels = clustering.fit_predict(centers_3d) |
| else: |
| |
| centers_2d = centers[:, :2] |
| clustering = DBSCAN(eps=cluster_eps, min_samples=cluster_min_samples) |
| cluster_labels = clustering.fit_predict(centers_2d) |
| |
| |
| unique_labels = set(cluster_labels) |
| if -1 in unique_labels: |
| unique_labels.remove(-1) |
| |
| print(f"[INFO] DBSCAN聚类识别到 {len(unique_labels)} 个空间(eps={cluster_eps}, min_samples={cluster_min_samples})") |
| |
| |
| for cluster_id in sorted(unique_labels): |
| |
| cluster_objects = [obj_info_list[i] for i in range(len(obj_info_list)) |
| if cluster_labels[i] == cluster_id] |
| |
| if not cluster_objects: |
| continue |
| |
| |
| all_vertices = np.vstack([obj['vertices'] for obj in cluster_objects]) |
| |
| bounds_min = all_vertices.min(axis=0) |
| bounds_max = all_vertices.max(axis=0) |
| center = (bounds_min + bounds_max) / 2 |
| size = bounds_max - bounds_min |
| |
| |
| if np.all(size > 0.2): |
| spaces.append({ |
| "space_id": cluster_id, |
| "bounds_min": bounds_min, |
| "bounds_max": bounds_max, |
| "center": center, |
| "object_count": len(cluster_objects), |
| "source": "glb_clustering" |
| }) |
| |
| except ImportError: |
| print("[WARN] sklearn未安装,无法使用空间聚类") |
| except Exception as e: |
| print(f"[WARN] 空间聚类失败: {e}") |
| |
| except Exception as e: |
| print(f"[WARN] 从GLB提取空间失败: {e}") |
| import traceback |
| traceback.print_exc() |
| |
| return spaces |
|
|
|
|
| def extract_navmesh_islands_from_semantic(navmesh_path: str, mesh_path: str, |
| room_to_objects: Dict[int, List[int]]) -> List[Dict]: |
| """ |
| 结合语义信息从NavMesh提取空间区域 |
| |
| 现在改为使用semantic.glb直接计算,不依赖NavMesh的island分割 |
| |
| Args: |
| navmesh_path: NavMesh文件路径(保留参数以保持兼容性) |
| mesh_path: GLB文件路径 |
| room_to_objects: 房间到对象的映射 {room_id: [object_ids]} |
| |
| Returns: |
| spaces: 空间列表 |
| """ |
| |
| from semantic_utils import find_semantic_file |
| from pathlib import Path |
| import os |
| |
| |
| mesh_path_obj = Path(mesh_path) |
| mesh_dir = mesh_path_obj.parent |
| mesh_stem = mesh_path_obj.stem |
| |
| semantic_glb_path = None |
| |
| |
| semantic_glb_candidate = mesh_dir / f"{mesh_stem}.semantic.glb" |
| if semantic_glb_candidate.exists(): |
| semantic_glb_path = str(semantic_glb_candidate) |
| |
| |
| if not semantic_glb_path and 'glb-v0.2' in str(mesh_dir): |
| semantic_dir = str(mesh_dir).replace('glb-v0.2', 'semantic-annots-v0.2') |
| semantic_dir_obj = Path(semantic_dir) |
| |
| if semantic_dir_obj.exists(): |
| semantic_glb_candidate = semantic_dir_obj / f"{mesh_stem}.semantic.glb" |
| if semantic_glb_candidate.exists(): |
| semantic_glb_path = str(semantic_glb_candidate) |
| |
| |
| if not semantic_glb_path: |
| dataset_root = mesh_dir.parent.parent if 'glb-v0.2' in str(mesh_dir) else mesh_dir.parent |
| semantic_annots_dir = dataset_root / "hm3d-example-semantic-annots-v0.2" |
| |
| if semantic_annots_dir.exists(): |
| |
| for scene_dir in semantic_annots_dir.iterdir(): |
| if scene_dir.is_dir(): |
| semantic_glb_candidate = scene_dir / f"{mesh_stem}.semantic.glb" |
| if semantic_glb_candidate.exists(): |
| semantic_glb_path = str(semantic_glb_candidate) |
| break |
| |
| if semantic_glb_path and os.path.exists(semantic_glb_path): |
| |
| return extract_spaces_from_semantic_glb(semantic_glb_path, room_to_objects, use_clustering=True) |
| elif mesh_path and os.path.exists(mesh_path): |
| |
| print(f" [INFO] 未找到semantic.glb,尝试从原始GLB文件进行空间聚类...") |
| return extract_spaces_from_glb(mesh_path, room_to_objects, use_clustering=True) |
| else: |
| |
| return [] |
|
|
|
|
| def get_room_bounds_from_mesh(mesh_path: str, object_ids: List[int], |
| scene=None) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]: |
| """ |
| 从mesh中获取指定对象ID列表的边界框 |
| |
| Args: |
| mesh_path: mesh文件路径 |
| object_ids: 对象ID列表 |
| scene: 已加载的场景(可选,避免重复加载) |
| |
| Returns: |
| bounds_min, bounds_max: 边界框的最小和最大坐标 |
| """ |
| try: |
| import trimesh |
| import re |
| |
| if scene is None: |
| scene = trimesh.load(mesh_path, process=False) |
| |
| if not isinstance(scene, trimesh.Scene): |
| return None, None |
| |
| object_ids_set = set(object_ids) |
| all_vertices = [] |
| |
| for name, geom in scene.geometry.items(): |
| if isinstance(geom, trimesh.Trimesh): |
| |
| obj_id = None |
| match = re.search(r'chunk(\d+)', name) |
| if match: |
| chunk_num = int(match.group(1)) |
| obj_id = chunk_num + 1 |
| |
| if obj_id is None or obj_id not in object_ids_set: |
| continue |
| |
| |
| for node_name in scene.graph.nodes_geometry: |
| if scene.graph[node_name][1] == name: |
| transform = scene.graph[node_name][0] |
| vertices = np.dot(geom.vertices, transform[:3, :3].T) + transform[:3, 3] |
| all_vertices.append(vertices) |
| break |
| |
| if all_vertices: |
| all_vertices = np.vstack(all_vertices) |
| bounds_min = all_vertices.min(axis=0) |
| bounds_max = all_vertices.max(axis=0) |
| return bounds_min, bounds_max |
| |
| return None, None |
| |
| except Exception as e: |
| print(f"[WARN] 获取房间边界框失败: {e}") |
| return None, None |
|
|
|
|
| def extract_navmesh_islands_connectivity(navmesh_path: str) -> List[Dict]: |
| """ |
| 通过连通性分析提取NavMesh中的独立区域(islands) |
| |
| 由于直接解析Recast格式复杂,此函数尝试: |
| 1. 解析NavMesh的基本几何信息 |
| 2. 使用连通性分析识别独立区域 |
| 3. 计算每个区域的边界框和中心 |
| |
| Args: |
| navmesh_path: NavMesh文件路径 |
| |
| Returns: |
| islands: 独立区域列表,每个元素包含边界框和中心信息 |
| """ |
| |
| |
| |
| |
| print("[INFO] NavMesh直接解析暂未实现,使用语义信息结合方法") |
| return [] |
|
|
|
|
| def navmesh_to_3d_spaces(navmesh_path: str, mesh_path: str = None, |
| room_to_objects: Dict[int, List[int]] = None, |
| use_clustering: bool = True) -> List[Dict]: |
| """ |
| 将NavMesh区域映射到3D空间 |
| |
| 优先策略: |
| 1. 如果提供了语义信息,使用semantic.glb + 空间聚类识别所有空间 |
| 2. 否则尝试直接解析NavMesh |
| |
| Args: |
| navmesh_path: NavMesh文件路径(保留参数以保持兼容性) |
| mesh_path: GLB文件路径(可选,用于查找semantic.glb) |
| room_to_objects: 房间到对象的映射(可选,用于结合语义信息) |
| use_clustering: 是否使用空间聚类(默认True) |
| |
| Returns: |
| spaces: 空间列表,每个元素包含空间ID、边界框、中心等信息 |
| """ |
| |
| if mesh_path: |
| from pathlib import Path |
| import os |
| |
| |
| mesh_path_obj = Path(mesh_path) |
| mesh_dir = mesh_path_obj.parent |
| mesh_stem = mesh_path_obj.stem |
| |
| semantic_glb_path = None |
| |
| |
| semantic_glb_candidate = mesh_dir / f"{mesh_stem}.semantic.glb" |
| if semantic_glb_candidate.exists(): |
| semantic_glb_path = str(semantic_glb_candidate) |
| |
| |
| if not semantic_glb_path and 'glb-v0.2' in str(mesh_dir): |
| semantic_dir = str(mesh_dir).replace('glb-v0.2', 'semantic-annots-v0.2') |
| semantic_dir_obj = Path(semantic_dir) |
| |
| if semantic_dir_obj.exists(): |
| semantic_glb_candidate = semantic_dir_obj / f"{mesh_stem}.semantic.glb" |
| if semantic_glb_candidate.exists(): |
| semantic_glb_path = str(semantic_glb_candidate) |
| |
| |
| if not semantic_glb_path: |
| dataset_root = mesh_dir.parent.parent if 'glb-v0.2' in str(mesh_dir) else mesh_dir.parent |
| semantic_annots_dir = dataset_root / "hm3d-example-semantic-annots-v0.2" |
| |
| if semantic_annots_dir.exists(): |
| |
| for scene_dir in semantic_annots_dir.iterdir(): |
| if scene_dir.is_dir(): |
| semantic_glb_candidate = scene_dir / f"{mesh_stem}.semantic.glb" |
| if semantic_glb_candidate.exists(): |
| semantic_glb_path = str(semantic_glb_candidate) |
| break |
| |
| if semantic_glb_path and os.path.exists(semantic_glb_path): |
| |
| return extract_spaces_from_semantic_glb(semantic_glb_path, room_to_objects, |
| use_clustering=use_clustering) |
| elif use_clustering and mesh_path and os.path.exists(mesh_path): |
| |
| print(f" [INFO] 未找到semantic.glb,尝试从原始GLB文件进行空间聚类...") |
| return extract_spaces_from_glb(mesh_path, room_to_objects, |
| use_clustering=True) |
| elif room_to_objects: |
| |
| return extract_navmesh_islands_from_semantic(navmesh_path, mesh_path, room_to_objects) |
| |
| |
| return extract_navmesh_islands_connectivity(navmesh_path) |
|
|
|
|
| def find_navmesh_file(mesh_path: str) -> Optional[str]: |
| """ |
| 根据GLB文件路径查找对应的NavMesh文件 |
| |
| 路径模式: |
| - GLB: hm3d-example-glb-v0.2/{scene_id}/{scene_id}.glb |
| - NavMesh: hm3d-example-habitat-v0.2/{scene_id}/{scene_id}.basis.navmesh |
| |
| Args: |
| mesh_path: GLB文件路径 |
| |
| Returns: |
| NavMesh文件路径,如果不存在则返回None |
| """ |
| from pathlib import Path |
| |
| mesh_path_obj = Path(mesh_path) |
| mesh_dir = mesh_path_obj.parent |
| mesh_stem = mesh_path_obj.stem |
| |
| |
| navmesh_candidate = mesh_dir / f"{mesh_stem}.basis.navmesh" |
| if navmesh_candidate.exists(): |
| return str(navmesh_candidate) |
| |
| |
| |
| if 'glb-v0.2' in str(mesh_dir): |
| habitat_dir = str(mesh_dir).replace('glb-v0.2', 'habitat-v0.2') |
| habitat_dir_obj = Path(habitat_dir) |
| |
| if habitat_dir_obj.exists(): |
| navmesh_candidate = habitat_dir_obj / f"{mesh_stem}.basis.navmesh" |
| if navmesh_candidate.exists(): |
| return str(navmesh_candidate) |
| |
| |
| dataset_root = mesh_dir.parent.parent if 'glb-v0.2' in str(mesh_dir) else mesh_dir.parent |
| habitat_dir = dataset_root / "hm3d-example-habitat-v0.2" |
| |
| if habitat_dir.exists(): |
| |
| for scene_dir in habitat_dir.iterdir(): |
| if scene_dir.is_dir(): |
| navmesh_candidate = scene_dir / f"{mesh_stem}.basis.navmesh" |
| if navmesh_candidate.exists(): |
| return str(navmesh_candidate) |
| |
| |
| navmesh_files = list(scene_dir.glob("*.navmesh")) |
| if navmesh_files: |
| |
| return str(navmesh_files[0]) |
| |
| return None |
|
|