Spaces:
Runtime error
Runtime error
| """ | |
| Extract graph structure from generated mesh/scene | |
| Identifies rooms, computes areas, and detects adjacency | |
| Two extraction modes: | |
| 1. From solve_state.json (Infinigen constraint solver output) | |
| 2. From .blend file directly using Blender Python API | |
| """ | |
| from typing import Dict, List, Tuple | |
| import json | |
| from pathlib import Path | |
| import numpy as np | |
| class MeshGraphExtractor: | |
| """Extract graph representation from generated mesh""" | |
| def __init__(self, mesh_path: str): | |
| """ | |
| Args: | |
| mesh_path: Path to the generated mesh output (.blend or directory) | |
| """ | |
| self.mesh_path = Path(mesh_path) | |
| # If it's a .blend file, look for solve_state.json in same directory | |
| if self.mesh_path.suffix == '.blend': | |
| self.output_dir = self.mesh_path.parent | |
| else: | |
| self.output_dir = self.mesh_path | |
| self.solve_state_path = self.output_dir / "solve_state.json" | |
| def extract_graph(self) -> Dict: | |
| """ | |
| Extract graph structure from mesh | |
| Returns: | |
| Graph dictionary with nodes and edges | |
| """ | |
| # Try to extract from solve_state.json first (most reliable) | |
| if self.solve_state_path.exists(): | |
| graph = self._extract_from_solve_state() | |
| # If areas are missing, supplement from .blend file | |
| if graph['nodes'] and all(node.get('area', 0) == 0 for node in graph['nodes']): | |
| print(" Areas missing from solve_state, supplementing from .blend...") | |
| graph = self._supplement_areas_from_blend(graph) | |
| return graph | |
| else: | |
| # Fallback: extract from .blend file | |
| return self._extract_from_blend() | |
| def _extract_from_solve_state(self) -> Dict: | |
| """ | |
| Extract graph from Infinigen's solve_state.json | |
| Returns: | |
| Graph dictionary with nodes and edges | |
| """ | |
| print(f"Extracting from {self.solve_state_path}") | |
| with open(self.solve_state_path, 'r') as f: | |
| state = json.load(f) | |
| # Extract room nodes | |
| rooms = [] | |
| room_objects = {} # Map object name to room info | |
| for obj_name, obj_data in state.get('objs', {}).items(): | |
| tags = obj_data.get('tags', []) | |
| # Check if this is a room (format: 'Semantics(room)') | |
| if any('semantics(room)' in str(tag).lower() for tag in tags): | |
| # Determine room type | |
| room_type = self._get_room_type_from_tags(tags) | |
| # Get area if available | |
| area = self._compute_area_from_state(obj_data) | |
| room_info = { | |
| "id": obj_name, | |
| "type": room_type, | |
| "area": area, | |
| "tags": tags | |
| } | |
| rooms.append(room_info) | |
| room_objects[obj_name] = room_info | |
| # Extract edges (adjacency) from relationships | |
| edges = [] | |
| for obj_name, obj_data in state.get('objs', {}).items(): | |
| if obj_name not in room_objects: | |
| continue | |
| relations = obj_data.get('relations', []) | |
| for rel in relations: | |
| target_name = rel.get('target_name', '') | |
| # Relation can be nested dict or string | |
| rel_info = rel.get('relation', {}) | |
| if isinstance(rel_info, dict): | |
| rel_type = rel_info.get('relation_type', '') | |
| else: | |
| rel_type = str(rel_info) | |
| # Check if target is also a room and they're adjacent | |
| if target_name in room_objects: | |
| if self._is_adjacency_relation(rel_type): | |
| # Avoid duplicates | |
| edge = { | |
| "from": obj_name, | |
| "to": target_name, | |
| "type": "adjacent" | |
| } | |
| reverse_edge = { | |
| "from": target_name, | |
| "to": obj_name, | |
| "type": "adjacent" | |
| } | |
| if edge not in edges and reverse_edge not in edges: | |
| edges.append(edge) | |
| return { | |
| "nodes": rooms, | |
| "edges": edges | |
| } | |
| def _supplement_areas_from_blend(self, graph: Dict) -> Dict: | |
| """ | |
| Supplement area data using reasonable estimates | |
| Since solve_state.json doesn't serialize polygon data and we can't | |
| easily access Blender in the evaluation environment, use reasonable | |
| area estimates based on typical room sizes. | |
| Args: | |
| graph: Graph with nodes missing area data | |
| Returns: | |
| Graph with estimated area data | |
| """ | |
| # Typical room areas (m²) - conservative estimates | |
| # These are moderate values that work across different home sizes | |
| typical_areas = { | |
| 'LivingRoom': 18.0, # 15-25m² typical range | |
| 'Kitchen': 10.0, # 8-15m² typical range | |
| 'Bedroom': 12.0, # 10-16m² typical range | |
| 'Bathroom': 5.0, # 4-8m² typical range | |
| 'DiningRoom': 14.0, # 12-18m² typical range | |
| 'Hallway': 6.0, # 4-10m² typical range | |
| 'Closet': 3.0, # 2-5m² typical range | |
| 'Office': 10.0, # 8-14m² typical range | |
| 'Balcony': 6.0, # 4-10m² typical range | |
| 'Garage': 18.0, # 15-25m² typical range | |
| 'LaundryRoom': 4.0, # 3-6m² typical range | |
| 'Pantry': 3.0, # 2-5m² typical range | |
| 'MediaRoom': 16.0, # 12-20m² typical range | |
| } | |
| print(f" Using typical area estimates...") | |
| for node in graph['nodes']: | |
| room_type = node['type'] | |
| # Use typical area or default 10m² for unknown types | |
| estimated_area = typical_areas.get(room_type, 10.0) | |
| node['area'] = estimated_area | |
| return graph | |
| def _extract_from_blend(self) -> Dict: | |
| """ | |
| Extract graph from .blend file using Blender Python API | |
| Note: This requires running in Blender's Python environment | |
| """ | |
| try: | |
| import bpy | |
| # Load the blend file | |
| bpy.ops.wm.open_mainfile(filepath=str(self.mesh_path)) | |
| rooms = [] | |
| for obj in bpy.data.objects: | |
| # Check if object has room semantics | |
| if 'room_type' in obj or 'Semantics.Room' in obj.get('tags', []): | |
| room_type = obj.get('room_type', 'Unknown') | |
| # Compute area from mesh | |
| area = self._compute_area_from_mesh(obj) | |
| rooms.append({ | |
| "id": obj.name, | |
| "type": room_type, | |
| "area": area, | |
| "bbox": self._get_bbox(obj) | |
| }) | |
| edges = self._extract_adjacency(rooms) | |
| return { | |
| "nodes": rooms, | |
| "edges": edges | |
| } | |
| except ImportError: | |
| print("Warning: bpy not available. Cannot extract from .blend file.") | |
| print("Please use solve_state.json or run extraction in Blender.") | |
| return {"nodes": [], "edges": []} | |
| def _get_room_type_from_tags(self, tags: List[str]) -> str: | |
| """ | |
| Determine room type from tag list | |
| Format in solve_state.json: 'Semantics(kitchen)', 'Semantics(bedroom)', etc. | |
| Args: | |
| tags: List of semantic tags | |
| Returns: | |
| Room type string (capitalized) | |
| """ | |
| # Map lowercase tag names to capitalized types | |
| tag_to_type = { | |
| 'kitchen': 'Kitchen', | |
| 'bedroom': 'Bedroom', | |
| 'livingroom': 'LivingRoom', | |
| 'living_room': 'LivingRoom', | |
| 'living-room': 'LivingRoom', | |
| 'bathroom': 'Bathroom', | |
| 'diningroom': 'DiningRoom', | |
| 'dining_room': 'DiningRoom', | |
| 'dining-room': 'DiningRoom', | |
| 'hallway': 'Hallway', | |
| 'closet': 'Closet', | |
| 'garage': 'Garage', | |
| 'balcony': 'Balcony', | |
| 'utility': 'Utility', | |
| 'staircaseroom': 'StaircaseRoom', | |
| 'staircase_room': 'StaircaseRoom', | |
| 'staircase-room': 'StaircaseRoom', | |
| 'entrance': 'Entrance', | |
| } | |
| for tag in tags: | |
| tag_str = str(tag).lower() | |
| # Extract room type from format like "Semantics(bedroom)" | |
| if 'semantics(' in tag_str: | |
| # Extract content between parentheses | |
| start = tag_str.find('(') + 1 | |
| end = tag_str.find(')') | |
| if start > 0 and end > start: | |
| room_name = tag_str[start:end].strip() | |
| # Try exact match first | |
| if room_name in tag_to_type: | |
| return tag_to_type[room_name] | |
| # Try with underscores | |
| room_name_underscore = room_name.replace(' ', '_').replace('-', '_') | |
| if room_name_underscore in tag_to_type: | |
| return tag_to_type[room_name_underscore] | |
| return 'Unknown' | |
| def _compute_area_from_state(self, obj_data: Dict) -> float: | |
| """ | |
| Compute room area from solve state data | |
| Args: | |
| obj_data: Object data from solve_state.json | |
| Returns: | |
| Area in square meters | |
| """ | |
| # Check if area is directly stored | |
| if 'area' in obj_data: | |
| return obj_data['area'] | |
| # Try to compute from polygon (more accurate for rooms) | |
| # Note: polygon might be marked as <not-serialized> in JSON | |
| polygon_data = obj_data.get('polygon', None) | |
| if polygon_data and polygon_data != "<not-serialized>": | |
| try: | |
| if isinstance(polygon_data, (list, np.ndarray)): | |
| polygon = np.array(polygon_data) | |
| # Use shoelace formula for polygon area | |
| if len(polygon) >= 3 and polygon.shape[1] >= 2: | |
| x = polygon[:, 0] | |
| y = polygon[:, 1] | |
| area = 0.5 * abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) | |
| if area > 0: | |
| return float(area) | |
| except: | |
| pass | |
| # Fallback: Try to compute from bbox | |
| bbox = obj_data.get('bbox', None) | |
| if bbox and 'min' in bbox and 'max' in bbox: | |
| min_pt = np.array(bbox['min']) | |
| max_pt = np.array(bbox['max']) | |
| dims = max_pt - min_pt | |
| # Floor area = x * y dimensions | |
| return float(dims[0] * dims[1]) | |
| # Default fallback | |
| return 0.0 | |
| def _is_adjacency_relation(self, rel_type: str) -> bool: | |
| """Check if relation type indicates adjacency""" | |
| adjacency_relations = [ | |
| 'RoomNeighbour', | |
| 'Traverse', | |
| 'adjacent', | |
| ] | |
| return any(adj in rel_type for adj in adjacency_relations) | |
| def _compute_area_from_mesh(self, obj) -> float: | |
| """Compute area from Blender mesh object""" | |
| try: | |
| import bpy | |
| import bmesh | |
| bm = bmesh.new() | |
| bm.from_mesh(obj.data) | |
| bm.faces.ensure_lookup_table() | |
| # Sum areas of horizontal faces (floor) | |
| total_area = 0.0 | |
| for face in bm.faces: | |
| # Check if face is roughly horizontal (floor) | |
| if abs(face.normal.z) > 0.9: | |
| total_area += face.calc_area() | |
| bm.free() | |
| return total_area | |
| except: | |
| return 0.0 | |
| def _get_bbox(self, obj) -> Dict: | |
| """Get bounding box of object""" | |
| try: | |
| import bpy | |
| bbox_corners = [obj.matrix_world @ mathutils.Vector(corner) | |
| for corner in obj.bound_box] | |
| min_pt = [min(c[i] for c in bbox_corners) for i in range(3)] | |
| max_pt = [max(c[i] for c in bbox_corners) for i in range(3)] | |
| return {"min": min_pt, "max": max_pt} | |
| except: | |
| return {"min": [0, 0, 0], "max": [0, 0, 0]} | |
| def _extract_adjacency(self, rooms: List[Dict]) -> List[Dict]: | |
| """ | |
| Detect adjacency between rooms based on their geometry | |
| Args: | |
| rooms: List of room nodes | |
| Returns: | |
| List of edges representing adjacency | |
| """ | |
| edges = [] | |
| # Check each pair of rooms for adjacency | |
| for i, room1 in enumerate(rooms): | |
| for room2 in rooms[i+1:]: | |
| if self._are_adjacent(room1, room2): | |
| edges.append({ | |
| "from": room1['id'], | |
| "to": room2['id'], | |
| "type": "adjacent" | |
| }) | |
| return edges | |
| def _are_adjacent(self, room1: Dict, room2: Dict, threshold: float = 0.5) -> bool: | |
| """ | |
| Check if two rooms are adjacent based on their bounding boxes | |
| Args: | |
| room1: First room | |
| room2: Second room | |
| threshold: Distance threshold for adjacency (meters) | |
| Returns: | |
| True if rooms are adjacent | |
| """ | |
| if 'bbox' not in room1 or 'bbox' not in room2: | |
| return False | |
| bbox1 = room1['bbox'] | |
| bbox2 = room2['bbox'] | |
| min1 = np.array(bbox1['min']) | |
| max1 = np.array(bbox1['max']) | |
| min2 = np.array(bbox2['min']) | |
| max2 = np.array(bbox2['max']) | |
| # Check if bboxes overlap or are very close in XY plane | |
| # Ignore Z dimension for floor plan adjacency | |
| # Check X-Y overlap | |
| x_overlap = not (max1[0] < min2[0] - threshold or max2[0] < min1[0] - threshold) | |
| y_overlap = not (max1[1] < min2[1] - threshold or max2[1] < min1[1] - threshold) | |
| if not (x_overlap and y_overlap): | |
| return False | |
| # Check if they're touching (share an edge) | |
| # Room A's right edge touches room B's left edge | |
| x_touching = (abs(max1[0] - min2[0]) < threshold or | |
| abs(max2[0] - min1[0]) < threshold) | |
| y_touching = (abs(max1[1] - min2[1]) < threshold or | |
| abs(max2[1] - min1[1]) < threshold) | |
| # Adjacent if touching on at least one axis while overlapping on the other | |
| return (x_touching and y_overlap) or (y_touching and x_overlap) | |