biptv3 / code /pointcept_framework /pointcept /datasets /preprocessing /scannet /preprocess_scannet.py
| """ | |
| Preprocessing Script for ScanNet 20/200 | |
| Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com) | |
| Please cite our work if the code is helpful to you. | |
| """ | |
| import warnings | |
| warnings.filterwarnings("ignore", category=DeprecationWarning) | |
| import os | |
| import argparse | |
| import glob | |
| import json | |
| import plyfile | |
| import numpy as np | |
| import pandas as pd | |
| import multiprocessing as mp | |
| from concurrent.futures import ProcessPoolExecutor | |
| from itertools import repeat | |
| from pathlib import Path | |
| # Load external constants | |
| from meta_data.scannet200_constants import VALID_CLASS_IDS_200, VALID_CLASS_IDS_20 | |
| CLOUD_FILE_PFIX = "_vh_clean_2" | |
| SEGMENTS_FILE_PFIX = ".0.010000.segs.json" | |
| AGGREGATIONS_FILE_PFIX = ".aggregation.json" | |
| CLASS_IDS200 = VALID_CLASS_IDS_200 | |
| CLASS_IDS20 = VALID_CLASS_IDS_20 | |
| IGNORE_INDEX = -1 | |
| def read_plymesh(filepath): | |
| """Read ply file and return it as numpy array. Returns None if emtpy.""" | |
| with open(filepath, "rb") as f: | |
| plydata = plyfile.PlyData.read(f) | |
| if plydata.elements: | |
| vertices = pd.DataFrame(plydata["vertex"].data).values | |
| faces = np.stack(plydata["face"].data["vertex_indices"], axis=0) | |
| return vertices, faces | |
| # Map the raw category id to the point cloud | |
| def point_indices_from_group(seg_indices, group, labels_pd): | |
| group_segments = np.array(group["segments"]) | |
| label = group["label"] | |
| print(f"[PREPROCESS DEBUG] Processing raw label from .aggregation.json: '{label}'") | |
| # 尝试用 'category' 列进行匹配 | |
| matched_rows = labels_pd[labels_pd["category"] == label] | |
| print(f"[PREPROCESS DEBUG] Found {len(matched_rows)} matching rows in labels file for category '{label}'") | |
| if len(matched_rows) > 0: | |
| label_id20 = int(matched_rows.iloc[0]["nyu40id"]) | |
| label_id200 = int(matched_rows.iloc[0]["id"]) | |
| print(f"[PREPROCESS DEBUG] Successfully mapped to NYU40 ID: {label_id20}, ScanNet200 ID: {label_id200}") | |
| else: | |
| label_id20 = IGNORE_INDEX | |
| label_id200 = IGNORE_INDEX | |
| print(f"[PREPROCESS DEBUG] WARNING: Label '{label}' NOT FOUND in 'category' column!") | |
| # 检查 ID 是否在有效列表中 | |
| is_valid_20 = label_id20 in CLASS_IDS20 | |
| is_valid_200 = label_id200 in CLASS_IDS200 | |
| print(f"[PREPROCESS DEBUG] Is NYU40 ID {label_id20} in VALID_CLASS_IDS_20? {is_valid_20}") | |
| print(f"[PREPROCESS DEBUG] Is ScanNet200 ID {label_id200} in VALID_CLASS_IDS_200? {is_valid_200}") | |
| if is_valid_20: | |
| final_label_20 = CLASS_IDS20.index(label_id20) | |
| print(f"[PREPROCESS DEBUG] Final ScanNet20 Label: {final_label_20}") | |
| else: | |
| final_label_20 = IGNORE_INDEX | |
| print(f"[PREPROCESS DEBUG] Final ScanNet20 Label: IGNORE_INDEX (-1)") | |
| if is_valid_200: | |
| final_label_200 = CLASS_IDS200.index(label_id200) | |
| print(f"[PREPROCESS DEBUG] Final ScanNet200 Label: {final_label_200}") | |
| else: | |
| final_label_200 = IGNORE_INDEX | |
| print(f"[PREPROCESS DEBUG] Final ScanNet200 Label: IGNORE_INDEX (-1)") | |
| # get points, where segment indices (points labelled with segment ids) are in the group segment list | |
| point_idx = np.where(np.isin(seg_indices, group_segments))[0] | |
| print(f"[PREPROCESS DEBUG] Assigned label to {len(point_idx)} points.\n") | |
| return point_idx, final_label_20, final_label_200 | |
| def face_normal(vertex, face): | |
| v01 = vertex[face[:, 1]] - vertex[face[:, 0]] | |
| v02 = vertex[face[:, 2]] - vertex[face[:, 0]] | |
| vec = np.cross(v01, v02) | |
| length = np.sqrt(np.sum(vec**2, axis=1, keepdims=True)) + 1.0e-8 | |
| nf = vec / length | |
| area = length * 0.5 | |
| return nf, area | |
| def vertex_normal(vertex, face): | |
| nf, area = face_normal(vertex, face) | |
| nf = nf * area | |
| nv = np.zeros_like(vertex) | |
| for i in range(face.shape[0]): | |
| nv[face[i]] += nf[i] | |
| length = np.sqrt(np.sum(nv**2, axis=1, keepdims=True)) + 1.0e-8 | |
| nv = nv / length | |
| return nv | |
| def handle_process( | |
| scene_path, output_path, labels_pd, train_scenes, val_scenes, parse_normals=True | |
| ): | |
| scene_id = os.path.basename(scene_path) | |
| mesh_path = os.path.join(scene_path, f"{scene_id}{CLOUD_FILE_PFIX}.ply") | |
| segments_file = os.path.join( | |
| scene_path, f"{scene_id}{CLOUD_FILE_PFIX}{SEGMENTS_FILE_PFIX}" | |
| ) | |
| aggregations_file = os.path.join(scene_path, f"{scene_id}{AGGREGATIONS_FILE_PFIX}") | |
| info_file = os.path.join(scene_path, f"{scene_id}.txt") | |
| # 确定 split | |
| if scene_id in train_scenes: | |
| output_path = os.path.join(output_path, "train", f"{scene_id}") | |
| split_name = "train" | |
| elif scene_id in val_scenes: | |
| output_path = os.path.join(output_path, "val", f"{scene_id}") | |
| split_name = "val" | |
| else: | |
| output_path = os.path.join(output_path, "test", f"{scene_id}") | |
| split_name = "test" | |
| # 🚀 关键修改:跳过缺失的 .ply 文件 | |
| if not os.path.exists(mesh_path): | |
| print(f"⚠️ 跳过缺失场景: {scene_id} (文件不存在: {mesh_path})") | |
| return # 👈 直接返回,不处理这个场景 | |
| print(f"✅ Processing: {scene_id} in {split_name}") | |
| try: | |
| vertices, faces = read_plymesh(mesh_path) | |
| coords = vertices[:, :3] | |
| colors = vertices[:, 3:6] | |
| save_dict = dict( | |
| coord=coords.astype(np.float32), | |
| color=colors.astype(np.uint8), | |
| ) | |
| # Parse Normals | |
| if parse_normals: | |
| save_dict["normal"] = vertex_normal(coords, faces).astype(np.float32) | |
| # Load segments and aggregations for train/val | |
| if split_name != "test": | |
| with open(segments_file) as f: | |
| segments = json.load(f) | |
| seg_indices = np.array(segments["segIndices"]) | |
| with open(aggregations_file) as f: | |
| aggregation = json.load(f) | |
| seg_groups = np.array(aggregation["segGroups"]) | |
| # Generate labels | |
| semantic_gt20 = np.ones((vertices.shape[0]), dtype=np.int16) * IGNORE_INDEX | |
| semantic_gt200 = np.ones((vertices.shape[0]), dtype=np.int16) * IGNORE_INDEX | |
| instance_ids = np.ones((vertices.shape[0]), dtype=np.int16) * IGNORE_INDEX | |
| for group in seg_groups: | |
| point_idx, label_id20, label_id200 = point_indices_from_group( | |
| seg_indices, group, labels_pd | |
| ) | |
| semantic_gt20[point_idx] = label_id20 | |
| semantic_gt200[point_idx] = label_id200 | |
| instance_ids[point_idx] = group["id"] | |
| save_dict["segment20"] = semantic_gt20 | |
| save_dict["segment200"] = semantic_gt200 | |
| save_dict["instance"] = instance_ids | |
| # ========== 修复:只保存包含有效标签的场景 ========== | |
| # 检查该场景是否包含至少一个有效的 ScanNet20 标签 | |
| has_valid_label = np.any(semantic_gt20 != IGNORE_INDEX) if split_name != "test" else True | |
| if has_valid_label: | |
| # Save processed data | |
| os.makedirs(output_path, exist_ok=True) | |
| for key in save_dict.keys(): | |
| np.save(os.path.join(output_path, f"{key}.npy"), save_dict[key]) | |
| print(f"🎉 保存成功: {output_path}") | |
| else: | |
| print(f"⚠️ 跳过场景: {scene_id} - 该场景不包含任何有效的 ScanNet20 类别。") | |
| # =============================================== | |
| except Exception as e: | |
| print(f"❌ 处理失败 {scene_id}: {str(e)}") | |
| return # 即使出错也跳过,不中断整个预处理 | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument( | |
| "--dataset_root", | |
| required=True, | |
| help="Path to the ScanNet dataset containing scene folders", | |
| ) | |
| parser.add_argument( | |
| "--output_root", | |
| required=True, | |
| help="Output path where train/val folders will be located", | |
| ) | |
| parser.add_argument( | |
| "--parse_normals", default=True, type=bool, help="Whether parse point normals" | |
| ) | |
| parser.add_argument( | |
| "--num_workers", | |
| default=mp.cpu_count(), | |
| type=int, | |
| help="Num workers for preprocessing.", | |
| ) | |
| config = parser.parse_args() | |
| meta_root = Path(os.path.dirname(__file__)) / "meta_data" | |
| # Load label map | |
| labels_pd = pd.read_csv( | |
| meta_root / "scannetv2-labels.combined.tsv", | |
| sep="\t", | |
| header=0, | |
| ) | |
| # # Load train/val splits | |
| # with open(meta_root / "scannetv2_train.txt") as train_file: | |
| # train_scenes = train_file.read().splitlines() | |
| # with open(meta_root / "scannetv2_val.txt") as val_file: | |
| # val_scenes = val_file.read().splitlines() | |
| # with open(os.path.join(config.dataset_root, "tasks", "custom_train.txt")) as train_file: | |
| # train_scenes = train_file.read().splitlines() | |
| # with open(os.path.join(config.dataset_root, "tasks", "custom_val.txt")) as val_file: | |
| # val_scenes = val_file.read().splitlines() | |
| # ========== 修复:使用新生成的、干净的分割文件 ========== | |
| with open(os.path.join(config.output_root, "tasks", "clean_train.txt")) as train_file: | |
| train_scenes = train_file.read().splitlines() | |
| with open(os.path.join(config.output_root, "tasks", "clean_val.txt")) as val_file: | |
| val_scenes = val_file.read().splitlines() | |
| # =============================================== | |
| # Create output directories | |
| train_output_dir = os.path.join(config.output_root, "train") | |
| os.makedirs(train_output_dir, exist_ok=True) | |
| val_output_dir = os.path.join(config.output_root, "val") | |
| os.makedirs(val_output_dir, exist_ok=True) | |
| test_output_dir = os.path.join(config.output_root, "test") | |
| os.makedirs(test_output_dir, exist_ok=True) | |
| # Load scene paths | |
| scene_paths = sorted(glob.glob(config.dataset_root + "/scans*/scene*")) | |
| # Preprocess data. | |
| print("Processing scenes...") | |
| pool = ProcessPoolExecutor(max_workers=config.num_workers) | |
| _ = list( | |
| pool.map( | |
| handle_process, | |
| scene_paths, | |
| repeat(config.output_root), | |
| repeat(labels_pd), | |
| repeat(train_scenes), | |
| repeat(val_scenes), | |
| repeat(config.parse_normals), | |
| ) | |
| ) | |