ChangChrisLiu/GNN_Disassembly_WorldModel
收藏资源简介:
--- license: cc-by-4.0 task_categories: - robotics - image-segmentation - graph-ml language: - en tags: - robotics - manipulation - disassembly - constraint-graph - gnn - world-model - sam2 - segmentation - ur5e size_categories: - 1K<n<10K pretty_name: GNN Disassembly World Model Dataset --- # GNN Disassembly World Model Dataset Real robot disassembly episodes with per-view constraint graphs, SAM2 segmentation masks, 256D feature embeddings, 3D positions, and synchronized robot states. **Hardware:** UR5e + Robotiq 2F-85 gripper, OAK-D Pro (side), RealSense D435i (wrist) ## Overview This dataset contains teleoperated demonstrations of a robot disassembling a desktop motherboard. Each episode includes: - **Dual-camera RGB-D video** at 30Hz (side + wrist views, 1280×720) - **13D robot state** per frame (6 joints + 6 TCP pose + 1 gripper) - **Robot actions** (frame-to-frame state deltas) - **Per-view constraint graphs** with directed edges and per-frame states - **Binary segmentation masks** per component per frame - **256D SAM2 feature embeddings** per component per frame - **3D position centroids** (depth backprojection, meters) ## Dataset Structure ``` data/disassembly/desktop/ ├── session_XXXX_YYYYYY/ # Timestamped sessions │ ├── session_metadata.json │ ├── episode_00/ # One episode = one component removal │ │ ├── metadata.json # goal_component, component_counts │ │ ├── robot_states.npy # (T, 13) float32 │ │ ├── robot_actions.npy # (T-1, 13) float32 deltas │ │ ├── timestamps.npy # (T, 3) float64 │ │ ├── side/ │ │ │ ├── rgb/frame_XXXXXX.png # 1280x720 RGB (side camera) │ │ │ └── depth/frame_XXXXXX.npy # 1280x720 uint16 (mm) │ │ ├── wrist/ │ │ │ ├── rgb/frame_XXXXXX.png # 1280x720 RGB (wrist camera) │ │ │ └── depth/frame_XXXXXX.npy │ │ └── annotations/ # Only for labeled episodes │ │ ├── side_graph.json # Side view constraint graph │ │ ├── wrist_graph.json # Wrist view constraint graph │ │ ├── side_masks/frame_XXXXXX.npz # {component_id: (H,W) uint8} │ │ ├── wrist_masks/frame_XXXXXX.npz │ │ ├── side_embeddings/frame_XXXXXX.npz # {component_id: (256,) float32} │ │ ├── wrist_embeddings/frame_XXXXXX.npz │ │ ├── side_centroids/frame_XXXXXX.json # {component_id: [x,y,z]} meters │ │ ├── wrist_centroids/frame_XXXXXX.json │ │ └── dataset_card.json │ └── episode_01/ ... └── session_.../ ... ``` ## Component Types (Type Vocab) 8 standard component types used throughout the dataset: | Index | Type | Color | Description | |-------|------|-------|-------------| | 0 | `cpu_fan` | #FF6B6B | CPU cooling fan | | 1 | `cpu_bracket` | #4ECDC4 | CPU retention bracket | | 2 | `cpu` | #45B7D1 | CPU processor | | 3 | `ram_clip` | #96CEB4 | RAM retention clip | | 4 | `ram` | #FFEAA7 | RAM stick | | 5 | `connector` | #DDA0DD | Cable/connector | | 6 | `graphic_card` | #FF8C42 | GPU card | | 7 | `motherboard` | #8B5CF6 | Main board | **Dynamic instances:** Components of types `ram`, `ram_clip`, `connector` can have multiple instances (e.g., `ram_1`, `ram_2`). They share the same type one-hot encoding — instances are distinguished by their 256D SAM2 embedding and 3D position. **Occluded components:** `cpu_bracket` and `cpu` are typically hidden under `cpu_fan` at the start of each episode and become visible mid-episode. This is tracked in `frame_states.visibility` using delta encoding. ## Constraint Graph Semantics Edges are **directed prerequisite constraints**: `A -> B` means "A blocks the removal of B" (A must be released before B can be removed). ### Auto-edge rules (physical knowledge) ``` cpu_fan -> cpu_bracket (fan covers bracket) cpu_fan -> motherboard (fan attached to board) cpu_bracket -> cpu (bracket holds CPU) cpu_bracket -> motherboard (bracket bolted to board) cpu -> motherboard (CPU in socket) ram_N -> motherboard (RAM in slot) ram_clip_N -> motherboard (clip attached to board) connector_N -> motherboard (connector plugged in) graphic_card -> motherboard (GPU in PCIe slot) ``` Manual edges (not auto-generated): `ram_clip_N -> ram_M` — users manually pair each clip with its matching RAM stick. ### Edge states - **Locked** (`true`, value=1): constraint active, component cannot be removed - **Unlocked** (`false`, value=0): constraint released, component is free - **Monotonic:** once unlocked during an episode, stays unlocked ### Delta-encoded frame states Frame states are stored as deltas — only frames where state changes are recorded. To resolve state at frame N, accumulate deltas from frame 0 through N: ```python def resolve_frame_state(graph_json, frame_idx): constraints = {} visibility = {} for c in graph_json["components"]: visibility[c["id"]] = True # default visible for e in graph_json["edges"]: constraints[f"{e['src']}->{e['dst']}"] = True # default locked frame_states = graph_json.get("frame_states", {}) for f in sorted([int(k) for k in frame_states]): if f > frame_idx: break fs = frame_states[str(f)] constraints.update(fs.get("constraints", {})) visibility.update(fs.get("visibility", {})) return constraints, visibility ``` ## Graph JSON Structure ```json { "view": "side", "episode_id": "episode_00", "goal_component": "cpu_fan", "components": [ {"id": "cpu_fan", "type": "cpu_fan", "color": "#FF6B6B"}, {"id": "ram_1", "type": "ram", "color": "#FFEAA7"} ], "edges": [ {"src": "cpu_fan", "dst": "cpu_bracket", "directed": true}, {"src": "ram_clip_1", "dst": "ram_1", "directed": true} ], "frame_states": { "0": { "constraints": {"cpu_fan->cpu_bracket": true}, "visibility": {"cpu_fan": true, "cpu_bracket": false, "cpu": false} }, "152": { "constraints": {"cpu_fan->cpu_bracket": false}, "visibility": {"cpu_fan": false, "cpu_bracket": true, "cpu": true} } }, "node_positions": {"cpu_fan": [120, 80]}, "embedding_dim": 256, "feature_extractor": "sam2.1_hiera_base_plus", "type_vocab": ["cpu_fan", "cpu_bracket", "cpu", "ram_clip", "ram", "connector", "graphic_card", "motherboard"] } ``` ## Node Features for GNN Training Each node has a 268D feature vector (for K=8 types): | Feature | Dim | Source | |---------|-----|--------| | SAM2 embedding | 256 | Masked average pool of `sam2.1_hiera_b+` encoder features | | 3D position | 3 | Depth backprojection, averaged over all valid mask pixels (meters, camera frame) | | Component type one-hot | 8 | Index by `type_vocab` — multiple instances share the same one-hot | | Visibility | 1 | Binary flag for this camera at this frame | | **Total** | **268** | | **Robot state** (13D) is stored separately in `robot_states.npy` and can be concatenated at training time to form Graph B features (281D per node). **3D position handling:** When the wrist camera is too close to the surface, depth becomes invalid. In those cases, the centroid entry is missing for that component. Handle this in your data loader (e.g., interpolate from previous frame or set zeros with a `depth_valid` flag). ## Converting to PyG (PyTorch Geometric) **IMPORTANT:** The labeling tool stores only sparse physical constraint edges. For GNN training, expand to a **fully connected graph** so message passing works across all node pairs. Edge features encode whether a constraint exists: | has_constraint | is_locked | Meaning | |---|---|---| | 1 | 1 | Physical constraint exists, locked | | 1 | 0 | Physical constraint exists, released | | 0 | 0 | No physical constraint (message passing only) | ### Example: Load a frame into PyG ```python import json import numpy as np import torch from pathlib import Path from torch_geometric.data import Data def resolve_frame_state(graph_json, frame_idx): """Resolve delta-encoded constraints and visibility at a frame.""" constraints = {} visibility = {} for c in graph_json["components"]: visibility[c["id"]] = True for e in graph_json["edges"]: constraints[f"{e['src']}->{e['dst']}"] = True fs_dict = graph_json.get("frame_states", {}) for f in sorted([int(k) for k in fs_dict]): if f > frame_idx: break fs = fs_dict[str(f)] constraints.update(fs.get("constraints", {})) visibility.update(fs.get("visibility", {})) return constraints, visibility def load_pyg_frame(episode_dir: Path, view: str, frame_idx: int) -> Data: """Load one frame of a view's graph as a fully connected PyG Data object.""" anno_dir = episode_dir / "annotations" # Load graph JSON with open(anno_dir / f"{view}_graph.json") as f: graph = json.load(f) nodes = graph["components"] type_vocab = graph["type_vocab"] N = len(nodes) # Load per-frame data masks_npz = np.load(anno_dir / f"{view}_masks" / f"frame_{frame_idx:06d}.npz") embeddings_npz = np.load(anno_dir / f"{view}_embeddings" / f"frame_{frame_idx:06d}.npz") with open(anno_dir / f"{view}_centroids" / f"frame_{frame_idx:06d}.json") as f: centroids = json.load(f) # Resolve delta-encoded state constraints, visibility = resolve_frame_state(graph, frame_idx) # Build node features: [256D SAM2 embedding, 3D pos, 8D type one-hot, 1D visibility] x_list = [] for node in nodes: cid = node["id"] emb = embeddings_npz[cid] if cid in embeddings_npz.files else np.zeros(256, dtype=np.float32) pos = centroids.get(cid, [0.0, 0.0, 0.0]) type_oh = [1.0 if t == node["type"] else 0.0 for t in type_vocab] vis = [1.0 if visibility.get(cid, True) else 0.0] x_list.append(list(emb) + list(pos) + type_oh + vis) # Build FULLY CONNECTED edge index + 2D edge features constraint_set = {(e["src"], e["dst"]) for e in graph["edges"]} src_idx, dst_idx, edge_attr = [], [], [] for i in range(N): for j in range(N): if i == j: continue # no self-loops src_id = nodes[i]["id"] dst_id = nodes[j]["id"] src_idx.append(i) dst_idx.append(j) if (src_id, dst_id) in constraint_set: is_locked = constraints.get(f"{src_id}->{dst_id}", True) edge_attr.append([1.0, 1.0 if is_locked else 0.0]) else: edge_attr.append([0.0, 0.0]) # message passing only return Data( x=torch.tensor(x_list, dtype=torch.float32), # (N, 268) edge_index=torch.tensor([src_idx, dst_idx], dtype=torch.long), # (2, N*(N-1)) edge_attr=torch.tensor(edge_attr, dtype=torch.float32), # (N*(N-1), 2) num_nodes=N, ) def load_episode(episode_dir: Path, view: str = "side"): """Generator: yield PyG Data objects for each annotated frame.""" anno_dir = episode_dir / "annotations" mask_dir = anno_dir / f"{view}_masks" for npz_path in sorted(mask_dir.glob("frame_*.npz")): frame_idx = int(npz_path.stem.split("_")[1]) yield frame_idx, load_pyg_frame(episode_dir, view, frame_idx) # Usage from pathlib import Path episode = Path("data/disassembly/desktop/session_0408_164005/episode_00") for frame_idx, data in load_episode(episode, view="side"): print(f"Frame {frame_idx}: {data.num_nodes} nodes, {data.num_edges} edges") # data.x shape: (N, 268) # data.edge_index shape: (2, N*(N-1)) # data.edge_attr shape: (N*(N-1), 2) ``` ### Adding Robot State (Graph B) To use robot state as additional node features, broadcast the 13D state to all nodes: ```python robot_states = np.load(episode_dir / "robot_states.npy") # (T, 13) # At frame t, concatenate to every node: robot_at_t = torch.tensor(robot_states[frame_idx], dtype=torch.float32) # (13,) robot_broadcast = robot_at_t.unsqueeze(0).expand(N, -1) # (N, 13) data.x = torch.cat([data.x, robot_broadcast], dim=1) # (N, 281) ``` ## Recording Hardware - **Robot:** UR5e + Robotiq 2F-85 gripper - **Side camera:** Luxonis OAK-D Pro (static, workspace view) - Intrinsics: fx=1033.8, fy=1033.7, cx=632.9, cy=359.9 - **Wrist camera:** Intel RealSense D435i (mounted on robot wrist) - Intrinsics: fx=906.6, fy=905.8, cx=645.9, cy=364.6 - **Recording rate:** 30 Hz - **Image size:** 1280 × 720 - **Depth format:** uint16, millimeters - **Teleoperation:** Thrustmaster SOL-R2 HOSAS controllers ## Annotation Tool Annotations were created with a custom SAM2-based labeling tool: - **Repository:** https://github.com/ChangChrisLiu/gnn-world-model - **Backend:** FastAPI + SAM2 (`sam2.1_hiera_base_plus`) - **Frontend:** Vanilla HTML/JS with 5 interaction modes (BBox, Point, Polygon, Brush, Eraser) - **Features:** Per-view independent graphs, dynamic component instances, interactive graph editor, scroll-to-zoom, undo/redo ## License This dataset is released under **CC BY 4.0**. You are free to use, share, and adapt the data for any purpose (including commercial) as long as you provide attribution. ## Acknowledgements This work was conducted at Texas A&M University for submission to CoRL 2026. Built using: - [Segment Anything Model 2 (SAM2)](https://github.com/facebookresearch/sam2) by Meta AI - [PyTorch Geometric](https://pytorch-geometric.readthedocs.io/) - [Hugging Face Datasets](https://huggingface.co/docs/datasets)




