mahdichamseddine commited on
Commit
7ab05dd
·
verified ·
1 Parent(s): c7768cd

Initial release: PT-v3m1 segmentation model (BIMStruct3D pipeline)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +3 -0
  2. README.md +154 -0
  3. configs/model_config.py +114 -0
  4. pointcept/__init__.py +0 -0
  5. pointcept/datasets/__init__.py +9 -0
  6. pointcept/datasets/aeo.py +29 -0
  7. pointcept/datasets/builder.py +15 -0
  8. pointcept/datasets/cap3d.py +245 -0
  9. pointcept/datasets/dataloader.py +205 -0
  10. pointcept/datasets/defaults.py +676 -0
  11. pointcept/datasets/hk.py +94 -0
  12. pointcept/datasets/hm3d.py +45 -0
  13. pointcept/datasets/modelnet.py +156 -0
  14. pointcept/datasets/nuscenes.py +583 -0
  15. pointcept/datasets/partnet.py +79 -0
  16. pointcept/datasets/partnete.py +111 -0
  17. pointcept/datasets/preprocessing/arkitscenes/preprocess_arkitscenes_mesh.py +93 -0
  18. pointcept/datasets/preprocessing/concerto/arkitscenes/preprocess_arkitscenes.py +343 -0
  19. pointcept/datasets/preprocessing/concerto/arkitscenes/preprocess_arkitscenes.sh +42 -0
  20. pointcept/datasets/preprocessing/concerto/arkitscenes/rotation.py +140 -0
  21. pointcept/datasets/preprocessing/concerto/arkitscenes/splits.py +64 -0
  22. pointcept/datasets/preprocessing/concerto/cap3d/preprocess_cap3d.py +261 -0
  23. pointcept/datasets/preprocessing/concerto/cap3d/preprocess_cap3d.sh +35 -0
  24. pointcept/datasets/preprocessing/concerto/cap3d/splits.py +76 -0
  25. pointcept/datasets/preprocessing/concerto/graspnet/preprocess_graspnet_poses.py +56 -0
  26. pointcept/datasets/preprocessing/concerto/graspnet/splits.py +91 -0
  27. pointcept/datasets/preprocessing/concerto/hk/hkremote_download.sh +60 -0
  28. pointcept/datasets/preprocessing/concerto/hk/preprocess_hk.py +110 -0
  29. pointcept/datasets/preprocessing/concerto/hk/preprocess_hk.sh +30 -0
  30. pointcept/datasets/preprocessing/concerto/hk/splits.py +35 -0
  31. pointcept/datasets/preprocessing/concerto/hm3d/__init__.py +0 -0
  32. pointcept/datasets/preprocessing/concerto/hm3d/common/utils.py +113 -0
  33. pointcept/datasets/preprocessing/concerto/hm3d/preprocessed_hm3d.py +617 -0
  34. pointcept/datasets/preprocessing/concerto/hm3d/preprocessed_hm3d.sh +42 -0
  35. pointcept/datasets/preprocessing/concerto/hm3d/splits.py +60 -0
  36. pointcept/datasets/preprocessing/concerto/nuscenes/preprocess_nuscenes_info.py +687 -0
  37. pointcept/datasets/preprocessing/concerto/re10k/preprocess_re10k.py +556 -0
  38. pointcept/datasets/preprocessing/concerto/re10k/splits.py +57 -0
  39. pointcept/datasets/preprocessing/concerto/s3dis/preprocess_s3dis.py +472 -0
  40. pointcept/datasets/preprocessing/concerto/s3dis/preprocess_s3dis.sh +45 -0
  41. pointcept/datasets/preprocessing/concerto/s3dis/splits.py +74 -0
  42. pointcept/datasets/preprocessing/concerto/scannet/preprocess_scannet.py +574 -0
  43. pointcept/datasets/preprocessing/concerto/scannet/preprocess_scannet.sh +42 -0
  44. pointcept/datasets/preprocessing/concerto/scannet/splits.py +62 -0
  45. pointcept/datasets/preprocessing/concerto/scannetpp/preprocess_scannetpp.py +557 -0
  46. pointcept/datasets/preprocessing/concerto/scannetpp/preprocess_scannetpp.sh +42 -0
  47. pointcept/datasets/preprocessing/concerto/scannetpp/splits.py +64 -0
  48. pointcept/datasets/preprocessing/concerto/structured3d/preprocess_structured3d.py +557 -0
  49. pointcept/datasets/preprocessing/concerto/structured3d/preprocess_structured3d.sh +43 -0
  50. pointcept/datasets/preprocessing/concerto/structured3d/splits.py +69 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
README.md ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: pytorch
3
+ tags:
4
+ - point-cloud
5
+ - semantic-segmentation
6
+ - point-transformer-v3
7
+ - lidar
8
+ - 3d
9
+ datasets:
10
+ - RPTU-FGMB/DeKH
11
+ license: cc-by-nc-sa-4.0
12
+ ---
13
+
14
+ # BIMStruct3D-segmentation
15
+
16
+ Point-cloud semantic segmentation model (PT-v3m1) from the
17
+ [BIMStruct3D](#citation) scan-to-BIM pipeline.
18
+
19
+ [![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/) [![arXiv](https://img.shields.io/badge/arXiv-2604.24311-b31b1b.svg)](https://arxiv.org/abs/2604.24311) [![EC3 2026](https://img.shields.io/badge/EC3-2026-blue.svg)](https://ec-3.org/publication/ec32026_197/) [![Dataset](https://img.shields.io/badge/%F0%9F%A4%97%20Dataset-DeKH-ffd21e.svg)](https://huggingface.co/datasets/RPTU-FGMB/DeKH)
20
+
21
+ This folder is a self-contained package: model weights, config, a minimal copy
22
+ of the [Pointcept](https://github.com/Pointcept/Pointcept) codebase (which
23
+ this model was trained with), and one script (`segment_scan.py`) that takes a
24
+ raw `.las`/`.laz`/`.ply` scan and produces a segmented file of the same
25
+ format back out.
26
+
27
+ ## What's in here
28
+
29
+ ```
30
+ configs/model_config.py model architecture + class list
31
+ weights/model_best.pth trained checkpoint
32
+ pointcept/ trimmed copy of the Pointcept codebase (see below)
33
+ segment_scan.py chunk -> infer -> merge inference script
34
+ pyproject.toml / uv.lock environment definition
35
+ ```
36
+
37
+ ## Model
38
+
39
+ - Backbone: **PT-v3m1** (Point Transformer V3), ~46M parameters.
40
+ - Pretrained jointly on Structured3D + ScanNet + S3DIS (Point Prompt
41
+ Training / PPT-v1m1) with a `RandomColorDrop` augmentation (points
42
+ randomly lose their color during training, so the model doesn't over-rely
43
+ on it).
44
+ - Fine-tuned on **CV4AEC** training data (indoor/construction point clouds).
45
+ - 10 output classes: `clutter, floor, ceiling, wall, column, door, window,
46
+ stairs, railing, lights` (see `configs/model_config.py` -> `data.names`).
47
+
48
+ The `pointcept/` directory here is deliberately **not** a full checkout of
49
+ the Pointcept codebase or of the internal repo this model was trained in —
50
+ it's trimmed to only what's needed to build this specific model and run
51
+ inference (backbone + generic dataset/transform machinery), so it doesn't
52
+ pull in unrelated dataset loaders or the CV4AEC-specific dataset class
53
+ (`CV4AECDataset`) at all -- `segment_scan.py` builds tiles as plain
54
+ `pointcept.datasets.DefaultDataset` items instead, so you don't need the
55
+ CV4AEC dataset or its raw data to run this. If you want the full upstream
56
+ codebase instead (e.g. to train further), see the notes at the bottom.
57
+
58
+ ## Setup
59
+
60
+ Requires a CUDA GPU (tested on an RTX 4090) and
61
+ [`uv`](https://docs.astral.sh/uv/):
62
+
63
+ ```bash
64
+ uv sync
65
+ ```
66
+
67
+ This creates `.venv/` with Python 3.12, PyTorch 2.7 (CUDA 12.6), spconv,
68
+ torch-scatter/torch-cluster, and the handful of other packages
69
+ `pointcept/` needs -- pinned in `uv.lock` for a reproducible install.
70
+ `flash-attn` is deliberately **not** installed (it requires a slow
71
+ from-source build); the model falls back to standard attention, which this
72
+ config already reflects (`enable_flash=False`).
73
+
74
+ ## Running it
75
+
76
+ ```bash
77
+ uv run segment_scan.py --input your_scan.las --output your_scan_segmented.las
78
+ ```
79
+
80
+ `--input`/`--output` accept `.las`, `.laz`, or `.ply` (mix and match --
81
+ input and output don't need to be the same format).
82
+
83
+ What it does:
84
+ 1. Reads the point cloud and estimates per-point normals (the model expects
85
+ color + normals as input features).
86
+ 2. Splits it into overlapping ~20m tiles (large scans don't fit the model in
87
+ one shot) -- `--tile-size` / `--overlap` to change this.
88
+ 3. Runs each tile through the model with 10-way test-time augmentation
89
+ (5 scales x flip/no-flip), softmax-averaged.
90
+ 4. Merges tiles back into one point cloud: for points covered by more than
91
+ one tile (the overlap region), takes a majority vote across tiles.
92
+ 5. Writes a file with the original coordinates/colors plus a per-point
93
+ `classification` field (0-9, matching the class list above -- for `.ply`
94
+ this is a custom scalar field, readable in CloudCompare/MeshLab) and a
95
+ companion `<output>_labels.npy`.
96
+
97
+ On a ~6.6M point scan this takes about 8 minutes end-to-end on an RTX 4090
98
+ (most of it is the 10x test-time-augmentation passes per tile). Use
99
+ `--tile-size` to trade off tile count vs. points-per-tile if you need to fit
100
+ tighter GPU memory, or edit `TEST_CFG["aug_transform"]` at the top of
101
+ `segment_scan.py` to drop some of the TTA passes for a faster, slightly
102
+ less accurate run.
103
+
104
+ ## If you want the full upstream Pointcept instead
105
+
106
+ This delivery's `pointcept/` is intentionally minimal. If you need the rest
107
+ (other model architectures, training scripts, the full dataset zoo), clone
108
+ <https://github.com/Pointcept/Pointcept> and drop `configs/model_config.py`
109
+ + `weights/model_best.pth` into it -- the checkpoint loads cleanly against
110
+ current upstream (`strict=True`), with one config-level rename to be aware
111
+ of: newer Pointcept renamed the backbone's `cls_mode` argument to
112
+ `enc_mode` (already reflected in `configs/model_config.py` here).
113
+
114
+ ## License
115
+
116
+ - **Code** (`segment_scan.py`, `pointcept/`, configs): MIT.
117
+ - **Weights** (`weights/model_best.pth`): CC-BY-NC-SA-4.0.
118
+
119
+ ## Citation
120
+
121
+ If you use this model, please cite the paper it was built for:
122
+
123
+ ```bibtex
124
+ @inproceedings{chamseddine2026bimstruct3d,
125
+ author = {Chamseddine, Mahdi and Kaufmann, Fabian and Schellen, Marius and Glock, Christian and Stricker, Didier and Rambach, Jason},
126
+ booktitle = {Proceedings of the European Conference on Computing in Construction (EC3)},
127
+ organization = {European Council on Computing in Construction},
128
+ title = {BIMStruct3D: A Fully Automated Hybrid Learning Scan-to-BIM Pipeline with Integrated Topology Refinement},
129
+ year = {2026},
130
+ doi = {10.35490/EC3.2026}
131
+ }
132
+ ```
133
+
134
+ Please also credit the [CV4AEC Scan-to-BIM Competition 2024](https://cv4aec.github.io/cvpr2024)
135
+ ([challenge repo](https://github.com/GradientSpaces/cv4aec-challenge), Gradient
136
+ Spaces @ Stanford) if you use data or results tied to that benchmark -- this
137
+ is the specific challenge whose data the model was fine-tuned on.
138
+
139
+ ## Related dataset
140
+
141
+ This model was used to produce the semantic segmentation underlying the
142
+ BIMStruct3D pipeline's results on the **DeKH (German Hospital) dataset**:
143
+ real hospital point clouds (four scenes across three buildings) with
144
+ semantic annotations and ground-truth IFC BIM models, released alongside
145
+ the paper above.
146
+
147
+ - Dataset: <https://huggingface.co/datasets/RPTU-FGMB/DeKH>
148
+ - License: CC BY-NC-SA 4.0 (non-commercial)
149
+
150
+ ## Acknowledgement
151
+
152
+ This research was funded by the European Union as part of the projects:
153
+ HumanTech (Grant Agreement 101058236) and ShieldBOT (Grant Agreement
154
+ 101235093).
configs/model_config.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Resolved training/model config for the bundled checkpoint (weights/model_best.pth).
2
+ # Trimmed down from the original training config: the original `data.train` /
3
+ # `data.val` / `data.test` blocks referenced our internal dataset class and
4
+ # training-data paths, which aren't needed to run inference and have been
5
+ # removed. `segment_scan.py` builds the model straight from `model` below and
6
+ # applies its own preprocessing pipeline (see PREPROCESS_TRANSFORM /
7
+ # TEST_TIME_AUGMENTATIONS in that script) instead of going through a dataset
8
+ # class, so nothing here is unused cruft other than the two notes below.
9
+
10
+ weight = "weights/model_best.pth" # relative to this delivery folder's root
11
+ resume = False
12
+ evaluate = True
13
+ test_only = False
14
+ seed = 37743921
15
+
16
+ # save_path is only consulted by the standard Pointcept tools/train.py /
17
+ # tools/test.py entry points (dataset-based train/eval), which this delivery
18
+ # doesn't use. Left here only so this file stays loadable by those scripts if
19
+ # you ever want to wire up your own dataset for standard evaluation.
20
+ save_path = "./output"
21
+
22
+ num_worker = 8
23
+ batch_size = 1
24
+ batch_size_val = None
25
+ batch_size_test = None
26
+ epoch = 3000
27
+ eval_epoch = 100
28
+ sync_bn = False
29
+ enable_amp = True
30
+ empty_cache = False
31
+ empty_cache_per_epoch = False
32
+ find_unused_parameters = False
33
+ mix_prob = 0.8
34
+ param_dicts = [dict(keyword="block", lr=0.0003)]
35
+ hooks = [
36
+ dict(type="CheckpointLoader"),
37
+ dict(type="IterationTimer", warmup_iter=2),
38
+ dict(type="InformationWriter"),
39
+ dict(type="SemSegEvaluator"),
40
+ dict(type="CheckpointSaver", save_freq=None),
41
+ dict(type="PreciseEvaluator", test_last=False),
42
+ ]
43
+ train = dict(type="DefaultTrainer")
44
+ test = dict(type="SemSegTester", verbose=True)
45
+ model = dict(
46
+ type="DefaultSegmentorV2",
47
+ num_classes=10,
48
+ backbone_out_channels=64,
49
+ backbone=dict(
50
+ type="PT-v3m1",
51
+ in_channels=6,
52
+ order=("z", "z-trans", "hilbert", "hilbert-trans"),
53
+ stride=(2, 2, 2, 2),
54
+ enc_depths=(2, 2, 2, 6, 2),
55
+ enc_channels=(32, 64, 128, 256, 512),
56
+ enc_num_head=(2, 4, 8, 16, 32),
57
+ enc_patch_size=(1024, 1024, 1024, 1024, 1024),
58
+ dec_depths=(2, 2, 2, 2),
59
+ dec_channels=(64, 64, 128, 256),
60
+ dec_num_head=(4, 4, 8, 16),
61
+ dec_patch_size=(1024, 1024, 1024, 1024),
62
+ mlp_ratio=4,
63
+ qkv_bias=True,
64
+ qk_scale=None,
65
+ attn_drop=0.0,
66
+ proj_drop=0.0,
67
+ drop_path=0.3,
68
+ shuffle_orders=True,
69
+ pre_norm=True,
70
+ enable_rpe=False,
71
+ enable_flash=False, # flash-attn deliberately excluded from the minimal install; standard attention is used instead
72
+ upcast_attention=False,
73
+ upcast_softmax=False,
74
+ enc_mode=False, # renamed from `cls_mode` in newer upstream Pointcept
75
+ pdnorm_bn=False,
76
+ pdnorm_ln=False,
77
+ pdnorm_decouple=True,
78
+ pdnorm_adaptive=False,
79
+ pdnorm_affine=True,
80
+ pdnorm_conditions=("ScanNet", "S3DIS", "Structured3D"),
81
+ ),
82
+ criteria=[
83
+ dict(type="CrossEntropyLoss", loss_weight=1.0, ignore_index=-1),
84
+ dict(type="LovaszLoss", mode="multiclass", loss_weight=1.0, ignore_index=-1),
85
+ ],
86
+ )
87
+ optimizer = dict(type="AdamW", lr=0.003, weight_decay=0.05)
88
+ scheduler = dict(
89
+ type="OneCycleLR",
90
+ max_lr=[0.003, 0.0003],
91
+ pct_start=0.05,
92
+ anneal_strategy="cos",
93
+ div_factor=10.0,
94
+ final_div_factor=1000.0,
95
+ )
96
+
97
+ # Class scheme the model was trained on. Predicted label indices from
98
+ # segment_scan.py map directly onto this list (no remap table needed).
99
+ data = dict(
100
+ num_classes=10,
101
+ ignore_index=-1,
102
+ names=[
103
+ "clutter",
104
+ "floor",
105
+ "ceiling",
106
+ "wall",
107
+ "column",
108
+ "door",
109
+ "window",
110
+ "stairs",
111
+ "railing",
112
+ "lights",
113
+ ],
114
+ )
pointcept/__init__.py ADDED
File without changes
pointcept/datasets/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Trimmed for inference-only use: segment_scan.py only needs the generic
2
+ # DefaultDataset (directory-of-.npy-assets loader) plus the transform/collate
3
+ # machinery. The named dataset classes below each need their own source
4
+ # data on disk to even be useful, and some pull in extra devkits (nuscenes,
5
+ # waymo, ...) we don't need here.
6
+ from .defaults import DefaultDataset, ConcatDataset
7
+ from .builder import build_dataset
8
+ from .utils import point_collate_fn, collate_fn
9
+ from .transform import *
pointcept/datasets/aeo.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AEO Dataset
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+
10
+ import numpy as np
11
+
12
+ from .defaults import DefaultDataset
13
+ from .builder import DATASETS
14
+
15
+
16
+ @DATASETS.register_module()
17
+ class AEODataset(DefaultDataset):
18
+ def __init__(self, **kwargs):
19
+ super().__init__(**kwargs)
20
+ label_mapping = np.ones(41, dtype=int) * -1
21
+ label_mapping[[0, 1, 3, 4, 13, 16, 19, 21, 22, 28, 29, 34, 36, 37, 38, 39]] = (
22
+ np.arange(16)
23
+ )
24
+ self.label_mapping = label_mapping
25
+
26
+ def get_data(self, idx):
27
+ data_dict = super().get_data(idx)
28
+ data_dict["segment"] = self.label_mapping[data_dict["segment"]]
29
+ return data_dict
pointcept/datasets/builder.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dataset Builder
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ from pointcept.utils.registry import Registry
9
+
10
+ DATASETS = Registry("datasets")
11
+
12
+
13
+ def build_dataset(cfg):
14
+ """Build datasets."""
15
+ return DATASETS.build(cfg)
pointcept/datasets/cap3d.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Objaverse Point Cloud Dataset Loader
3
+
4
+ Load pre-processed .pt files.
5
+ Each .pt file is assumed to be a dictionary of PyTorch tensors.
6
+ """
7
+
8
+ import os
9
+ import torch
10
+ import numpy as np
11
+ import open3d as o3d
12
+ from PIL import Image
13
+ import json
14
+ from collections.abc import Sequence
15
+ import random
16
+
17
+ from .builder import DATASETS
18
+ from .defaults import DefaultDataset, DefaultImagePointDataset
19
+
20
+
21
+ @DATASETS.register_module()
22
+ class Cap3DDataset(DefaultDataset):
23
+ """
24
+ Cap3D Dataset for loading .pt files.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ data_num=None,
30
+ **kwargs,
31
+ ):
32
+ super().__init__(**kwargs)
33
+ if data_num:
34
+ self.data_list = self.data_list[:data_num]
35
+
36
+ @staticmethod
37
+ def get_normals(center, coords):
38
+ Cs = np.repeat(center.reshape((1, -1)), coords.shape[0], axis=0)
39
+ view_dirs = coords - Cs
40
+ view_dirs = view_dirs / np.linalg.norm(view_dirs, axis=-1, keepdims=True)
41
+ pcd = o3d.geometry.PointCloud()
42
+ pcd.points = o3d.utility.Vector3dVector(coords)
43
+ pcd.estimate_normals()
44
+ normals = np.asarray(pcd.normals)
45
+ dot_product = np.sum(normals * view_dirs, axis=-1)
46
+ flip_mask = dot_product < 0
47
+ normals[flip_mask] = -normals[flip_mask]
48
+
49
+ # Normalize normals a nd m
50
+ normals = normals / np.linalg.norm(normals, axis=-1, keepdims=True)
51
+ return normals
52
+
53
+ def get_data(self, idx):
54
+ """
55
+ The core data loading function.
56
+
57
+ Args:
58
+ idx (int): The index of the data sample.
59
+
60
+ Returns:
61
+ dict: A dictionary containing the loaded and processed point cloud data.
62
+ """
63
+ data_path = self.data_list[idx % len(self.data_list)]
64
+ name = self.get_data_name(idx)
65
+ split = self.get_split_name(idx)
66
+
67
+ try:
68
+ data = torch.load(data_path, map_location="cpu")
69
+ except Exception as e:
70
+ raise IOError(f"Failed to load a .pt file at {data_path}. Error: {e}")
71
+
72
+ data = data.numpy().transpose()
73
+ color = data[:, 3:]
74
+ coord = data[:, :3]
75
+ normal = self.get_normals(np.mean(coord, axis=0), coord)
76
+
77
+ data_dict = {}
78
+
79
+ data_dict["coord"] = coord
80
+ data_dict["color"] = color
81
+ data_dict["normal"] = normal
82
+
83
+ data_dict["name"] = name
84
+ data_dict["split"] = split
85
+
86
+ return data_dict
87
+
88
+
89
+ @DATASETS.register_module()
90
+ class Cap3DImagePointDataset(DefaultImagePointDataset):
91
+ def __init__(
92
+ self,
93
+ data_num=-1,
94
+ **kwargs,
95
+ ):
96
+ self.data_num = data_num
97
+ super().__init__(**kwargs)
98
+
99
+ def get_data_list(self):
100
+ split_list = {}
101
+ if isinstance(self.split, str):
102
+ data_path = os.path.join(self.data_root, "splits", f"{self.split}.json")
103
+ with open(data_path, "r", encoding="utf-8") as file:
104
+ data_list = json.load(file)
105
+ split_list[self.split] = list(data_list.keys())
106
+ elif isinstance(self.split, Sequence):
107
+ data_list = {}
108
+ for split in self.split:
109
+ data_path = os.path.join(self.data_root, "splits", f"{split}.json")
110
+ with open(data_path, "r", encoding="utf-8") as file:
111
+ data_split_dict = json.load(file)
112
+ data_list.update(data_split_dict)
113
+ split_list[split] = list(data_split_dict.keys())
114
+ else:
115
+ raise NotImplementedError
116
+
117
+ if self.data_num < 0 or self.data_num >= len(data_list):
118
+ selected_data_list = data_list
119
+ selected_split_list = split_list
120
+ return selected_data_list, selected_split_list
121
+
122
+ all_keys = list(data_list.keys())
123
+ selected_keys = random.sample(all_keys, self.data_num)
124
+ selected_data_list = {key: data_list[key] for key in selected_keys}
125
+ valid_keys = set(selected_keys)
126
+
127
+ selected_split_list = {}
128
+ for split_name, key_list in split_list.items():
129
+ filtered_keys = [key for key in key_list if key in valid_keys]
130
+ if filtered_keys:
131
+ selected_split_list[split_name] = filtered_keys
132
+ return selected_data_list, selected_split_list
133
+
134
+ @staticmethod
135
+ def get_normals(center, coords):
136
+ Cs = np.repeat(center.reshape((1, -1)), coords.shape[0], axis=0)
137
+ view_dirs = coords - Cs
138
+ view_dirs = view_dirs / np.linalg.norm(view_dirs, axis=-1, keepdims=True)
139
+ pcd = o3d.geometry.PointCloud()
140
+ pcd.points = o3d.utility.Vector3dVector(coords)
141
+ pcd.estimate_normals()
142
+ normals = np.asarray(pcd.normals)
143
+ dot_product = np.sum(normals * view_dirs, axis=-1)
144
+ flip_mask = dot_product < 0
145
+ normals[flip_mask] = -normals[flip_mask]
146
+ normals = normals / np.linalg.norm(normals, axis=-1, keepdims=True)
147
+ return normals
148
+
149
+ def get_pc_data(self, data_path):
150
+ try:
151
+ data = torch.load(data_path, map_location="cpu")
152
+ except Exception as e:
153
+ raise IOError(f"Failed to load a .pt file at {data_path}. Error: {e}")
154
+
155
+ data = data.numpy().transpose()
156
+ color = data[:, 3:]
157
+ coord = data[:, :3]
158
+ normal = self.get_normals(np.mean(coord, axis=0), coord)
159
+
160
+ data_dict = {}
161
+
162
+ data_dict["coord"] = coord
163
+ data_dict["color"] = color
164
+ data_dict["normal"] = normal
165
+
166
+ return data_dict
167
+
168
+ def get_data(self, idx):
169
+ data_dict = {}
170
+ name = self.get_data_name(idx)
171
+ split = self.get_split_name(idx)
172
+ data_dict["name"] = name
173
+ data_dict["split"] = split
174
+ data_path = self.data_list[name]
175
+
176
+ pointclouds_path = data_path["pointclouds"]
177
+ pc_dict = self.get_pc_data(pointclouds_path)
178
+ data_dict.update(pc_dict)
179
+ imgs_path = data_path["images"]
180
+ imgs = [Image.open(asset).convert("RGB") for asset in imgs_path]
181
+ if len(imgs) > 0:
182
+ img_width, img_height = imgs[0].size
183
+ div_w = img_width // self.patch_w
184
+ div_h = img_height // self.patch_h
185
+ div_min = max(min(div_w, div_h), 1)
186
+ crop_img_width = div_min * self.patch_w
187
+ crop_img_height = div_min * self.patch_h
188
+ left = int((img_width - crop_img_width) / 2)
189
+ top = int((img_height - crop_img_height) / 2)
190
+ right = int((img_width + crop_img_width) / 2)
191
+ bottom = int((img_height + crop_img_height) / 2)
192
+ imgs = [img.crop((left, top, right, bottom)) for img in imgs]
193
+ imgs = [self.transform_img(img) for img in imgs]
194
+ imgs_list = torch.stack(imgs)
195
+ data_dict["images"] = imgs_list.float()
196
+ else:
197
+ data_dict["images"] = torch.empty(
198
+ (0, 3, self.patch_h * self.patch_size, self.patch_w * self.patch_size)
199
+ )
200
+ data_dict["img_num"] = np.array([data_dict["images"].shape[0]], dtype=np.int32)
201
+
202
+ correspondences_path = data_path["correspondences"]
203
+ correspondence_infos = np.ones(
204
+ (data_dict["coord"].shape[0], len(correspondences_path), 2),
205
+ dtype=np.float32,
206
+ ) * (-1)
207
+ for asset_id, asset in enumerate(correspondences_path):
208
+ correspondence_info = np.load(asset).astype(np.float32)
209
+ if np.array_equal(correspondence_info, -np.ones((1, 3))):
210
+ continue
211
+ correspondence_info = self.resize_correspondence_info(
212
+ correspondence_info,
213
+ (self.patch_h * self.patch_size, self.patch_w * self.patch_size),
214
+ (img_height, img_width),
215
+ (left, top, right, bottom),
216
+ self.patch_size,
217
+ )
218
+ correspondence_infos[
219
+ correspondence_info[:, -1].astype(np.int32), asset_id, :
220
+ ] = correspondence_info[:, :-1]
221
+ data_dict["correspondence"] = correspondence_infos
222
+
223
+ if "coord" in data_dict.keys():
224
+ data_dict["coord"] = data_dict["coord"].astype(np.float32)
225
+
226
+ if "color" in data_dict.keys():
227
+ data_dict["color"] = data_dict["color"].astype(np.float32)
228
+
229
+ if "normal" in data_dict.keys():
230
+ data_dict["normal"] = data_dict["normal"].astype(np.float32)
231
+
232
+ if "segment" in data_dict.keys():
233
+ data_dict["segment"] = data_dict["segment"].reshape([-1]).astype(np.int32)
234
+ else:
235
+ data_dict["segment"] = (
236
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
237
+ )
238
+
239
+ if "instance" in data_dict.keys():
240
+ data_dict["instance"] = data_dict["instance"].reshape([-1]).astype(np.int32)
241
+ else:
242
+ data_dict["instance"] = (
243
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
244
+ )
245
+ return data_dict
pointcept/datasets/dataloader.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import partial
2
+ import weakref
3
+ import torch
4
+ import math
5
+ import torch.utils.data
6
+ import torch.distributed as dist
7
+ from torch.utils.data import Sampler
8
+
9
+ import pointcept.utils.comm as comm
10
+ from pointcept.datasets.utils import point_collate_fn
11
+ from pointcept.datasets import ConcatDataset
12
+ from pointcept.utils.env import set_seed
13
+
14
+
15
+ class MultiDatasetDummySampler:
16
+ def __init__(self):
17
+ self.dataloader = None
18
+
19
+ def set_epoch(self, epoch):
20
+ if comm.get_world_size() > 1:
21
+ for dataloader in self.dataloader.dataloaders:
22
+ dataloader.sampler.set_epoch(epoch)
23
+ return
24
+
25
+
26
+ class MultiDatasetDataloader:
27
+ """
28
+ Multiple Datasets Dataloader, batch data from a same dataset and mix up ratio determined by loop of each sub dataset.
29
+ The overall length is determined by the main dataset (first) and loop of concat dataset.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ concat_dataset: ConcatDataset,
35
+ batch_size_per_gpu: int,
36
+ num_worker_per_gpu: int,
37
+ mix_prob=0,
38
+ seed=None,
39
+ ):
40
+ self.datasets = concat_dataset.datasets
41
+ self.ratios = [dataset.loop for dataset in self.datasets]
42
+ # reset data loop, original loop serve as ratios
43
+ for dataset in self.datasets:
44
+ dataset.loop = 1
45
+ # determine union training epoch by main dataset
46
+ self.datasets[0].loop = concat_dataset.loop
47
+ # build sub-dataloaders
48
+ num_workers = num_worker_per_gpu // len(self.datasets)
49
+ self.dataloaders = []
50
+ for dataset_id, dataset in enumerate(self.datasets):
51
+ if comm.get_world_size() > 1:
52
+ sampler = torch.utils.data.distributed.DistributedSampler(dataset)
53
+ else:
54
+ sampler = None
55
+
56
+ init_fn = (
57
+ partial(
58
+ self._worker_init_fn,
59
+ dataset_id=dataset_id,
60
+ num_workers=num_workers,
61
+ num_datasets=len(self.datasets),
62
+ rank=comm.get_rank(),
63
+ seed=seed,
64
+ )
65
+ if seed is not None
66
+ else None
67
+ )
68
+ self.dataloaders.append(
69
+ torch.utils.data.DataLoader(
70
+ dataset,
71
+ batch_size=batch_size_per_gpu,
72
+ shuffle=(sampler is None),
73
+ num_workers=num_worker_per_gpu,
74
+ sampler=sampler,
75
+ collate_fn=partial(point_collate_fn, mix_prob=mix_prob),
76
+ pin_memory=True,
77
+ worker_init_fn=init_fn,
78
+ drop_last=True,
79
+ persistent_workers=True,
80
+ )
81
+ )
82
+ self.sampler = MultiDatasetDummySampler()
83
+ self.sampler.dataloader = weakref.proxy(self)
84
+
85
+ def __iter__(self):
86
+ iterator = [iter(dataloader) for dataloader in self.dataloaders]
87
+ while True:
88
+ for i in range(len(self.ratios)):
89
+ for _ in range(self.ratios[i]):
90
+ try:
91
+ batch = next(iterator[i])
92
+ except StopIteration:
93
+ if i == 0:
94
+ return
95
+ else:
96
+ iterator[i] = iter(self.dataloaders[i])
97
+ batch = next(iterator[i])
98
+ yield batch
99
+
100
+ def __len__(self):
101
+ main_data_loader_length = len(self.dataloaders[0])
102
+ return (
103
+ main_data_loader_length // self.ratios[0] * sum(self.ratios)
104
+ + main_data_loader_length % self.ratios[0]
105
+ )
106
+
107
+ @staticmethod
108
+ def _worker_init_fn(worker_id, num_workers, dataset_id, num_datasets, rank, seed):
109
+ worker_seed = (
110
+ num_workers * num_datasets * rank
111
+ + num_workers * dataset_id
112
+ + worker_id
113
+ + seed
114
+ )
115
+ set_seed(worker_seed)
116
+
117
+
118
+ class DistributedImbalancedSampler(Sampler):
119
+ def __init__(
120
+ self,
121
+ dataset,
122
+ sampled_dataset_index,
123
+ sampled_dataset_limit,
124
+ num_replicas=None,
125
+ rank=None,
126
+ shuffle=True,
127
+ seed=0,
128
+ ):
129
+ if num_replicas is None:
130
+ if not dist.is_available():
131
+ num_replicas = 1
132
+ rank = 0
133
+ else:
134
+ try:
135
+ num_replicas = dist.get_world_size()
136
+ rank = dist.get_rank()
137
+ except:
138
+ num_replicas = 1
139
+ rank = 0
140
+
141
+ self.dataset = dataset
142
+ self.num_replicas = num_replicas
143
+ self.rank = rank
144
+ self.epoch = 0
145
+ self.seed = seed
146
+ self.sampled_dataset_index = sampled_dataset_index
147
+ self.sampled_dataset_limit = sampled_dataset_limit
148
+ self.shuffle = shuffle
149
+
150
+ self.lengths = [len(d) for d in dataset.datasets]
151
+ self.offsets = [0] + list(
152
+ torch.cumsum(torch.tensor(self.lengths), 0)[:-1].numpy()
153
+ )
154
+ self.total_size_per_epoch = (
155
+ sum(self.lengths)
156
+ - self.lengths[self.sampled_dataset_index]
157
+ + self.sampled_dataset_limit
158
+ )
159
+ self.num_samples = math.ceil(self.total_size_per_epoch / self.num_replicas)
160
+ self.total_size = self.num_samples * self.num_replicas
161
+
162
+ def __iter__(self):
163
+ g = torch.Generator()
164
+ g.manual_seed(self.seed + self.epoch)
165
+
166
+ indices = []
167
+ for i, length in enumerate(self.lengths):
168
+ if i == self.sampled_dataset_index:
169
+ sub_indices = (
170
+ torch.randperm(length, generator=g)[: self.sampled_dataset_limit]
171
+ + self.offsets[i]
172
+ )
173
+ indices.append(sub_indices)
174
+ else:
175
+ sub_indices = torch.arange(length) + self.offsets[i]
176
+ indices.append(sub_indices)
177
+
178
+ final_indices = torch.cat(indices)
179
+
180
+ if self.shuffle:
181
+ final_indices = final_indices[
182
+ torch.randperm(len(final_indices), generator=g)
183
+ ]
184
+
185
+ final_indices = final_indices.tolist()
186
+ padding_size = self.total_size - len(final_indices)
187
+ if padding_size <= len(final_indices):
188
+ final_indices += final_indices[:padding_size]
189
+ else:
190
+ final_indices += (
191
+ final_indices * math.ceil(padding_size / len(final_indices))
192
+ )[:padding_size]
193
+
194
+ assert len(final_indices) == self.total_size
195
+
196
+ offset = self.num_samples * self.rank
197
+ subsample_indices = final_indices[offset : offset + self.num_samples]
198
+
199
+ return iter(subsample_indices)
200
+
201
+ def __len__(self):
202
+ return self.num_samples
203
+
204
+ def set_epoch(self, epoch):
205
+ self.epoch = epoch
pointcept/datasets/defaults.py ADDED
@@ -0,0 +1,676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Default Datasets
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+ import glob
10
+ import json
11
+ import numpy as np
12
+ import torch
13
+ from copy import deepcopy
14
+ from torch.utils.data import Dataset
15
+ from collections.abc import Sequence
16
+ from torchvision.transforms import InterpolationMode
17
+ from PIL import Image
18
+ from torchvision.transforms import transforms as T
19
+
20
+ from pointcept.utils.logger import get_root_logger
21
+ from pointcept.utils.cache import shared_dict
22
+
23
+ from .builder import DATASETS, build_dataset
24
+ from .transform import Compose, TRANSFORMS
25
+
26
+ INTERPOLATION_MODE = {
27
+ "bilinear": InterpolationMode.BILINEAR,
28
+ "bicubic": InterpolationMode.BICUBIC,
29
+ "nearest": InterpolationMode.NEAREST,
30
+ }
31
+
32
+
33
+ @DATASETS.register_module()
34
+ class DefaultDataset(Dataset):
35
+ VALID_ASSETS = [
36
+ "coord",
37
+ "color",
38
+ "normal",
39
+ "strength",
40
+ "segment",
41
+ "instance",
42
+ "pose",
43
+ ]
44
+
45
+ def __init__(
46
+ self,
47
+ split="train",
48
+ data_root="data/dataset",
49
+ transform=None,
50
+ test_mode=False,
51
+ test_cfg=None,
52
+ cache=False,
53
+ ignore_index=-1,
54
+ loop=1,
55
+ ):
56
+ super(DefaultDataset, self).__init__()
57
+ self.data_root = data_root
58
+ self.split = split
59
+ self.transform = Compose(transform)
60
+ self.cache = cache
61
+ self.ignore_index = ignore_index
62
+ self.loop = (
63
+ loop if not test_mode else 1
64
+ ) # force make loop = 1 while in test mode
65
+ self.test_mode = test_mode
66
+ self.test_cfg = test_cfg if test_mode else None
67
+ if test_mode:
68
+ self.test_voxelize = TRANSFORMS.build(self.test_cfg.voxelize)
69
+ self.test_crop = (
70
+ TRANSFORMS.build(self.test_cfg.crop) if self.test_cfg.crop else None
71
+ )
72
+ self.post_transform = Compose(self.test_cfg.post_transform)
73
+ self.aug_transform = [Compose(aug) for aug in self.test_cfg.aug_transform]
74
+
75
+ self.data_list = self.get_data_list()
76
+ logger = get_root_logger()
77
+ logger.info(
78
+ "Totally {} x {} samples in {} {} set.".format(
79
+ len(self.data_list), self.loop, os.path.basename(self.data_root), split
80
+ )
81
+ )
82
+
83
+ def get_data_list(self):
84
+ if isinstance(self.split, str):
85
+ split_list = [self.split]
86
+ elif isinstance(self.split, Sequence):
87
+ split_list = self.split
88
+ else:
89
+ raise NotImplementedError
90
+
91
+ data_list = []
92
+ for split in split_list:
93
+ if os.path.isfile(os.path.join(self.data_root, split)):
94
+ with open(os.path.join(self.data_root, split)) as f:
95
+ data_list += [
96
+ os.path.join(self.data_root, data) for data in json.load(f)
97
+ ]
98
+ else:
99
+ data_list += glob.glob(os.path.join(self.data_root, split, "*"))
100
+ return data_list
101
+
102
+ def get_data(self, idx):
103
+ data_path = self.data_list[idx % len(self.data_list)]
104
+ name = self.get_data_name(idx)
105
+ split = self.get_split_name(idx)
106
+ if self.cache:
107
+ cache_name = f"pointcept-{name}"
108
+ return shared_dict(cache_name)
109
+
110
+ data_dict = {}
111
+ assets = os.listdir(data_path)
112
+ for asset in assets:
113
+ if not asset.endswith(".npy"):
114
+ continue
115
+ if asset[:-4] not in self.VALID_ASSETS:
116
+ continue
117
+ data_dict[asset[:-4]] = np.load(os.path.join(data_path, asset))
118
+ data_dict["name"] = name
119
+ data_dict["split"] = split
120
+
121
+ if "coord" in data_dict.keys():
122
+ data_dict["coord"] = data_dict["coord"].astype(np.float32)
123
+
124
+ if "color" in data_dict.keys():
125
+ data_dict["color"] = data_dict["color"].astype(np.float32)
126
+
127
+ if "normal" in data_dict.keys():
128
+ data_dict["normal"] = data_dict["normal"].astype(np.float32)
129
+
130
+ if "segment" in data_dict.keys():
131
+ data_dict["segment"] = data_dict["segment"].reshape([-1]).astype(np.int32)
132
+ else:
133
+ data_dict["segment"] = (
134
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
135
+ )
136
+
137
+ if "instance" in data_dict.keys():
138
+ data_dict["instance"] = data_dict["instance"].reshape([-1]).astype(np.int32)
139
+ else:
140
+ data_dict["instance"] = (
141
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
142
+ )
143
+ return data_dict
144
+
145
+ def get_data_name(self, idx):
146
+ return os.path.basename(self.data_list[idx % len(self.data_list)])
147
+
148
+ def get_split_name(self, idx):
149
+ return os.path.basename(
150
+ os.path.dirname(self.data_list[idx % len(self.data_list)])
151
+ )
152
+
153
+ def prepare_train_data(self, idx):
154
+ # load data
155
+ data_dict = self.get_data(idx)
156
+ data_dict = self.transform(data_dict)
157
+ return data_dict
158
+
159
+ def prepare_test_data(self, idx):
160
+ # load data
161
+ data_dict = self.get_data(idx)
162
+ data_dict = self.transform(data_dict)
163
+ result_dict = dict(segment=data_dict.pop("segment"), name=data_dict.pop("name"))
164
+ if "origin_segment" in data_dict:
165
+ assert "inverse" in data_dict
166
+ result_dict["origin_segment"] = data_dict.pop("origin_segment")
167
+ result_dict["inverse"] = data_dict.pop("inverse")
168
+
169
+ data_dict_list = []
170
+ for aug in self.aug_transform:
171
+ data_dict_list.append(aug(deepcopy(data_dict)))
172
+
173
+ fragment_list = []
174
+ for data in data_dict_list:
175
+ if self.test_voxelize is not None:
176
+ data_part_list = self.test_voxelize(data)
177
+ else:
178
+ data["index"] = np.arange(data["coord"].shape[0])
179
+ data_part_list = [data]
180
+ for data_part in data_part_list:
181
+ if self.test_crop is not None:
182
+ data_part = self.test_crop(data_part)
183
+ else:
184
+ data_part = [data_part]
185
+ fragment_list += data_part
186
+
187
+ for i in range(len(fragment_list)):
188
+ fragment_list[i] = self.post_transform(fragment_list[i])
189
+ result_dict["fragment_list"] = fragment_list
190
+ return result_dict
191
+
192
+ def __getitem__(self, idx):
193
+ if self.test_mode:
194
+ return self.prepare_test_data(idx)
195
+ else:
196
+ return self.prepare_train_data(idx)
197
+
198
+ def __len__(self):
199
+ return len(self.data_list) * self.loop
200
+
201
+
202
+ @DATASETS.register_module()
203
+ class DefaultImagePointDataset(Dataset):
204
+ PC_VALID_ASSETS = [
205
+ "coord",
206
+ "color",
207
+ "normal",
208
+ "superpoint",
209
+ "pose",
210
+ "segment",
211
+ "instance",
212
+ "strength",
213
+ ]
214
+
215
+ def __init__(
216
+ self,
217
+ split="train",
218
+ data_root="data/dataset",
219
+ transform=None,
220
+ test_mode=False,
221
+ test_cfg=None,
222
+ cache=False,
223
+ ignore_index=-1,
224
+ loop=1,
225
+ crop_h=630,
226
+ crop_w=1120,
227
+ patch_size=14,
228
+ interpolation="bilinear",
229
+ if_img=True,
230
+ ):
231
+ super(DefaultImagePointDataset, self).__init__()
232
+ self.if_img = if_img
233
+ self.data_root = data_root
234
+ self.split = split
235
+ self.transform = Compose(transform)
236
+ self.cache = cache
237
+ self.ignore_index = ignore_index
238
+ self.loop = (
239
+ loop if not test_mode else 1
240
+ ) # force make loop = 1 while in test mode
241
+ self.test_mode = test_mode
242
+ self.test_cfg = test_cfg if test_mode else None
243
+
244
+ if test_mode:
245
+ self.test_voxelize = TRANSFORMS.build(self.test_cfg.voxelize)
246
+ self.test_crop = (
247
+ TRANSFORMS.build(self.test_cfg.crop) if self.test_cfg.crop else None
248
+ )
249
+ self.post_transform = Compose(self.test_cfg.post_transform)
250
+ self.aug_transform = [Compose(aug) for aug in self.test_cfg.aug_transform]
251
+
252
+ self.data_list, self.split_list = self.get_data_list()
253
+ if isinstance(self.data_list, dict):
254
+ self.data_name = list(self.data_list.keys())
255
+ elif isinstance(self.data_list[0], dict):
256
+ self.data_name = list([data["token"] for data in self.data_list])
257
+ else:
258
+ self.data_name = self.data_list
259
+ logger = get_root_logger()
260
+ logger.info(
261
+ "Totally {} x {} samples in {} {} set.".format(
262
+ len(self.data_name), self.loop, os.path.basename(self.data_root), split
263
+ )
264
+ )
265
+
266
+ self.crop_h = crop_h
267
+ self.crop_w = crop_w
268
+ self.patch_size = patch_size
269
+ self.patch_h = crop_h // patch_size
270
+ self.patch_w = crop_w // patch_size
271
+ self.transform_img = T.Compose(
272
+ [
273
+ T.Resize(
274
+ (self.patch_h * self.patch_size, self.patch_w * self.patch_size),
275
+ interpolation=INTERPOLATION_MODE[interpolation],
276
+ ),
277
+ T.ToTensor(),
278
+ ]
279
+ )
280
+
281
+ def resize_correspondence_info(
282
+ self, correspondence, size, size0, crop_size, _alignment
283
+ ):
284
+ h, w = size
285
+ h0, w0 = size0
286
+ left, top, right, bottom = crop_size
287
+ crop_h = bottom - top
288
+ crop_w = right - left
289
+ mask_crop = (
290
+ (correspondence[:, 1] >= top)
291
+ & (correspondence[:, 1] < bottom)
292
+ & (correspondence[:, 0] >= left)
293
+ & (correspondence[:, 0] < right)
294
+ )
295
+ correspondence = correspondence[mask_crop]
296
+ correspondence[:, 1] -= top
297
+ correspondence[:, 0] -= left
298
+ correspondence[:, 1] = (correspondence[:, 1] * h / crop_h / _alignment).astype(
299
+ np.float32
300
+ )
301
+ correspondence[:, 0] = (correspondence[:, 0] * w / crop_w / _alignment).astype(
302
+ np.float32
303
+ )
304
+ correspondence = correspondence[:, [1, 0, 2]]
305
+ correspondence = np.unique(correspondence, axis=0)
306
+ return correspondence
307
+
308
+ def get_data_list(self):
309
+ split_list = {}
310
+ if isinstance(self.split, str):
311
+ data_path = os.path.join(self.data_root, "splits", f"{self.split}.json")
312
+ with open(data_path, "r", encoding="utf-8") as file:
313
+ data_list = json.load(file)
314
+ split_list[self.split] = list(data_list.keys())
315
+ elif isinstance(self.split, Sequence):
316
+ data_list = {}
317
+ for split in self.split:
318
+ data_path = os.path.join(self.data_root, "splits", f"{split}.json")
319
+ with open(data_path, "r", encoding="utf-8") as file:
320
+ data_split_dict = json.load(file)
321
+ data_list.update(data_split_dict)
322
+ split_list[split] = list(data_split_dict.keys())
323
+ else:
324
+ raise NotImplementedError
325
+ return data_list, split_list
326
+
327
+ def get_data_name(self, idx):
328
+ return self.data_name[idx % len(self.data_name)]
329
+
330
+ def get_split_name(self, idx):
331
+ for split, names in self.split_list.items():
332
+ if self.data_name[idx % len(self.data_name)] in names:
333
+ return split
334
+ return None
335
+
336
+ def get_data(self, idx):
337
+ data_dict = {}
338
+ name = self.get_data_name(idx)
339
+ split = self.get_split_name(idx)
340
+ data_dict["name"] = name
341
+ data_dict["split"] = split
342
+ data_path = self.data_list[name]
343
+
344
+ pointclouds_path = data_path["pointclouds"]
345
+ assets = os.listdir(pointclouds_path)
346
+ for asset in assets:
347
+ if not asset.endswith(".npy"):
348
+ continue
349
+ if asset[:-4] not in self.PC_VALID_ASSETS:
350
+ continue
351
+ data_dict[asset[:-4]] = np.load(os.path.join(pointclouds_path, asset))
352
+ if self.if_img:
353
+ imgs_path = data_path["images"]
354
+ imgs = [Image.open(asset) for asset in imgs_path]
355
+ if len(imgs) > 0:
356
+ img_width, img_height = imgs[0].size
357
+ div_w = img_width // self.patch_w
358
+ div_h = img_height // self.patch_h
359
+ div_min = max(min(div_w, div_h), 1)
360
+ crop_img_width = div_min * self.patch_w
361
+ crop_img_height = div_min * self.patch_h
362
+ left = int((img_width - crop_img_width) / 2)
363
+ top = int((img_height - crop_img_height) / 2)
364
+ right = int((img_width + crop_img_width) / 2)
365
+ bottom = int((img_height + crop_img_height) / 2)
366
+ imgs = [img.crop((left, top, right, bottom)) for img in imgs]
367
+ imgs = [self.transform_img(img) for img in imgs]
368
+ imgs_list = torch.stack(imgs)
369
+ data_dict["images"] = imgs_list.float()
370
+ else:
371
+ data_dict["images"] = torch.empty(
372
+ (
373
+ 0,
374
+ 3,
375
+ self.patch_h * self.patch_size,
376
+ self.patch_w * self.patch_size,
377
+ )
378
+ )
379
+ data_dict["img_num"] = np.array(
380
+ [data_dict["images"].shape[0]], dtype=np.int32
381
+ )
382
+
383
+ correspondences_path = data_path["correspondences"]
384
+ correspondence_infos = np.ones(
385
+ (data_dict["coord"].shape[0], len(correspondences_path), 2),
386
+ dtype=np.float32,
387
+ ) * (-1)
388
+ for asset_id, asset in enumerate(correspondences_path):
389
+ correspondence_info = np.load(asset).astype(np.float32)
390
+ if np.array_equal(correspondence_info, -np.ones((1, 3))):
391
+ continue
392
+ correspondence_info = self.resize_correspondence_info(
393
+ correspondence_info,
394
+ (self.patch_h * self.patch_size, self.patch_w * self.patch_size),
395
+ (img_height, img_width),
396
+ (left, top, right, bottom),
397
+ self.patch_size,
398
+ )
399
+ correspondence_infos[
400
+ correspondence_info[:, -1].astype(np.int32), asset_id, :
401
+ ] = correspondence_info[:, :-1]
402
+ data_dict["correspondence"] = correspondence_infos # .reshape(-1, 2)
403
+
404
+ if "coord" in data_dict.keys():
405
+ data_dict["coord"] = data_dict["coord"].astype(np.float32)
406
+
407
+ if "color" in data_dict.keys():
408
+ data_dict["color"] = data_dict["color"].astype(np.float32)
409
+
410
+ if "normal" in data_dict.keys():
411
+ data_dict["normal"] = data_dict["normal"].astype(np.float32)
412
+
413
+ if "segment" in data_dict.keys():
414
+ data_dict["segment"] = data_dict["segment"].reshape([-1]).astype(np.int32)
415
+ else:
416
+ data_dict["segment"] = (
417
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
418
+ )
419
+
420
+ if "instance" in data_dict.keys():
421
+ data_dict["instance"] = data_dict["instance"].reshape([-1]).astype(np.int32)
422
+ else:
423
+ data_dict["instance"] = (
424
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
425
+ )
426
+ return data_dict
427
+
428
+ def prepare_train_data(self, idx):
429
+ # load data
430
+ data_dict = self.get_data(idx)
431
+ data_dict = self.transform(data_dict)
432
+ return data_dict
433
+
434
+ def prepare_test_data(self, idx):
435
+ # load data
436
+ data_dict = self.get_data(idx)
437
+ data_dict = self.transform(data_dict)
438
+ result_dict = dict(segment=data_dict.pop("segment"), name=data_dict.pop("name"))
439
+ if "origin_segment" in data_dict:
440
+ assert "inverse" in data_dict
441
+ result_dict["origin_segment"] = data_dict.pop("origin_segment")
442
+ result_dict["inverse"] = data_dict.pop("inverse")
443
+
444
+ data_dict_list = []
445
+ for aug in self.aug_transform:
446
+ data_dict_list.append(aug(deepcopy(data_dict)))
447
+
448
+ fragment_list = []
449
+ for data in data_dict_list:
450
+ if self.test_voxelize is not None:
451
+ data_part_list = self.test_voxelize(data)
452
+ else:
453
+ data["index"] = np.arange(data["coord"].shape[0])
454
+ data_part_list = [data]
455
+ for data_part in data_part_list:
456
+ if self.test_crop is not None:
457
+ data_part = self.test_crop(data_part)
458
+ else:
459
+ data_part = [data_part]
460
+ fragment_list += data_part
461
+
462
+ for i in range(len(fragment_list)):
463
+ fragment_list[i] = self.post_transform(fragment_list[i])
464
+ result_dict["fragment_list"] = fragment_list
465
+ return result_dict
466
+
467
+ def __getitem__(self, idx):
468
+ if self.test_mode:
469
+ return self.prepare_test_data(idx)
470
+ else:
471
+ return self.prepare_train_data(idx)
472
+
473
+ def __len__(self):
474
+ return len(self.data_list) * self.loop
475
+
476
+
477
+ @DATASETS.register_module()
478
+ class DefaultMultiViewImagePointDataset(DefaultImagePointDataset):
479
+ def __init__(self, depth_scale=1000.0, if_img=True, **kwargs):
480
+ self.depth_scale = depth_scale
481
+ self.if_img = if_img
482
+ super().__init__(if_img=if_img, **kwargs)
483
+
484
+ def get_data_list(self):
485
+ split_list = {}
486
+ if isinstance(self.split, str):
487
+ splits_to_load = [self.split]
488
+ elif isinstance(self.split, Sequence):
489
+ splits_to_load = self.split
490
+ else:
491
+ raise NotImplementedError
492
+
493
+ data_list = {}
494
+ for split in splits_to_load:
495
+ data_path = os.path.join(self.data_root, "splits", f"{split}.json")
496
+ if not os.path.exists(data_path):
497
+ raise FileNotFoundError(f"Split file not found: {data_path}")
498
+
499
+ with open(data_path, "r", encoding="utf-8") as file:
500
+ data_split_dict = json.load(file)
501
+ data_list.update(data_split_dict)
502
+ split_list[split] = list(data_split_dict.keys())
503
+ return data_list, split_list
504
+
505
+ @staticmethod
506
+ def get_normals(center, coords):
507
+ normals = coords - center[None, :]
508
+ normals = normals / np.linalg.norm(normals, axis=-1, keepdims=True)
509
+ return normals
510
+
511
+ def get_data(self, idx):
512
+ name = self.get_data_name(idx)
513
+ split = self.get_split_name(idx)
514
+ data_info = self.data_list[name]
515
+
516
+ all_points_world, all_colors, camera_params = [], [], []
517
+ correspondences = []
518
+ coord_num = 0
519
+ for i in range(len(data_info["images"])):
520
+ rgb_path = data_info["images"][i]
521
+ depth_path = data_info["depths"][i]
522
+ pose_path = data_info["Ts"][i]
523
+ intr_path = data_info["Ks"][i]
524
+
525
+ try:
526
+ with Image.open(rgb_path) as img:
527
+ color_img = np.array(img)
528
+ with Image.open(depth_path) as img:
529
+ depth_img = np.array(img)
530
+ cam_pose_world = np.load(pose_path)
531
+ cam_intr = np.load(intr_path)
532
+ camera_params.append({"pose": cam_pose_world, "intr": cam_intr})
533
+ except FileNotFoundError:
534
+ continue
535
+
536
+ depth_m = depth_img.astype(np.float32) / self.depth_scale
537
+ h, w = depth_m.shape
538
+ fx, fy, cx, cy = (
539
+ cam_intr[0, 0],
540
+ cam_intr[1, 1],
541
+ cam_intr[0, 2],
542
+ cam_intr[1, 2],
543
+ )
544
+ v, u = np.indices((h, w))
545
+ valid_mask = (depth_m > 0) & (depth_m < 0.8)
546
+ z_cam = depth_m[valid_mask]
547
+ x_cam, y_cam = (u[valid_mask] - cx) * z_cam / fx, (
548
+ v[valid_mask] - cy
549
+ ) * z_cam / fy
550
+ points_cam = np.vstack((x_cam, y_cam, z_cam)).T
551
+ points_world = (
552
+ cam_pose_world
553
+ @ np.hstack([points_cam, np.ones((points_cam.shape[0], 1))]).T
554
+ ).T[:, :3]
555
+ all_points_world.append(points_world)
556
+ all_colors.append(color_img[valid_mask])
557
+
558
+ pixel = np.vstack([u[valid_mask], v[valid_mask]]).T
559
+ pixel = np.concatenate([pixel, np.arange(pixel.shape[0])[:, None]], axis=1)
560
+ pixel[:, 2] += coord_num
561
+ coord_num += points_world.shape[0]
562
+ correspondences.append(pixel)
563
+ coord = (
564
+ np.concatenate(all_points_world, axis=0)
565
+ if all_points_world
566
+ else np.zeros((0, 3))
567
+ )
568
+ coord[:, 2] = -coord[:, 2]
569
+ color = np.concatenate(all_colors, axis=0) if all_colors else np.zeros((0, 3))
570
+ normal = self.get_normals(np.array([0, 0, 0]), coord)
571
+
572
+ if self.if_img:
573
+ imgs_path = data_info["images"]
574
+ imgs = [Image.open(asset) for asset in imgs_path]
575
+ img_width, img_height = imgs[0].size
576
+ div_w = img_width // self.patch_w
577
+ div_h = img_height // self.patch_h
578
+ div_min = max(min(div_w, div_h), 1)
579
+ crop_img_width = div_min * self.patch_w
580
+ crop_img_height = div_min * self.patch_h
581
+ left = int((img_width - crop_img_width) / 2)
582
+ top = int((img_height - crop_img_height) / 2)
583
+ right = int((img_width + crop_img_width) / 2)
584
+ bottom = int((img_height + crop_img_height) / 2)
585
+ imgs = [img.crop((left, top, right, bottom)) for img in imgs]
586
+ imgs = [self.transform_img(img) for img in imgs]
587
+ if len(imgs) > 0:
588
+ imgs_list = torch.stack(imgs)
589
+ imgs_tensor = imgs_list.float()
590
+ else:
591
+ imgs_tensor = torch.empty(
592
+ (
593
+ 0,
594
+ 3,
595
+ self.patch_h * self.patch_size,
596
+ self.patch_w * self.patch_size,
597
+ )
598
+ )
599
+ img_num = np.array([imgs_tensor.shape[0]], dtype=np.int32)
600
+
601
+ num_points, num_imgs = coord.shape[0], len(imgs)
602
+ correspondence_infos = (
603
+ np.ones((num_points, num_imgs, 2), dtype=np.float32) * -1
604
+ )
605
+
606
+ for asset_id, correspondence_info in enumerate(correspondences):
607
+ correspondence_info = self.resize_correspondence_info(
608
+ correspondence_info,
609
+ (self.patch_h * self.patch_size, self.patch_w * self.patch_size),
610
+ (img_height, img_width),
611
+ (left, top, right, bottom),
612
+ self.patch_size,
613
+ )
614
+ correspondence_infos[
615
+ correspondence_info[:, -1].astype(np.int32), asset_id, :
616
+ ] = correspondence_info[:, :-1]
617
+
618
+ data_dict = {
619
+ "name": name,
620
+ "split": split,
621
+ "coord": coord.astype(np.float32),
622
+ "color": color.astype(np.float32),
623
+ "normal": normal.astype(np.float32),
624
+ "images": imgs_tensor,
625
+ "img_num": img_num,
626
+ "correspondence": correspondence_infos,
627
+ }
628
+ else:
629
+ data_dict = {
630
+ "name": name,
631
+ "split": split,
632
+ "coord": coord.astype(np.float32),
633
+ "color": color.astype(np.float32),
634
+ "normal": normal.astype(np.float32),
635
+ }
636
+ return data_dict
637
+
638
+
639
+ @DATASETS.register_module()
640
+ class ConcatDataset(Dataset):
641
+ def __init__(self, datasets, loop=1):
642
+ super(ConcatDataset, self).__init__()
643
+ self.datasets = [build_dataset(dataset) for dataset in datasets]
644
+ self.loop = loop
645
+ self.data_list = self.get_data_list()
646
+ logger = get_root_logger()
647
+ logger.info(
648
+ "Totally {} x {} samples in the concat set.".format(
649
+ len(self.data_list), self.loop
650
+ )
651
+ )
652
+
653
+ def get_data_list(self):
654
+ data_list = []
655
+ for i in range(len(self.datasets)):
656
+ data_list.extend(
657
+ zip(
658
+ np.ones(len(self.datasets[i]), dtype=int) * i,
659
+ np.arange(len(self.datasets[i])),
660
+ )
661
+ )
662
+ return data_list
663
+
664
+ def get_data(self, idx):
665
+ dataset_idx, data_idx = self.data_list[idx % len(self.data_list)]
666
+ return self.datasets[dataset_idx][data_idx]
667
+
668
+ def get_data_name(self, idx):
669
+ dataset_idx, data_idx = self.data_list[idx % len(self.data_list)]
670
+ return self.datasets[dataset_idx].get_data_name(data_idx)
671
+
672
+ def __getitem__(self, idx):
673
+ return self.get_data(idx)
674
+
675
+ def __len__(self):
676
+ return len(self.data_list) * self.loop
pointcept/datasets/hk.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HK Remote Data Dataset
3
+
4
+ Author: Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+ import numpy as np
10
+ import torch
11
+
12
+ from pointcept.utils.cache import shared_dict
13
+ from .builder import DATASETS
14
+ from .defaults import DefaultDataset
15
+ import numpy as np
16
+ from scipy.spatial import cKDTree
17
+
18
+
19
+ def find_closest_pair_kdtree(points):
20
+ n = points.shape[0]
21
+ if n < 2:
22
+ return float("inf"), None, None
23
+
24
+ tree = cKDTree(points)
25
+ distances, indices = tree.query(points, k=2)
26
+ min_dist = np.min(distances[:, 1])
27
+ min_idx_in_distances = np.argmin(distances[:, 1])
28
+ point1_idx = min_idx_in_distances
29
+ point2_idx = indices[min_idx_in_distances, 1]
30
+
31
+ return min_dist, (points[point1_idx], points[point2_idx])
32
+
33
+
34
+ @DATASETS.register_module()
35
+ class HKDataset(DefaultDataset):
36
+ def __init__(self, crop_h=630, crop_w=1120, patch_size=14, if_img=True, **kwargs):
37
+ self.crop_h = crop_h
38
+ self.crop_w = crop_w
39
+ self.patch_size = patch_size
40
+ self.patch_h = crop_h // patch_size
41
+ self.patch_w = crop_w // patch_size
42
+ self.if_img = if_img
43
+ super().__init__(**kwargs)
44
+
45
+ def get_data(self, idx):
46
+ data_path = self.data_list[idx % len(self.data_list)]
47
+ name = self.get_data_name(idx)
48
+ split = self.get_split_name(idx)
49
+ if self.cache:
50
+ cache_name = f"pointcept-{name}"
51
+ return shared_dict(cache_name)
52
+
53
+ data_dict = {}
54
+ assets = os.listdir(data_path)
55
+ for asset in assets:
56
+ if not asset.endswith(".npy"):
57
+ continue
58
+ if asset[:-4] not in self.VALID_ASSETS:
59
+ continue
60
+ data_dict[asset[:-4]] = np.load(os.path.join(data_path, asset))
61
+ data_dict["name"] = name
62
+ data_dict["split"] = split
63
+ if self.if_img:
64
+ data_dict["images"] = torch.empty(
65
+ (0, 3, self.patch_h * self.patch_size, self.patch_w * self.patch_size)
66
+ )
67
+ data_dict["img_num"] = np.array([0], dtype=np.int32)
68
+ data_dict["correspondence"] = np.ones(
69
+ (data_dict["coord"].shape[0], 0, 2), dtype=np.float32
70
+ ) * (-1)
71
+ if "coord" in data_dict.keys():
72
+ data_dict["coord"] = data_dict["coord"].astype(np.float32)
73
+
74
+ if "color" in data_dict.keys():
75
+ data_dict["color"] = data_dict["color"].astype(np.float32)
76
+
77
+ if "normal" in data_dict.keys():
78
+ data_dict["normal"] = data_dict["normal"].astype(np.float32)
79
+
80
+ if "segment" in data_dict.keys():
81
+ data_dict["segment"] = data_dict["segment"].reshape([-1]).astype(np.int32)
82
+ else:
83
+ data_dict["segment"] = (
84
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
85
+ )
86
+
87
+ if "instance" in data_dict.keys():
88
+ data_dict["instance"] = data_dict["instance"].reshape([-1]).astype(np.int32)
89
+ else:
90
+ data_dict["instance"] = (
91
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
92
+ )
93
+
94
+ return data_dict
pointcept/datasets/hm3d.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Habitat-Matterport 3D Dataset
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import glob
9
+ import os
10
+ from collections.abc import Sequence
11
+ from .builder import DATASETS
12
+ from .defaults import DefaultDataset
13
+
14
+
15
+ @DATASETS.register_module()
16
+ class HM3DDataset(DefaultDataset):
17
+ def __init__(
18
+ self,
19
+ force_label=True,
20
+ **kwargs,
21
+ ):
22
+ # if force_label, only load data with label
23
+ self.force_label = force_label
24
+ super().__init__(**kwargs)
25
+
26
+ def get_single_data_list(self, split):
27
+ if self.force_label:
28
+ data_list = glob.glob(
29
+ os.path.join(self.data_root, split, "*", "segment.npy")
30
+ )
31
+ data_list = [os.path.dirname(data) for data in data_list]
32
+ else:
33
+ data_list = glob.glob(os.path.join(self.data_root, split, "*"))
34
+ return data_list
35
+
36
+ def get_data_list(self):
37
+ if isinstance(self.split, str):
38
+ data_list = self.get_single_data_list(self.split)
39
+ elif isinstance(self.split, Sequence):
40
+ data_list = []
41
+ for split in self.split:
42
+ data_list += self.get_single_data_list(split)
43
+ else:
44
+ raise NotImplementedError
45
+ return data_list
pointcept/datasets/modelnet.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ModelNet40 Dataset
3
+
4
+ get sampled point clouds of ModelNet40 (XYZ and normal from mesh, 10k points per shape)
5
+ at "https://shapenet.cs.stanford.edu/media/modelnet40_normal_resampled.zip"
6
+
7
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com)
8
+ Please cite our work if the code is helpful to you.
9
+ """
10
+
11
+ import os
12
+ import numpy as np
13
+ import copy
14
+ import pointops
15
+ import torch
16
+ from torch.utils.data import Dataset
17
+ from copy import deepcopy
18
+
19
+
20
+ from pointcept.utils.logger import get_root_logger
21
+ from .builder import DATASETS
22
+ from .transform import Compose
23
+
24
+
25
+ @DATASETS.register_module()
26
+ class ModelNetDataset(Dataset):
27
+ def __init__(
28
+ self,
29
+ split="train",
30
+ data_root="data/modelnet40",
31
+ class_names=None,
32
+ transform=None,
33
+ num_points=8192,
34
+ uniform_sampling=True,
35
+ save_record=True,
36
+ test_mode=False,
37
+ test_cfg=None,
38
+ loop=1,
39
+ if_color=False,
40
+ ):
41
+ super().__init__()
42
+ self.data_root = data_root
43
+ self.class_names = dict(zip(class_names, range(len(class_names))))
44
+ self.split = split
45
+ self.num_point = num_points
46
+ self.uniform_sampling = uniform_sampling
47
+ self.transform = Compose(transform)
48
+ self.loop = (
49
+ loop if not test_mode else 1
50
+ ) # force make loop = 1 while in test mode
51
+ self.if_color = if_color
52
+ self.test_mode = test_mode
53
+ self.test_cfg = test_cfg if test_mode else None
54
+ if test_mode:
55
+ self.post_transform = Compose(self.test_cfg.post_transform)
56
+ self.aug_transform = [Compose(aug) for aug in self.test_cfg.aug_transform]
57
+
58
+ self.data_list = self.get_data_list()
59
+ logger = get_root_logger()
60
+ logger.info(
61
+ "Totally {} x {} samples in {} set.".format(
62
+ len(self.data_list), self.loop, split
63
+ )
64
+ )
65
+
66
+ # check, prepare record
67
+ record_name = f"modelnet40_{self.split}"
68
+ if num_points is not None:
69
+ record_name += f"_{num_points}points"
70
+ if uniform_sampling:
71
+ record_name += "_uniform"
72
+ record_path = os.path.join(self.data_root, f"{record_name}.pth")
73
+ if os.path.isfile(record_path):
74
+ logger.info(f"Loading record: {record_name} ...")
75
+ self.data = torch.load(record_path, weights_only=False)
76
+ else:
77
+ logger.info(f"Preparing record: {record_name} ...")
78
+ self.data = {}
79
+ for idx in range(len(self.data_list)):
80
+ data_name = self.data_list[idx]
81
+ logger.info(f"Parsing data [{idx}/{len(self.data_list)}]: {data_name}")
82
+ self.data[data_name] = self.get_data(idx)
83
+ if save_record:
84
+ torch.save(self.data, record_path)
85
+
86
+ def get_data(self, idx):
87
+ data_idx = idx % len(self.data_list)
88
+ data_name = self.data_list[data_idx]
89
+ if data_name in self.data.keys():
90
+ return copy.deepcopy(self.data[data_name])
91
+ else:
92
+ data_shape = "_".join(data_name.split("_")[0:-1])
93
+ data_path = os.path.join(
94
+ self.data_root, data_shape, self.data_list[data_idx] + ".txt"
95
+ )
96
+ data = np.loadtxt(data_path, delimiter=",").astype(np.float32)
97
+ if self.num_point is not None:
98
+ if self.uniform_sampling:
99
+ with torch.no_grad():
100
+ mask = pointops.farthest_point_sampling(
101
+ torch.tensor(data).float().cuda(),
102
+ torch.tensor([len(data)]).long().cuda(),
103
+ torch.tensor([self.num_point]).long().cuda(),
104
+ )
105
+ data = data[mask.cpu()]
106
+ else:
107
+ data = data[: self.num_point]
108
+ coord, normal = data[:, 0:3], data[:, 3:6]
109
+ category = np.array([self.class_names[data_shape]])
110
+ if self.if_color:
111
+ color = np.zeros_like(coord)
112
+ return dict(coord=coord, color=color, normal=normal, category=category)
113
+ return dict(coord=coord, normal=normal, category=category)
114
+
115
+ def get_data_list(self):
116
+ assert isinstance(self.split, str)
117
+ split_path = os.path.join(
118
+ self.data_root, "modelnet40_{}.txt".format(self.split)
119
+ )
120
+ data_list = np.loadtxt(split_path, dtype="str")
121
+ return data_list
122
+
123
+ def get_data_name(self, idx):
124
+ data_idx = idx % len(self.data_list)
125
+ return self.data_list[data_idx]
126
+
127
+ def __getitem__(self, idx):
128
+ if self.test_mode:
129
+ return self.prepare_test_data(idx)
130
+ else:
131
+ return self.prepare_train_data(idx)
132
+
133
+ def __len__(self):
134
+ return len(self.data_list) * self.loop
135
+
136
+ def prepare_train_data(self, idx):
137
+ data_dict = self.get_data(idx)
138
+ data_dict = self.transform(data_dict)
139
+ return data_dict
140
+
141
+ def prepare_test_data(self, idx):
142
+ assert idx < len(self.data_list)
143
+ data_dict = self.get_data(idx)
144
+ category = data_dict.pop("category")
145
+ data_dict = self.transform(data_dict)
146
+ data_dict_list = []
147
+ for aug in self.aug_transform:
148
+ data_dict_list.append(aug(copy.deepcopy(data_dict)))
149
+ for i in range(len(data_dict_list)):
150
+ data_dict_list[i] = self.post_transform(data_dict_list[i])
151
+ data_dict = dict(
152
+ voting_list=data_dict_list,
153
+ category=category,
154
+ name=self.get_data_name(idx),
155
+ )
156
+ return data_dict
pointcept/datasets/nuscenes.py ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ nuScenes Dataset
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Zheng Zhang
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+ import numpy as np
10
+ from collections.abc import Sequence
11
+ import pickle
12
+ from PIL import Image
13
+ import open3d as o3d
14
+ import torch
15
+
16
+ from .builder import DATASETS
17
+ from .defaults import DefaultDataset, DefaultImagePointDataset
18
+
19
+ os.environ["OMP_NUM_THREADS"] = "1"
20
+
21
+
22
+ @DATASETS.register_module()
23
+ class NuScenesDataset(DefaultDataset):
24
+ def __init__(self, sweeps=10, ignore_index=-1, **kwargs):
25
+ self.sweeps = sweeps
26
+ self.ignore_index = ignore_index
27
+ self.learning_map = self.get_learning_map(ignore_index)
28
+ super().__init__(ignore_index=ignore_index, **kwargs)
29
+
30
+ def get_info_path(self, split):
31
+ assert split in ["train", "val", "test"]
32
+ if split == "train":
33
+ return os.path.join(
34
+ self.data_root, "info", f"nuscenes_infos_{self.sweeps}sweeps_train.pkl"
35
+ )
36
+ elif split == "val":
37
+ return os.path.join(
38
+ self.data_root, "info", f"nuscenes_infos_{self.sweeps}sweeps_val.pkl"
39
+ )
40
+ elif split == "test":
41
+ return os.path.join(
42
+ self.data_root, "info", f"nuscenes_infos_{self.sweeps}sweeps_test.pkl"
43
+ )
44
+ else:
45
+ raise NotImplementedError
46
+
47
+ def get_data_list(self):
48
+ if isinstance(self.split, str):
49
+ info_paths = [self.get_info_path(self.split)]
50
+ elif isinstance(self.split, Sequence):
51
+ info_paths = [self.get_info_path(s) for s in self.split]
52
+ else:
53
+ raise NotImplementedError
54
+ data_list = []
55
+ for info_path in info_paths:
56
+ with open(info_path, "rb") as f:
57
+ info = pickle.load(f)
58
+ data_list.extend(info)
59
+ return data_list
60
+
61
+ def get_data(self, idx):
62
+ data = self.data_list[idx % len(self.data_list)]
63
+ lidar_path = os.path.join(self.data_root, "raw", data["lidar_path"])
64
+ points = np.fromfile(str(lidar_path), dtype=np.float32, count=-1).reshape(
65
+ [-1, 5]
66
+ )
67
+ coord = points[:, :3]
68
+ strength = points[:, 3].reshape([-1, 1]) / 255 # scale strength to [0, 1]
69
+
70
+ if "gt_segment_path" in data.keys():
71
+ gt_segment_path = os.path.join(
72
+ self.data_root, "raw", data["gt_segment_path"]
73
+ )
74
+ segment = np.fromfile(
75
+ str(gt_segment_path), dtype=np.uint8, count=-1
76
+ ).reshape([-1])
77
+ segment = np.vectorize(self.learning_map.__getitem__)(segment).astype(
78
+ np.int64
79
+ )
80
+ else:
81
+ segment = np.ones((points.shape[0],), dtype=np.int64) * self.ignore_index
82
+ data_dict = dict(
83
+ coord=coord,
84
+ strength=strength,
85
+ segment=segment,
86
+ name=self.get_data_name(idx),
87
+ )
88
+ return data_dict
89
+
90
+ def get_data_name(self, idx):
91
+ # return data name for lidar seg, optimize the code when need to support detection
92
+ return self.data_list[idx % len(self.data_list)]["lidar_token"]
93
+
94
+ @staticmethod
95
+ def get_learning_map(ignore_index):
96
+ learning_map = {
97
+ 0: ignore_index,
98
+ 1: ignore_index,
99
+ 2: 6,
100
+ 3: 6,
101
+ 4: 6,
102
+ 5: ignore_index,
103
+ 6: 6,
104
+ 7: ignore_index,
105
+ 8: ignore_index,
106
+ 9: 0,
107
+ 10: ignore_index,
108
+ 11: ignore_index,
109
+ 12: 7,
110
+ 13: ignore_index,
111
+ 14: 1,
112
+ 15: 2,
113
+ 16: 2,
114
+ 17: 3,
115
+ 18: 4,
116
+ 19: ignore_index,
117
+ 20: ignore_index,
118
+ 21: 5,
119
+ 22: 8,
120
+ 23: 9,
121
+ 24: 10,
122
+ 25: 11,
123
+ 26: 12,
124
+ 27: 13,
125
+ 28: 14,
126
+ 29: ignore_index,
127
+ 30: 15,
128
+ 31: ignore_index,
129
+ }
130
+ return learning_map
131
+
132
+
133
+ @DATASETS.register_module()
134
+ class NuScenesColorNormalDataset(NuScenesDataset):
135
+ @staticmethod
136
+ def estimate_normals(points, center=np.array([0, 0, 0])):
137
+ normals = points - center[None, :]
138
+ norms = np.linalg.norm(normals, axis=1, keepdims=True)
139
+ normals = normals / norms
140
+ return normals
141
+
142
+ def get_data(self, idx):
143
+ data = self.data_list[idx % len(self.data_list)]
144
+ lidar_path = os.path.join(self.data_root, "raw", data["lidar_path"])
145
+ points = np.fromfile(str(lidar_path), dtype=np.float32, count=-1).reshape(
146
+ [-1, 5]
147
+ )
148
+ coord = points[:, :3]
149
+ normal = self.estimate_normals(coord)
150
+ if "gt_segment_path" in data.keys():
151
+ gt_segment_path = os.path.join(
152
+ self.data_root, "raw", data["gt_segment_path"]
153
+ )
154
+ segment = np.fromfile(
155
+ str(gt_segment_path), dtype=np.uint8, count=-1
156
+ ).reshape([-1])
157
+ segment = np.vectorize(self.learning_map.__getitem__)(segment).astype(
158
+ np.int64
159
+ )
160
+ else:
161
+ segment = np.ones((points.shape[0],), dtype=np.int64) * self.ignore_index
162
+ data_dict = dict(
163
+ coord=coord,
164
+ color=np.zeros_like(coord), # placeholder for color
165
+ normal=np.zeros_like(coord), # placeholder for normal
166
+ segment=segment,
167
+ name=self.get_data_name(idx),
168
+ )
169
+ return data_dict
170
+
171
+
172
+ @DATASETS.register_module()
173
+ class NuScenesImagePointDataset(DefaultImagePointDataset):
174
+ CAMERA_TYPES = [
175
+ "CAM_FRONT",
176
+ "CAM_FRONT_RIGHT",
177
+ "CAM_FRONT_LEFT",
178
+ "CAM_BACK",
179
+ "CAM_BACK_LEFT",
180
+ "CAM_BACK_RIGHT",
181
+ ]
182
+
183
+ def __init__(
184
+ self,
185
+ if_img=False,
186
+ if_sweep=False,
187
+ sweeps_max=10,
188
+ sweeps=10,
189
+ sweep_gap=1,
190
+ ignore_index=-1,
191
+ img_num=4,
192
+ **kwargs,
193
+ ):
194
+ self.sweeps = sweeps
195
+ self.sweep_gap = sweep_gap
196
+ self.sweeps_max = sweeps_max
197
+ self.if_sweep = if_sweep
198
+ self.if_img = if_img
199
+ self.ignore_index = ignore_index
200
+ self.learning_map = self.get_learning_map(ignore_index)
201
+ frame_count = sweeps + 1 if if_sweep else 1
202
+ self.img_ratio = img_num / (6 * frame_count)
203
+ super().__init__(ignore_index=ignore_index, if_img=if_img, **kwargs)
204
+
205
+ @staticmethod
206
+ def project_lidar_to_image_with_color(
207
+ lidar_points, # shape: (N, 3) or (N, 4)
208
+ image, # shape: (H, W, 3), uint8 RGB
209
+ cam_intrinsic, # shape: (3, 3)
210
+ lidar_to_cam, # shape: (4, 4)
211
+ lidar_colors,
212
+ ):
213
+ """
214
+ Projects LiDAR points to the image, fetches pixel color and pixel coordinates.
215
+ Returns:
216
+ filtered_points: (M, 3) - 3D points in camera frame that project onto the image.
217
+ colors: (M, 3) - RGB colors at projected 2D locations.
218
+ uv_coords: (M, 2) - Integer pixel coordinates (u, v) on the image.
219
+ mask: (N,) - (optional) Boolean mask indicating which lidar points are used.
220
+ """
221
+ lidar_uv_coords = np.full(
222
+ (lidar_points.shape[0], 2), -1, dtype=int
223
+ ) # Default to (-1, -1)
224
+ lidar_points_coord = lidar_points[:, :3]
225
+ ones = np.ones((lidar_points_coord.shape[0], 1))
226
+ lidar_hom = np.concatenate([lidar_points_coord, ones], axis=1) # (N, 4)
227
+
228
+ points_cam = (lidar_to_cam @ lidar_hom.T).T # (N, 4)
229
+
230
+ valid = points_cam[:, 2] > 0
231
+ points_cam = points_cam[valid]
232
+
233
+ pts_2d = (cam_intrinsic @ points_cam[:, :3].T).T # (N, 3)
234
+ pts_2d = pts_2d[:, :2] / pts_2d[:, 2:3] # (N, 2) - pixel (u, v)
235
+
236
+ H, W = image.shape[:2]
237
+ u, v = pts_2d[:, 0], pts_2d[:, 1]
238
+ inside = (u >= 0) & (u < W) & (v >= 0) & (v < H)
239
+
240
+ u = u[inside].astype(int)
241
+ v = v[inside].astype(int)
242
+
243
+ mask = np.zeros(lidar_points.shape[0], dtype=bool)
244
+ mask[np.where(valid)[0][inside]] = True
245
+
246
+ lidar_colors[mask] = image[v, u, :]
247
+ lidar_uv_coords[mask] = np.stack([u, v], axis=1) # (M, 2)
248
+ return lidar_colors, lidar_uv_coords, mask
249
+
250
+ @staticmethod
251
+ def transform_normal(normal, transform):
252
+ return (transform[:3, :3] @ normal.T).T
253
+
254
+ def get_info_path(self, split):
255
+ assert split in ["train", "val", "test"]
256
+ if split == "train":
257
+ return os.path.join(
258
+ self.data_root,
259
+ "info",
260
+ f"nuscenes_infos_{self.sweeps_max}sweeps_train.pkl",
261
+ )
262
+ elif split == "val":
263
+ return os.path.join(
264
+ self.data_root,
265
+ "info",
266
+ f"nuscenes_infos_{self.sweeps_max}sweeps_val.pkl",
267
+ )
268
+ elif split == "test":
269
+ return os.path.join(
270
+ self.data_root,
271
+ "info",
272
+ f"nuscenes_infos_{self.sweeps_max}sweeps_test.pkl",
273
+ )
274
+ else:
275
+ raise NotImplementedError
276
+
277
+ def get_data_list(self):
278
+ split_list = {}
279
+ if isinstance(self.split, str):
280
+ info_paths = [self.get_info_path(self.split)]
281
+ split = [self.split]
282
+ elif isinstance(self.split, Sequence):
283
+ split = self.split
284
+ info_paths = [self.get_info_path(s) for s in self.split]
285
+ else:
286
+ raise NotImplementedError
287
+ data_list = []
288
+ for info_path, split_i in zip(info_paths, split):
289
+ with open(info_path, "rb") as f:
290
+ info = pickle.load(f)
291
+ data_list.extend(info)
292
+ split_list[split_i] = list([i["token"] for i in info])
293
+ return data_list, split_list
294
+
295
+ def get_data(self, idx):
296
+ data = self.data_list[idx % len(self.data_list)]
297
+ lidar_path = os.path.join(self.data_root, "raw", data["lidar_path"])
298
+ points = np.fromfile(str(lidar_path), dtype=np.float32, count=-1).reshape(
299
+ [-1, 5]
300
+ )
301
+ keyframe_point_count = points.shape[0]
302
+
303
+ imgs = []
304
+ cam_coords = []
305
+ cam_normals = []
306
+ cam_colors = []
307
+ cam_strengths = []
308
+ cam_correspondences = []
309
+ correspondence_start = 0
310
+ frame_pcd_offset = []
311
+ lidar_colors = np.zeros((points.shape[0], 3), dtype=int) # Default to black
312
+ for id, cam_name in enumerate(self.CAMERA_TYPES):
313
+ cam_info = data["cams"][cam_name]
314
+ cam_intrinsic = cam_info["camera_intrinsics"]
315
+ cam_image = Image.open(
316
+ os.path.join(self.data_root, "raw", data["cams"][cam_name]["data_path"])
317
+ )
318
+ cam_image_np = np.array(cam_image)
319
+ sensor2lidar = np.eye(4)
320
+ sensor2lidar[:3, :3] = cam_info["sensor2lidar_rotation"]
321
+ sensor2lidar[:3, 3] = cam_info["sensor2lidar_translation"]
322
+ lidar2sensor = np.linalg.inv(sensor2lidar)
323
+ lidar_colors, correspondence_info, _ = (
324
+ self.project_lidar_to_image_with_color(
325
+ points, cam_image_np, cam_intrinsic, lidar2sensor, lidar_colors
326
+ )
327
+ )
328
+ correspondence_point_id = (
329
+ np.array(range(correspondence_info.shape[0])).reshape((-1, 1))
330
+ + correspondence_start
331
+ )
332
+ correspondence_info = np.hstack(
333
+ [correspondence_info, correspondence_point_id]
334
+ )
335
+ if np.random.rand() < self.img_ratio:
336
+ cam_correspondences.append(correspondence_info)
337
+ imgs.append(cam_image)
338
+ correspondence_start += points.shape[0]
339
+ cam_coord = points[:, :3]
340
+ cam_center = np.array([0, 0, 0])
341
+ cam_normal = self.get_normals(cam_center, cam_coord)
342
+ cam_normals.append(cam_normal)
343
+ cam_strength = points[:, 3].reshape([-1, 1]) / 255
344
+ cam_coords.append(cam_coord)
345
+ cam_colors.append(lidar_colors)
346
+ cam_strengths.append(cam_strength)
347
+
348
+ if self.if_sweep:
349
+ frame_pcd_offset.append(points.shape[0])
350
+ for id, sweep in enumerate(
351
+ data["sweeps"][: (self.sweep_gap * self.sweeps) : self.sweep_gap]
352
+ ):
353
+ lidar_path = os.path.join(self.data_root, "raw", sweep["lidar_path"])
354
+ points = np.fromfile(
355
+ str(lidar_path), dtype=np.float32, count=-1
356
+ ).reshape([-1, 5])
357
+ lidar_colors = np.zeros(
358
+ (points.shape[0], 3), dtype=int
359
+ ) # Default to black
360
+ cam_lidar_tm = (
361
+ sweep["transform_matrix"]
362
+ if sweep["transform_matrix"] is not None
363
+ else np.eye(4)
364
+ )
365
+ for id, cam_name in enumerate(self.CAMERA_TYPES):
366
+ cam_info = sweep["cams"][cam_name]
367
+ # cam_image_np = np.array(imgs[id])
368
+ cam_intrinsic = cam_info["camera_intrinsics"]
369
+ cam_image = Image.open(
370
+ os.path.join(self.data_root, "raw", cam_info["data_path"])
371
+ )
372
+ cam_image_np = np.array(cam_image)
373
+ sensor2lidar = np.eye(4)
374
+ sensor2lidar[:3, :3] = cam_info["sensor2lidar_rotation"]
375
+ sensor2lidar[:3, 3] = cam_info["sensor2lidar_translation"]
376
+ lidar2sensor = np.linalg.inv(sensor2lidar) @ cam_lidar_tm
377
+ lidar_colors, correspondence_info, _ = (
378
+ self.project_lidar_to_image_with_color(
379
+ points,
380
+ cam_image_np,
381
+ cam_intrinsic,
382
+ lidar2sensor,
383
+ lidar_colors,
384
+ )
385
+ )
386
+ correspondence_point_id = (
387
+ np.array(range(correspondence_info.shape[0])).reshape((-1, 1))
388
+ + correspondence_start
389
+ )
390
+ correspondence_info = np.hstack(
391
+ [correspondence_info, correspondence_point_id]
392
+ )
393
+ if np.random.rand() < self.img_ratio:
394
+ cam_correspondences.append(correspondence_info)
395
+ imgs.append(cam_image)
396
+ correspondence_start += correspondence_info.shape[0]
397
+ frame_pcd_offset.append(correspondence_start)
398
+ cam_coord = points[:, :3]
399
+ cam_center = np.array([0, 0, 0])
400
+ cam_normal = self.get_normals(cam_center, cam_coord)
401
+ cam_normal = self.transform_normal(cam_normal, cam_lidar_tm)
402
+ cam_normals.append(cam_normal)
403
+ ones = np.ones((points.shape[0], 1))
404
+ cam_coord_hom = np.concatenate([cam_coord, ones], axis=1) # (N, 4)
405
+ cam_coord = cam_coord_hom @ cam_lidar_tm.T
406
+ cam_coord = cam_coord[:, :3]
407
+ cam_strength = points[:, 3].reshape([-1, 1]) / 255
408
+ cam_coords.append(cam_coord)
409
+ cam_colors.append(lidar_colors)
410
+ cam_strengths.append(cam_strength)
411
+
412
+ coord = np.vstack(cam_coords)
413
+ color = np.vstack(cam_colors)
414
+ normal = np.vstack(cam_normals)
415
+ strength = np.vstack(cam_strengths)
416
+ frame_pcd_offset = np.array(frame_pcd_offset)
417
+
418
+ car_from_ref = np.linalg.inv(data["ref_from_car"])
419
+ coord_homo = np.hstack((coord, np.ones((coord.shape[0], 1))))
420
+ coord_homo = coord_homo @ car_from_ref.T
421
+ coord = coord_homo[:, :3]
422
+ normal = self.transform_normal(normal, car_from_ref)
423
+
424
+ img_assets = dict()
425
+ if self.if_img:
426
+ if len(imgs) > 0:
427
+ img_width, img_height = imgs[0].size
428
+ div_w = img_width // self.patch_w
429
+ div_h = img_height // self.patch_h
430
+ div_min = max(min(div_w, div_h), 1)
431
+ crop_img_width = div_min * self.patch_w
432
+ crop_img_height = div_min * self.patch_h
433
+ left = int((img_width - crop_img_width) / 2)
434
+ top = int((img_height - crop_img_height) / 2)
435
+ right = int((img_width + crop_img_width) / 2)
436
+ bottom = int((img_height + crop_img_height) / 2)
437
+ imgs = [img.crop((left, top, right, bottom)) for img in imgs]
438
+ imgs = [self.transform_img(img) for img in imgs]
439
+ imgs_list = torch.stack(imgs)
440
+ img_assets["images"] = imgs_list.float()
441
+ else:
442
+ img_assets["images"] = torch.empty(
443
+ (
444
+ 0,
445
+ 3,
446
+ self.patch_h * self.patch_size,
447
+ self.patch_w * self.patch_size,
448
+ )
449
+ )
450
+ img_assets["img_num"] = np.array(
451
+ [img_assets["images"].shape[0]], dtype=np.int32
452
+ )
453
+
454
+ correspondence_infos = np.ones(
455
+ (coord.shape[0], len(cam_correspondences), 2), dtype=np.int32
456
+ ) * (-1)
457
+ for id, correspondence_info in enumerate(cam_correspondences):
458
+ correspondence_info = self.resize_correspondence_info(
459
+ correspondence_info,
460
+ (self.patch_h * self.patch_size, self.patch_w * self.patch_size),
461
+ (img_height, img_width),
462
+ (left, top, right, bottom),
463
+ self.patch_size,
464
+ )
465
+ correspondence_infos[correspondence_info[:, -1], id, :] = (
466
+ correspondence_info[:, :-1]
467
+ )
468
+ img_assets["correspondence"] = correspondence_infos
469
+ if "gt_segment_path" in data.keys():
470
+ gt_segment_path = os.path.join(
471
+ self.data_root, "raw", data["gt_segment_path"]
472
+ )
473
+ segment = np.fromfile(
474
+ str(gt_segment_path), dtype=np.uint8, count=-1
475
+ ).reshape([-1])
476
+ segment = np.vectorize(self.learning_map.__getitem__)(segment).astype(
477
+ np.int64
478
+ )
479
+ else:
480
+ segment = (
481
+ np.ones((keyframe_point_count,), dtype=np.int64) * self.ignore_index
482
+ )
483
+
484
+ if segment.shape[0] != keyframe_point_count:
485
+ raise ValueError(
486
+ f"nuScenes sample {self.get_data_name(idx)} has "
487
+ f"{keyframe_point_count} keyframe points but "
488
+ f"{segment.shape[0]} segment labels."
489
+ )
490
+
491
+ if self.if_sweep:
492
+ sweep_point_count = coord.shape[0] - keyframe_point_count
493
+ sweep_segment = np.full(
494
+ (sweep_point_count,), self.ignore_index, dtype=segment.dtype
495
+ )
496
+ segment = np.concatenate((segment, sweep_segment), axis=0)
497
+
498
+ color = color.astype(np.float32)
499
+ if self.if_sweep:
500
+ data_dict = dict(
501
+ coord=coord,
502
+ color=color,
503
+ normal=normal,
504
+ strength=strength,
505
+ segment=segment,
506
+ frame_pcd_offset=frame_pcd_offset,
507
+ name=self.get_data_name(idx),
508
+ )
509
+ else:
510
+ data_dict = dict(
511
+ coord=coord,
512
+ color=color,
513
+ normal=normal,
514
+ strength=strength,
515
+ segment=segment,
516
+ name=self.get_data_name(idx),
517
+ )
518
+ data_dict.update(img_assets)
519
+ return data_dict
520
+
521
+ def get_data_name(self, idx):
522
+ return self.data_list[idx % len(self.data_list)]["lidar_token"]
523
+
524
+ @staticmethod
525
+ def get_normals(cam_center, coords):
526
+ Cs = np.repeat(cam_center.reshape((1, -1)), coords.shape[0], axis=0)
527
+ view_dirs = coords - Cs
528
+ view_dirs = view_dirs / np.linalg.norm(view_dirs, axis=-1, keepdims=True)
529
+ pcd = o3d.geometry.PointCloud()
530
+ pcd.points = o3d.utility.Vector3dVector(coords)
531
+ pcd.estimate_normals()
532
+ normals = np.asarray(pcd.normals)
533
+ dot_product = np.sum(normals * view_dirs, axis=-1)
534
+ flip_mask = dot_product > 0
535
+ normals[flip_mask] = -normals[flip_mask]
536
+ normals = normals / np.linalg.norm(normals, axis=-1, keepdims=True)
537
+ return normals
538
+
539
+ # or using the vector from LiDAR to points, which is faster for data loading
540
+ # @staticmethod
541
+ # def get_normals(center, points):
542
+ # normals = points - center[None, :]
543
+ # norms = np.linalg.norm(normals, axis=1, keepdims=True)
544
+ # normals = normals / norms
545
+ # return normals
546
+
547
+ @staticmethod
548
+ def get_learning_map(ignore_index):
549
+ learning_map = {
550
+ 0: ignore_index,
551
+ 1: ignore_index,
552
+ 2: 6,
553
+ 3: 6,
554
+ 4: 6,
555
+ 5: ignore_index,
556
+ 6: 6,
557
+ 7: ignore_index,
558
+ 8: ignore_index,
559
+ 9: 0,
560
+ 10: ignore_index,
561
+ 11: ignore_index,
562
+ 12: 7,
563
+ 13: ignore_index,
564
+ 14: 1,
565
+ 15: 2,
566
+ 16: 2,
567
+ 17: 3,
568
+ 18: 4,
569
+ 19: ignore_index,
570
+ 20: ignore_index,
571
+ 21: 5,
572
+ 22: 8,
573
+ 23: 9,
574
+ 24: 10,
575
+ 25: 11,
576
+ 26: 12,
577
+ 27: 13,
578
+ 28: 14,
579
+ 29: ignore_index,
580
+ 30: 15,
581
+ 31: ignore_index,
582
+ }
583
+ return learning_map
pointcept/datasets/partnet.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HK Remote Data Dataset
3
+
4
+ Author: Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+ import numpy as np
10
+ import open3d as o3d
11
+ import torch
12
+
13
+ from .defaults import DefaultDataset
14
+ from .builder import DATASETS
15
+ from pointcept.utils.cache import shared_dict
16
+
17
+
18
+ @DATASETS.register_module()
19
+ class PartNetDataDataset(DefaultDataset):
20
+ def __init__(self, if_img=True, crop_h=630, crop_w=1120, patch_size=14, **kwargs):
21
+ self.crop_h = crop_h
22
+ self.crop_w = crop_w
23
+ self.patch_size = patch_size
24
+ self.patch_h = crop_h // patch_size
25
+ self.patch_w = crop_w // patch_size
26
+ self.if_img = if_img
27
+ super().__init__(**kwargs)
28
+
29
+ def get_data(self, idx):
30
+ data_path = self.data_list[idx % len(self.data_list)]
31
+ pc_data_path = os.path.join(
32
+ data_path, "point_sample", "sample-points-all-pts-nor-rgba-10000.ply"
33
+ )
34
+ name = self.get_data_name(idx)
35
+ split = self.get_split_name(idx)
36
+ if self.cache:
37
+ cache_name = f"pointcept-{name}"
38
+ return shared_dict(cache_name)
39
+
40
+ data_dict = {}
41
+ pc = o3d.io.read_point_cloud(pc_data_path)
42
+ data_dict["coord"] = np.asarray(pc.points)
43
+ data_dict["color"] = np.asarray(pc.colors)
44
+ data_dict["normal"] = np.asarray(pc.normals)
45
+ data_dict["name"] = name
46
+ data_dict["split"] = split
47
+
48
+ if self.if_img:
49
+ data_dict["images"] = torch.empty(
50
+ (0, 3, self.patch_h * self.patch_size, self.patch_w * self.patch_size)
51
+ )
52
+ data_dict["img_num"] = np.array([0], dtype=np.int32)
53
+ data_dict["correspondence"] = np.ones(
54
+ (data_dict["coord"].shape[0], 0, 2), dtype=np.float32
55
+ ) * (-1)
56
+
57
+ if "coord" in data_dict.keys():
58
+ data_dict["coord"] = data_dict["coord"].astype(np.float32)
59
+
60
+ if "color" in data_dict.keys():
61
+ data_dict["color"] = data_dict["color"].astype(np.float32)
62
+
63
+ if "normal" in data_dict.keys():
64
+ data_dict["normal"] = data_dict["normal"].astype(np.float32)
65
+
66
+ if "segment" in data_dict.keys():
67
+ data_dict["segment"] = data_dict["segment"].reshape([-1]).astype(np.int32)
68
+ else:
69
+ data_dict["segment"] = (
70
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
71
+ )
72
+
73
+ if "instance" in data_dict.keys():
74
+ data_dict["instance"] = data_dict["instance"].reshape([-1]).astype(np.int32)
75
+ else:
76
+ data_dict["instance"] = (
77
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
78
+ )
79
+ return data_dict
pointcept/datasets/partnete.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PartNetE Datasets
3
+
4
+ Author: Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+ import glob
10
+ from collections.abc import Sequence
11
+ import numpy as np
12
+ import json
13
+
14
+ from .defaults import DefaultDataset
15
+ from .builder import DATASETS
16
+
17
+ from pointcept.utils.cache import shared_dict
18
+
19
+
20
+ @DATASETS.register_module()
21
+ class PartNetEDataset(DefaultDataset):
22
+ def __init__(
23
+ self, class_names, num_parts, data_root, meta_path, split, *args, **kwargs
24
+ ):
25
+ self.categories = class_names
26
+ self.num_parts = num_parts
27
+ self.num_part_offset = np.concatenate(([0], np.cumsum(self.num_parts)))
28
+ with open(meta_path, "r", encoding="utf-8") as file:
29
+ meta_data = json.load(file)
30
+ self.category2part = {}
31
+ self.parts = []
32
+ for class_id, class_name in enumerate(self.categories):
33
+ self.category2part[class_name] = self.num_part_offset[class_id] + list(
34
+ range(self.num_parts[class_id])
35
+ )
36
+ category_part_name = [
37
+ class_name + "_" + part_name
38
+ for part_name in ["other"] + meta_data[class_name]
39
+ ]
40
+ self.parts.extend(category_part_name)
41
+ super().__init__(data_root=data_root, split=split, *args, **kwargs)
42
+
43
+ def get_data_list(self):
44
+ if isinstance(self.split, str):
45
+ data_list = glob.glob(os.path.join(self.data_root, self.split, "*/*"))
46
+ elif isinstance(self.split, Sequence):
47
+ data_list = []
48
+ for split in self.split:
49
+ data_list += glob.glob(os.path.join(self.data_root, split, "*/*"))
50
+ else:
51
+ raise NotImplementedError
52
+ return data_list
53
+
54
+ def get_data_name(self, idx):
55
+ file_path = self.data_list[idx % len(self.data_list)]
56
+ dir_path, id_name = os.path.split(file_path)
57
+ object_name = os.path.basename(dir_path)
58
+ data_name = f"{object_name}_{id_name}"
59
+ return data_name
60
+
61
+ def get_data(self, idx):
62
+ data_path = self.data_list[idx % len(self.data_list)]
63
+ name = self.get_data_name(idx)
64
+ split = self.get_split_name(idx)
65
+ if self.cache:
66
+ cache_name = f"pointcept-{name}"
67
+ return shared_dict(cache_name)
68
+
69
+ data_dict = {}
70
+ assets = os.listdir(data_path)
71
+ for asset in assets:
72
+ if not asset.endswith(".npy"):
73
+ continue
74
+ if asset[:-4] not in self.VALID_ASSETS:
75
+ continue
76
+ data_dict[asset[:-4]] = np.load(os.path.join(data_path, asset))
77
+ data_dict["name"] = name
78
+ data_dict["split"] = split
79
+
80
+ object_name = name.split("_")[0]
81
+ cls_token = self.categories.index(object_name)
82
+ data_dict["cls_token"] = cls_token
83
+
84
+ if "coord" in data_dict.keys():
85
+ data_dict["coord"] = data_dict["coord"].astype(np.float32)
86
+
87
+ if "color" in data_dict.keys():
88
+ data_dict["color"] = data_dict["color"].astype(np.float32)
89
+
90
+ if "normal" in data_dict.keys():
91
+ data_dict["normal"] = data_dict["normal"].astype(np.float32)
92
+
93
+ if "segment" in data_dict.keys():
94
+ data_dict["segment"] = (
95
+ data_dict["segment"].reshape([-1]).astype(np.int32)
96
+ + self.num_part_offset[cls_token]
97
+ + 1
98
+ )
99
+ else:
100
+ data_dict["segment"] = (
101
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
102
+ )
103
+
104
+ if "instance" in data_dict.keys():
105
+ data_dict["instance"] = data_dict["instance"].reshape([-1]).astype(np.int32)
106
+ else:
107
+ data_dict["instance"] = (
108
+ np.ones(data_dict["coord"].shape[0], dtype=np.int32) * -1
109
+ )
110
+
111
+ return data_dict
pointcept/datasets/preprocessing/arkitscenes/preprocess_arkitscenes_mesh.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing ArkitScenes
3
+ """
4
+
5
+ import os
6
+ import argparse
7
+ import glob
8
+ import plyfile
9
+ import numpy as np
10
+ import pandas as pd
11
+ import multiprocessing as mp
12
+ from concurrent.futures import ProcessPoolExecutor
13
+ from itertools import repeat
14
+ from pathlib import Path
15
+
16
+
17
+ def read_plymesh(filepath):
18
+ """Read ply file and return it as numpy array. Returns None if emtpy."""
19
+ with open(filepath, "rb") as f:
20
+ plydata = plyfile.PlyData.read(f)
21
+ if plydata.elements:
22
+ vertices = pd.DataFrame(plydata["vertex"].data).values
23
+ faces = np.stack(plydata["face"].data["vertex_indices"], axis=0)
24
+ return vertices, faces
25
+
26
+
27
+ def face_normal(vertex, face):
28
+ v01 = vertex[face[:, 1]] - vertex[face[:, 0]]
29
+ v02 = vertex[face[:, 2]] - vertex[face[:, 0]]
30
+ vec = np.cross(v01, v02)
31
+ length = np.sqrt(np.sum(vec**2, axis=1, keepdims=True)) + 1.0e-8
32
+ nf = vec / length
33
+ area = length * 0.5
34
+ return nf, area
35
+
36
+
37
+ def vertex_normal(vertex, face):
38
+ nf, area = face_normal(vertex, face)
39
+ nf = nf * area
40
+
41
+ nv = np.zeros_like(vertex)
42
+ for i in range(face.shape[0]):
43
+ nv[face[i]] += nf[i]
44
+
45
+ length = np.sqrt(np.sum(nv**2, axis=1, keepdims=True)) + 1.0e-8
46
+ nv = nv / length
47
+ return nv
48
+
49
+
50
+ def parse_scene(scene_path, output_dir):
51
+ print(f"Parsing scene {scene_path}")
52
+ split = os.path.basename(os.path.dirname(os.path.dirname(scene_path)))
53
+ scene_id = os.path.basename(os.path.dirname(scene_path))
54
+ vertices, faces = read_plymesh(scene_path)
55
+ coords = vertices[:, :3]
56
+ colors = vertices[:, 3:6]
57
+ normals = vertex_normal(coords, faces)
58
+ data_dict = dict(coord=coords, color=colors, normal=normals)
59
+ os.makedirs(output_dir / split / scene_id, exist_ok=True)
60
+ for key in data_dict.keys():
61
+ np.save(output_dir / split / scene_id / f"{key}.npy", data_dict[key])
62
+
63
+
64
+ if __name__ == "__main__":
65
+ parser = argparse.ArgumentParser()
66
+ parser.add_argument(
67
+ "--dataset_root",
68
+ required=True,
69
+ help="Path to the ArkitScenes dataset containing 3dod folder",
70
+ )
71
+ parser.add_argument(
72
+ "--output_root",
73
+ required=True,
74
+ help="Output path where train/val folders will be located",
75
+ )
76
+ parser.add_argument(
77
+ "--num_workers",
78
+ default=mp.cpu_count(),
79
+ type=int,
80
+ help="Num workers for preprocessing.",
81
+ )
82
+ opt = parser.parse_args()
83
+ # Create output directories
84
+ train_output_dir = os.path.join(opt.output_root, "Training")
85
+ os.makedirs(train_output_dir, exist_ok=True)
86
+ val_output_dir = os.path.join(opt.output_root, "Validation")
87
+ os.makedirs(val_output_dir, exist_ok=True)
88
+ # Load scene paths
89
+ scene_paths = sorted(glob.glob(opt.dataset_root + "/3dod/*/*/*_mesh.ply"))
90
+ # Preprocess data.
91
+ pool = ProcessPoolExecutor(max_workers=opt.num_workers)
92
+ print("Processing scenes...")
93
+ _ = list(pool.map(parse_scene, scene_paths, repeat(Path(opt.output_root))))
pointcept/datasets/preprocessing/concerto/arkitscenes/preprocess_arkitscenes.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for ARKitScenes
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ from pathlib import Path
9
+ import argparse
10
+ import os
11
+ import pandas as pd
12
+ import camtools as ct
13
+ import numpy as np
14
+ from scipy.spatial import cKDTree
15
+ import shutil
16
+ import open3d as o3d
17
+ import cv2
18
+ import glob
19
+ import numpy as np
20
+ import os
21
+ import plyfile
22
+ import multiprocessing as mp
23
+ from rotation import convert_angle_axis_to_matrix3
24
+
25
+
26
+ def correspondenceGet(mesh, K, T, img_size, coords_gt):
27
+ height, width = img_size
28
+ if np.isnan(T).any() or np.isinf(T).any():
29
+ return None
30
+ depth = ct.raycast.mesh_to_im_depth(
31
+ mesh=mesh, K=K, T=np.linalg.inv(T), height=height, width=width
32
+ )
33
+ pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
34
+ pixel = pixel.reshape((-1, 2))
35
+ pixel = np.hstack((pixel, np.ones((pixel.shape[0], 1))))
36
+ depth = depth.reshape((-1, 1))
37
+ valid = ~np.isinf(depth).squeeze(-1)
38
+ coord = np.zeros_like(pixel, dtype=np.float32)
39
+ coord[valid] = depth[valid] * (np.linalg.inv(K) @ pixel[valid].T).T # coord_camera
40
+ coord[valid] = coord[valid] @ T[:3, :3].T + T[:3, 3] # column then row
41
+ pixel = pixel[valid]
42
+ coord = coord[valid]
43
+ if coord.shape[0] == 0:
44
+ return None
45
+ pixel = pixel[:, :2]
46
+ coord_dict = {"pixel": pixel, "coord": coord}
47
+ return coord_dict
48
+
49
+
50
+ def correspondenceSave(mesh, scene_dir, coords_gt, output_dir, img_size, Ks, Ts):
51
+ os.makedirs(output_dir, exist_ok=True)
52
+ scene_dir = Path(scene_dir)
53
+ index_gt = [
54
+ img_name[:-4].split("_")[1]
55
+ for img_name in os.listdir(str(scene_dir / "color"))
56
+ if img_name.endswith(".png")
57
+ ]
58
+ index_gt = sorted(index_gt, key=lambda x: float(x))
59
+
60
+ coords_gt_ = coords_gt
61
+ pixels_ = []
62
+ coords_ = []
63
+
64
+ for i, (K, T) in enumerate(zip(Ks, Ts)):
65
+ coord_dict = correspondenceGet(mesh, K, T, img_size, coords_gt)
66
+ if coord_dict is None:
67
+ correspondences = -np.ones((1, 3))
68
+ else:
69
+ pixels_ = coord_dict["pixel"]
70
+ coords_ = coord_dict["coord"]
71
+ tree = cKDTree(coords_gt_)
72
+ dis, idx = tree.query(coords_, k=1)
73
+ idx_valid = idx[dis < 0.01]
74
+ pixels_valid = pixels_[dis < 0.01]
75
+ correspondences = np.hstack((pixels_valid, idx_valid.reshape(-1, 1)))
76
+ np.save(Path(output_dir) / "{}.npy".format(index_gt[i]), correspondences)
77
+
78
+
79
+ def read_plymesh(filepath):
80
+ """Read ply file and return it as numpy array. Returns None if emtpy."""
81
+ with open(filepath, "rb") as f:
82
+ plydata = plyfile.PlyData.read(f)
83
+ if plydata.elements:
84
+ vertices = pd.DataFrame(plydata["vertex"].data).values
85
+ faces = np.stack(plydata["face"].data["vertex_indices"], axis=0)
86
+ return vertices, faces
87
+
88
+
89
+ def face_normal(vertex, face):
90
+ v01 = vertex[face[:, 1]] - vertex[face[:, 0]]
91
+ v02 = vertex[face[:, 2]] - vertex[face[:, 0]]
92
+ vec = np.cross(v01, v02)
93
+ length = np.sqrt(np.sum(vec**2, axis=1, keepdims=True)) + 1.0e-8
94
+ nf = vec / length
95
+ area = length * 0.5
96
+ return nf, area
97
+
98
+
99
+ def vertex_normal(vertex, face):
100
+ nf, area = face_normal(vertex, face)
101
+ nf = nf * area
102
+
103
+ nv = np.zeros_like(vertex)
104
+ for i in range(face.shape[0]):
105
+ nv[face[i]] += nf[i]
106
+
107
+ length = np.sqrt(np.sum(nv**2, axis=1, keepdims=True)) + 1.0e-8
108
+ nv = nv / length
109
+ return nv
110
+
111
+
112
+ def TrajStringToMatrix(traj_str):
113
+ """convert traj_str into translation and rotation matrices
114
+ Args:
115
+ traj_str: A space-delimited file where each line represents a camera position at a particular timestamp.
116
+ The file has seven columns:
117
+ * Column 1: timestamp
118
+ * Columns 2-4: rotation (axis-angle representation in radians)
119
+ * Columns 5-7: translation (usually in meters)
120
+
121
+ Returns:
122
+ ts: translation matrix
123
+ Rt: rotation matrix
124
+ """
125
+ # line=[float(x) for x in traj_str.split()]
126
+ # ts = line[0];
127
+ # R = cv2.Rodrigues(np.array(line[1:4]))[0];
128
+ # t = np.array(line[4:7]);
129
+ # Rt = np.concatenate((np.concatenate((R, t[:,np.newaxis]), axis=1), [[0.0,0.0,0.0,1.0]]), axis=0)
130
+ tokens = traj_str.split()
131
+ assert len(tokens) == 7
132
+ ts = tokens[0]
133
+ # Rotation in angle axis
134
+ angle_axis = [float(tokens[1]), float(tokens[2]), float(tokens[3])]
135
+ r_w_to_p = convert_angle_axis_to_matrix3(np.asarray(angle_axis))
136
+ # Translation
137
+ t_w_to_p = np.asarray([float(tokens[4]), float(tokens[5]), float(tokens[6])])
138
+ extrinsics = np.eye(4, 4)
139
+ extrinsics[:3, :3] = r_w_to_p
140
+ extrinsics[:3, -1] = t_w_to_p
141
+ Rt = np.linalg.inv(extrinsics)
142
+ return (ts, Rt)
143
+
144
+
145
+ def st2_camera_intrinsics(filename):
146
+ w, h, fx, fy, hw, hh = np.loadtxt(filename)
147
+ return np.asarray([[fx, 0, hw], [0, fy, hh], [0, 0, 1]])
148
+
149
+
150
+ def handle_process(
151
+ scene_path,
152
+ output_path,
153
+ pointclouds_root,
154
+ frame_gap,
155
+ parse_pointclouds,
156
+ parse_depths,
157
+ ):
158
+ print("loading from ", scene_path)
159
+ split = os.path.basename(os.path.dirname(scene_path))
160
+ scene_id = os.path.basename(scene_path)
161
+ img_folder = os.path.join(scene_path, f"{scene_id}_frames", "lowres_wide")
162
+ traj_file = os.path.join(scene_path, f"{scene_id}_frames", "lowres_wide.traj")
163
+ pc_output_path = os.path.join(output_path, split, f"{scene_id}")
164
+ pc_input_path = os.path.join(pointclouds_root, split, f"{scene_id}")
165
+ im_output_path = os.path.join(output_path, "images", split, f"{scene_id}")
166
+ os.makedirs(im_output_path, exist_ok=True)
167
+ if not os.path.exists(img_folder):
168
+ frame_ids = []
169
+ else:
170
+ images = sorted(
171
+ glob.glob(os.path.join(img_folder, "*.png")),
172
+ key=lambda x: float(x.split("/")[-1].split("_")[1][:-4]),
173
+ )
174
+ frame_ids = [os.path.basename(x) for x in images]
175
+ frame_ids = [x.split(".png")[0].split("_")[1] for x in frame_ids]
176
+ video_id = img_folder.split("/")[-2].split("_frames")[0]
177
+ frame_ids.sort()
178
+ frame_ids = frame_ids[::frame_gap]
179
+ if len(frame_ids) > 0:
180
+ height, width = cv2.imread(images[0]).shape[:2]
181
+ if os.path.exists(traj_file):
182
+ with open(traj_file) as f:
183
+ traj = f.readlines()
184
+ # convert traj to json dict
185
+ poses_from_traj = {}
186
+ for line in traj:
187
+ traj_timestamp = line.split(" ")[0]
188
+ poses_from_traj[f"{round(float(traj_timestamp), 3):.3f}"] = (
189
+ TrajStringToMatrix(line)[1].tolist()
190
+ )
191
+
192
+ if os.path.exists(traj_file):
193
+ poses = poses_from_traj
194
+ else:
195
+ poses = {}
196
+
197
+ # get img dlc
198
+ poses_keys = list(poses.keys())
199
+ intrinsics = []
200
+ poses_list = []
201
+ Ks_path = os.path.join(im_output_path, "intrinsic")
202
+ Ts_path = os.path.join(im_output_path, "pose")
203
+ img_path = os.path.join(im_output_path, "color")
204
+
205
+ os.makedirs(Ks_path, exist_ok=True)
206
+ os.makedirs(Ts_path, exist_ok=True)
207
+ os.makedirs(img_path, exist_ok=True)
208
+ if parse_depths:
209
+ depth_path = os.path.join(im_output_path, "depth")
210
+ os.makedirs(depth_path, exist_ok=True)
211
+ for frame_id in frame_ids:
212
+ shutil.copy(
213
+ os.path.join(
214
+ scene_path,
215
+ f"{scene_id}_frames",
216
+ "lowres_wide",
217
+ f"{video_id}_{frame_id}.png",
218
+ ),
219
+ img_path,
220
+ )
221
+ if parse_depths:
222
+ shutil.copy(
223
+ os.path.join(
224
+ scene_path,
225
+ f"{scene_id}_frames",
226
+ "lowres_depth",
227
+ f"{video_id}_{frame_id}.png",
228
+ ),
229
+ depth_path,
230
+ )
231
+ intrinsic_fn = os.path.join(
232
+ scene_path,
233
+ f"{scene_id}_frames",
234
+ "lowres_wide_intrinsics",
235
+ f"{video_id}_{frame_id}.pincam",
236
+ )
237
+ intrinsic = st2_camera_intrinsics(intrinsic_fn)
238
+ intrinsics.append(intrinsic)
239
+ np.save(os.path.join(Ks_path, f"{video_id}_{frame_id}.npy"), intrinsic)
240
+ frame_id = f"{round(float(frame_id), 3):.3f}"
241
+ closest_key = min(
242
+ poses_keys, key=lambda k: abs(float(k) - float(frame_id))
243
+ )
244
+ np.save(
245
+ os.path.join(Ts_path, f"{video_id}_{frame_id}.npy"),
246
+ poses[closest_key],
247
+ )
248
+ poses_list.append(poses[closest_key])
249
+ # match extrinsics
250
+ intrinsics = np.stack(intrinsics)
251
+ poses = np.stack(poses_list)
252
+
253
+ ply_path = os.path.join(scene_path, f"{scene_id}_3dod_mesh.ply")
254
+ if parse_pointclouds:
255
+ vertices, faces = read_plymesh(ply_path)
256
+ coords = vertices[:, :3]
257
+ colors = vertices[:, 3:6]
258
+ normals = vertex_normal(coords, faces)
259
+ data_dict = dict(coord=coords, color=colors, normal=normals)
260
+ os.makedirs(pc_output_path, exist_ok=True)
261
+ for key in data_dict.keys():
262
+ np.save(os.path.join(pc_output_path, f"{key}.npy"), data_dict[key])
263
+ else:
264
+ coords = np.load(os.path.join(pc_input_path, "coord.npy"))
265
+
266
+ # Save img data
267
+ mesh = o3d.io.read_triangle_mesh(ply_path)
268
+ correspondenceSave(
269
+ mesh,
270
+ im_output_path,
271
+ coords,
272
+ os.path.join(im_output_path, "correspondence"),
273
+ (height, width),
274
+ intrinsics,
275
+ poses,
276
+ )
277
+
278
+
279
+ if __name__ == "__main__":
280
+ parser = argparse.ArgumentParser()
281
+ parser.add_argument(
282
+ "--dataset_root",
283
+ required=True,
284
+ help="Path to the ArkitScenes dataset containing 3dod folder",
285
+ )
286
+ parser.add_argument(
287
+ "--output_root",
288
+ required=True,
289
+ help="Output path where train/val folders will be located",
290
+ )
291
+ parser.add_argument(
292
+ "--pointclouds_root",
293
+ default="data/arkitscenes",
294
+ type=str,
295
+ help="Input path where previous pointclouds folder located",
296
+ )
297
+ parser.add_argument(
298
+ "--frame_gap", default=50, type=int, help="Frame gap for processing"
299
+ )
300
+ parser.add_argument(
301
+ "--num_workers",
302
+ default=mp.cpu_count(),
303
+ type=int,
304
+ help="Num workers for preprocessing.",
305
+ )
306
+ parser.add_argument(
307
+ "--thread_id",
308
+ default=0,
309
+ type=int,
310
+ help="thread_id",
311
+ )
312
+ parser.add_argument(
313
+ "--parse_pointclouds", action="store_true", help="Whether parse point clouds"
314
+ )
315
+ parser.add_argument(
316
+ "--parse_depths", action="store_true", help="Whether parse depths"
317
+ )
318
+ opt = parser.parse_args()
319
+ # Load scene paths
320
+ metadata_path = f"{opt.dataset_root}/3dod/metadata.csv"
321
+ metadata = pd.read_csv(metadata_path)
322
+ # Initialize the splits dictionary
323
+ splits = {}
324
+ scene_paths = []
325
+ # Populate the splits dictionary
326
+ for index, row in metadata.iterrows():
327
+ scene_id = str(row["video_id"])
328
+ split = row["fold"]
329
+ splits[scene_id] = split
330
+ scene_paths.append(os.path.join(opt.dataset_root, "3dod", split, scene_id))
331
+ # Preprocess data.
332
+ scene_paths_list = np.array_split(scene_paths, opt.num_workers)
333
+ scene_paths_ = scene_paths_list[opt.thread_id]
334
+ print("Processing scenes...")
335
+ for scene_path in scene_paths_:
336
+ handle_process(
337
+ scene_path,
338
+ opt.output_root,
339
+ opt.pointclouds_root,
340
+ opt.frame_gap,
341
+ opt.parse_pointclouds,
342
+ opt.parse_depths,
343
+ )
pointcept/datasets/preprocessing/concerto/arkitscenes/preprocess_arkitscenes.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ dataset_root=""
4
+ output_root=""
5
+ num_workers=16
6
+ parse_depths=false
7
+ parse_pointclouds=false
8
+
9
+ while getopts "d:o:n:pc" opt; do
10
+ case $opt in
11
+ d) dataset_root=$OPTARG ;;
12
+ o) output_root=$OPTARG ;;
13
+ n) num_workers=$OPTARG ;;
14
+ p) parse_depths=true ;;
15
+ c) parse_pointclouds=true ;;
16
+ *) echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"; exit 1 ;;
17
+ esac
18
+ done
19
+
20
+ if [ -z "$dataset_root" ] || [ -z "$output_root" ]; then
21
+ echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"
22
+ exit 1
23
+ fi
24
+
25
+ for i in $(seq 0 $((num_workers - 1))); do
26
+ cmd="python pointcept/datasets/preprocessing/concerto/arkitscenes/preprocess_arkitscenes.py --thread_id $i \
27
+ --num_workers $num_workers \
28
+ --dataset_root $dataset_root \
29
+ --output_root $output_root"
30
+
31
+ if $parse_depths; then
32
+ cmd="$cmd --parse_depths"
33
+ fi
34
+
35
+ if $parse_pointclouds; then
36
+ cmd="$cmd --parse_pointclouds"
37
+ fi
38
+
39
+ eval "$cmd &"
40
+ done
41
+
42
+ wait
pointcept/datasets/preprocessing/concerto/arkitscenes/rotation.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import math
3
+ import numpy as np
4
+
5
+
6
+ def eulerAnglesToRotationMatrix(theta):
7
+ """Euler rotation matrix with clockwise logic.
8
+ Rotation
9
+
10
+ Args:
11
+ theta: list of float
12
+ [theta_x, theta_y, theta_z]
13
+ Returns:
14
+ R: np.array (3, 3)
15
+ rotation matrix of Rz*Ry*Rx
16
+ """
17
+ R_x = np.array(
18
+ [
19
+ [1, 0, 0],
20
+ [0, math.cos(theta[0]), -math.sin(theta[0])],
21
+ [0, math.sin(theta[0]), math.cos(theta[0])],
22
+ ]
23
+ )
24
+
25
+ R_y = np.array(
26
+ [
27
+ [math.cos(theta[1]), 0, math.sin(theta[1])],
28
+ [0, 1, 0],
29
+ [-math.sin(theta[1]), 0, math.cos(theta[1])],
30
+ ]
31
+ )
32
+
33
+ R_z = np.array(
34
+ [
35
+ [math.cos(theta[2]), -math.sin(theta[2]), 0],
36
+ [math.sin(theta[2]), math.cos(theta[2]), 0],
37
+ [0, 0, 1],
38
+ ]
39
+ )
40
+
41
+ R = np.dot(R_z, np.dot(R_y, R_x))
42
+ return R
43
+
44
+
45
+ def upright_camera_relative_transform(pose):
46
+ """Generate pose matrix with z-dim as height
47
+
48
+ Args:
49
+ pose: np.array (4, 4)
50
+ Returns:
51
+ urc: (4, 4)
52
+ urc_inv: (4, 4)
53
+ """
54
+
55
+ # take viewing direction in camera local coordiantes (which is simply unit vector along +z)
56
+ view_dir_camera = np.asarray([0, 0, 1])
57
+ R = pose[0:3, 0:3]
58
+ t = pose[0:3, 3]
59
+
60
+ # convert to world coordinates
61
+ view_dir_world = np.dot(R, view_dir_camera)
62
+
63
+ # compute heading
64
+ view_dir_xy = view_dir_world[0:2]
65
+ heading = math.atan2(view_dir_xy[1], view_dir_xy[0])
66
+
67
+ # compute rotation around Z to align heading with +Y
68
+ zRot = -heading + math.pi / 2
69
+
70
+ # translation first, back to camera point
71
+ urc_t = np.identity(4)
72
+ urc_t[0:2, 3] = -1 * t[0:2]
73
+
74
+ # compute rotation matrix
75
+ urc_r = np.identity(4)
76
+ urc_r[0:3, 0:3] = eulerAnglesToRotationMatrix([0, 0, zRot])
77
+
78
+ urc = np.dot(urc_r, urc_t)
79
+ urc_inv = np.linalg.inv(urc)
80
+
81
+ return urc, urc_inv
82
+
83
+
84
+ def rotate_pc(pc, rotmat):
85
+ """Rotation points w.r.t. rotmat
86
+ Args:
87
+ pc: np.array (n, 3)
88
+ rotmat: np.array (4, 4)
89
+ Returns:
90
+ pc: (n, 3)
91
+ """
92
+ pc_4 = np.ones([pc.shape[0], 4])
93
+ pc_4[:, 0:3] = pc
94
+ pc_4 = np.dot(pc_4, np.transpose(rotmat))
95
+
96
+ return pc_4[:, 0:3]
97
+
98
+
99
+ def rotate_points_along_z(points, angle):
100
+ """Rotation clockwise
101
+ Args:
102
+ points: np.array of np.array (B, N, 3 + C) or
103
+ (N, 3 + C) for single batch
104
+ angle: np.array of np.array (B, )
105
+ or (, ) for single batch
106
+ angle along z-axis, angle increases x ==> y
107
+ Returns:
108
+ points_rot: (B, N, 3 + C) or (N, 3 + C)
109
+
110
+ """
111
+ single_batch = len(points.shape) == 2
112
+ if single_batch:
113
+ points = np.expand_dims(points, axis=0)
114
+ angle = np.expand_dims(angle, axis=0)
115
+ cosa = np.expand_dims(np.cos(angle), axis=1)
116
+ sina = np.expand_dims(np.sin(angle), axis=1)
117
+ zeros = np.zeros_like(cosa) # angle.new_zeros(points.shape[0])
118
+ ones = np.ones_like(sina) # angle.new_ones(points.shape[0])
119
+
120
+ rot_matrix = np.concatenate(
121
+ (cosa, -sina, zeros, sina, cosa, zeros, zeros, zeros, ones), axis=1
122
+ ).reshape(-1, 3, 3)
123
+
124
+ # print(rot_matrix.view(3, 3))
125
+ points_rot = np.matmul(points[:, :, :3], rot_matrix)
126
+ points_rot = np.concatenate((points_rot, points[:, :, 3:]), axis=-1)
127
+
128
+ if single_batch:
129
+ points_rot = points_rot.squeeze(0)
130
+
131
+ return points_rot
132
+
133
+
134
+ def convert_angle_axis_to_matrix3(angle_axis):
135
+ """Return a Matrix3 for the angle axis.
136
+ Arguments:
137
+ angle_axis {Point3} -- a rotation in angle axis form.
138
+ """
139
+ matrix, jacobian = cv2.Rodrigues(angle_axis)
140
+ return matrix
pointcept/datasets/preprocessing/concerto/arkitscenes/splits.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ im_path = os.path.join(dataset_path, "images")
9
+ pc_path = dataset_path
10
+ splits = ["Training", "Validation"]
11
+ split_path = os.path.join(dataset_path, "splits")
12
+ os.makedirs(split_path, exist_ok=True)
13
+ for split in splits:
14
+ im_split_path = os.path.join(im_path, split)
15
+ pc_split_path = os.path.join(pc_path, split).replace(
16
+ dataset_path, "data/arkitscenes"
17
+ )
18
+ split_names = [f.name for f in os.scandir(im_split_path) if f.is_dir()]
19
+ split_dict = {}
20
+ for name in split_names:
21
+ im_split_name_path = os.path.join(im_split_path, name, "color")
22
+ co_split_name_path = os.path.join(im_split_path, name, "correspondence")
23
+ png_files = [
24
+ f for f in os.listdir(im_split_name_path) if f.endswith(".png")
25
+ ]
26
+ png_files = sorted(png_files, key=lambda x: float(x.split("_")[1][:-4]))
27
+ co_files = [f for f in os.listdir(co_split_name_path) if f.endswith(".npy")]
28
+ co_files = sorted(co_files, key=lambda x: float(x[:-4]))
29
+ # Get the full paths of the .png files
30
+ png_file_paths = [
31
+ os.path.join(im_split_name_path, f).replace(
32
+ dataset_path, "data/arkitscenes"
33
+ )
34
+ for f in png_files
35
+ ]
36
+ co_file_paths = [
37
+ os.path.join(co_split_name_path, f).replace(
38
+ dataset_path, "data/arkitscenes"
39
+ )
40
+ for f in co_files
41
+ ]
42
+ for i in range(0, len(png_file_paths), 4):
43
+ split_dict[f"{name}_{i//4}"] = {}
44
+ split_dict[f"{name}_{i//4}"]["pointclouds"] = os.path.join(
45
+ pc_split_path, name
46
+ )
47
+ split_dict[f"{name}_{i//4}"]["images"] = png_file_paths[i : i + 4]
48
+ split_dict[f"{name}_{i//4}"]["correspondences"] = co_file_paths[
49
+ i : i + 4
50
+ ]
51
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
52
+ json.dump(split_dict, f, indent=4)
53
+
54
+
55
+ # Example usage
56
+ if __name__ == "__main__":
57
+ parser = argparse.ArgumentParser()
58
+ parser.add_argument(
59
+ "--dataset_root",
60
+ required=True,
61
+ help="Path to the arkitscenes dataset containing scene folders",
62
+ )
63
+ config = parser.parse_args()
64
+ get_splits_paths(config.dataset_root)
pointcept/datasets/preprocessing/concerto/cap3d/preprocess_cap3d.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for Cap3D
3
+
4
+ Author: Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+ import argparse
10
+ import torch
11
+ import numpy as np
12
+ from scipy.spatial import cKDTree
13
+ from PIL import Image
14
+ from pathlib import Path
15
+ import multiprocessing as mp
16
+ import zipfile
17
+ import glob
18
+ import json
19
+ import math
20
+
21
+ NUM_VIEWS = 4
22
+ MAX_DEPTH = 5.0
23
+
24
+
25
+ def recover_depth_from_image(depth_image_path, max_depth):
26
+ with Image.open(depth_image_path) as depth_pil:
27
+ depth_uint16 = np.array(depth_pil)
28
+ invalid_depth_mask = depth_uint16 == 65535
29
+ depth_normalized_float = depth_uint16.astype(np.float32) / 65535.0
30
+ depth_meters = depth_normalized_float * max_depth
31
+ return depth_meters, invalid_depth_mask
32
+
33
+
34
+ def normalize_point_cloud(point_cloud: np.ndarray) -> np.ndarray:
35
+ bbox_min = np.min(point_cloud, axis=0)
36
+ bbox_max = np.max(point_cloud, axis=0)
37
+ bbox_size = bbox_max - bbox_min
38
+ max_dim = np.max(bbox_size)
39
+ if max_dim < 1e-8:
40
+ return point_cloud - bbox_min
41
+
42
+ scale = 1.0 / max_dim
43
+ scaled_point_cloud = point_cloud * scale
44
+ new_bbox_min = np.min(scaled_point_cloud, axis=0)
45
+ new_bbox_max = np.max(scaled_point_cloud, axis=0)
46
+ center = (new_bbox_min + new_bbox_max) / 2.0
47
+ offset = -center
48
+ normalized_point_cloud = scaled_point_cloud + offset
49
+
50
+ return normalized_point_cloud
51
+
52
+
53
+ def get_cam_params(cam_params_file, width, height):
54
+ with open(cam_params_file, "r") as f:
55
+ data = json.load(f)
56
+
57
+ x_vector = data["x"]
58
+ y_vector = data["y"]
59
+ z_vector = data["z"]
60
+ origin = data["origin"]
61
+
62
+ rotation_matrix = np.array([x_vector, y_vector, z_vector]).T
63
+
64
+ translation_vector = np.array(origin)
65
+
66
+ rt_matrix = np.eye(4)
67
+ rt_matrix[:3, :3] = rotation_matrix
68
+ rt_matrix[:3, 3] = translation_vector
69
+
70
+ x_fov = data["x_fov"]
71
+ y_fov = data["y_fov"]
72
+
73
+ cx = width / 2.0
74
+ cy = height / 2.0
75
+
76
+ fx = (width / 2.0) / math.tan(x_fov / 2.0)
77
+ fy = (height / 2.0) / math.tan(y_fov / 2.0)
78
+
79
+ s = 0
80
+
81
+ K = np.array([[fx, s, cx], [0, fy, cy], [0, 0, 1]])
82
+
83
+ return rt_matrix, K
84
+
85
+
86
+ def correspondenceGet(depth, invalid_depth_mask, T, K, width, height):
87
+ pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
88
+ pixel = pixel.reshape((-1, 2))
89
+ pixel = np.hstack((pixel, np.ones((pixel.shape[0], 1))))
90
+ depth = depth.reshape((-1, 1))
91
+ invalid_depth_mask = invalid_depth_mask.reshape((-1, 1))
92
+ valid = ~np.logical_or(np.isinf(depth), invalid_depth_mask).squeeze(-1)
93
+ coord = np.zeros_like(pixel, dtype=np.float32)
94
+ coord[valid] = depth[valid] * (np.linalg.inv(K) @ pixel[valid].T).T # coord_camera
95
+ coord[valid] = coord[valid] @ T[:3, :3].T + T[:3, 3] # column then row
96
+
97
+ pixel = pixel[valid]
98
+ coord = coord[valid]
99
+ if coord.shape[0] == 0:
100
+ return None
101
+ pixel = pixel[:, :2]
102
+ coord_dict = {"pixel": pixel, "coord": coord}
103
+ return coord_dict
104
+
105
+
106
+ def correspondenceSave(
107
+ depth_paths, data_name, point_cloud_gt, cam_params_paths, output_root
108
+ ):
109
+ """
110
+ Calculates and saves the correspondences for all views of a given mesh and point cloud.
111
+ """
112
+ # Build a KDTree for the ground truth point cloud for fast querying.
113
+ tree = cKDTree(point_cloud_gt)
114
+
115
+ # Iterate over each predefined view.
116
+ for view_idx in range(len(depth_paths)):
117
+ # Construct file paths.
118
+ cam_params_path = cam_params_paths[view_idx]
119
+ depth_path = depth_paths[view_idx]
120
+
121
+ view_name = cam_params_path.split("/")[-1][:-5]
122
+
123
+ # Load camera parameters and image dimensions.
124
+ depth, invalid_depth_mask = recover_depth_from_image(depth_path, MAX_DEPTH)
125
+ height, width = depth.shape
126
+ T, K = get_cam_params(cam_params_path, width, height)
127
+
128
+ coord_dict = correspondenceGet(depth, invalid_depth_mask, T, K, width, height)
129
+ if coord_dict is None:
130
+ correspondences = -np.ones((1, 3))
131
+ else:
132
+ pixels_ = coord_dict["pixel"]
133
+ coords_ = coord_dict["coord"]
134
+ dis, idx = tree.query(coords_, k=1)
135
+ idx_valid = idx[dis < 0.01]
136
+ pixels_valid = pixels_[dis < 0.01]
137
+ correspondences = np.hstack((pixels_valid, idx_valid.reshape(-1, 1)))
138
+ output_dir = output_root / data_name
139
+ os.makedirs(output_dir, exist_ok=True)
140
+ output_filename = output_dir / f"{view_name}.npy"
141
+ np.save(output_filename, correspondences)
142
+
143
+
144
+ def handle_process(pt_path, output_root, cam_root):
145
+ """
146
+ The complete processing pipeline for a single GLB file.
147
+ """
148
+ data_name = pt_path.stem
149
+
150
+ if not pt_path.exists():
151
+ return
152
+ cam_path = cam_root / f"{data_name}"
153
+ cam_path_zip = cam_root / f"{data_name}.zip"
154
+
155
+ # Check if files exist, just in case.
156
+ if not cam_path.exists():
157
+ if not cam_path_zip.exists():
158
+ print(f"not exist {str(cam_path)} and its zip file")
159
+ return
160
+ with zipfile.ZipFile(cam_path_zip, "r") as zip_ref:
161
+ zip_ref.extractall(cam_path)
162
+
163
+ # Load the point cloud.
164
+ point_cloud = torch.load(pt_path)
165
+ point_cloud_np = (
166
+ point_cloud.cpu().numpy()
167
+ if hasattr(point_cloud, "numpy")
168
+ else np.array(point_cloud)
169
+ )
170
+ point_cloud_np = point_cloud_np[:3, :].T
171
+ depth_paths = glob.glob(os.path.join(cam_path, "*_depth.png"))
172
+ if len(depth_paths) >= NUM_VIEWS:
173
+ depth_paths = sorted(depth_paths)[:: len(depth_paths) // NUM_VIEWS]
174
+ else:
175
+ depth_paths = sorted(depth_paths)
176
+ cam_params_paths = glob.glob(os.path.join(cam_path, "*.json"))
177
+ if os.path.isfile(os.path.join(cam_path, "transforms_train.json")):
178
+ cam_params_paths.remove(os.path.join(cam_path, "transforms_train.json"))
179
+ if os.path.isfile(os.path.join(cam_path, "info.json")):
180
+ cam_params_paths.remove(os.path.join(cam_path, "info.json"))
181
+ if len(depth_paths) >= NUM_VIEWS:
182
+ cam_params_paths = sorted(cam_params_paths)[
183
+ :: len(cam_params_paths) // NUM_VIEWS
184
+ ]
185
+ else:
186
+ cam_params_paths = sorted(cam_params_paths)
187
+
188
+ valid_depth_paths = {
189
+ os.path.basename(p).replace("_depth.png", ""): p for p in depth_paths
190
+ }
191
+ valid_cam_params_paths = {
192
+ os.path.basename(p).replace(".json", ""): p for p in cam_params_paths
193
+ }
194
+ common_ids = set(valid_depth_paths.keys()) & set(valid_cam_params_paths.keys())
195
+ sorted_common_ids = sorted(list(common_ids))
196
+
197
+ valid_depth_paths_ = [valid_depth_paths[uid] for uid in sorted_common_ids]
198
+ valid_cam_params_paths_ = [valid_cam_params_paths[uid] for uid in sorted_common_ids]
199
+ # Calculate and save correspondences for all views.
200
+ correspondenceSave(
201
+ valid_depth_paths_,
202
+ data_name,
203
+ point_cloud_np,
204
+ valid_cam_params_paths_,
205
+ output_root,
206
+ )
207
+
208
+
209
+ if __name__ == "__main__":
210
+ parser = argparse.ArgumentParser(
211
+ description="Generate 2D-3D correspondences for GLB models and point clouds."
212
+ )
213
+ parser.add_argument(
214
+ "--cam_root",
215
+ type=Path,
216
+ required=True,
217
+ help="Root directory containing camera parameters.",
218
+ )
219
+ parser.add_argument(
220
+ "--point_cloud_root",
221
+ type=Path,
222
+ required=True,
223
+ help="Directory containing .pt point cloud files.",
224
+ )
225
+ parser.add_argument(
226
+ "--output_root",
227
+ type=Path,
228
+ required=True,
229
+ help="Output directory to save correspondence files.",
230
+ )
231
+ parser.add_argument(
232
+ "--num_workers",
233
+ type=int,
234
+ default=mp.cpu_count(),
235
+ help="Number of processes to use for processing.",
236
+ )
237
+ parser.add_argument(
238
+ "--thread_id", type=int, default=0, help="thread id to use for processing."
239
+ )
240
+ config = parser.parse_args()
241
+
242
+ # Ensure the output directory exists.
243
+ os.makedirs(config.output_root, exist_ok=True)
244
+
245
+ print("Scanning for .pt files...")
246
+ pt_files = list(config.point_cloud_root.glob("**/*.pt"))
247
+ print(f"Found {len(pt_files)} .pt files.")
248
+
249
+ # Load scene paths
250
+ pt_files_list = np.array_split(pt_files, config.num_workers)
251
+ pt_files_ = pt_files_list[config.thread_id]
252
+ # Preprocess data.
253
+ print("Processing scenes...")
254
+ for pt_files_i in pt_files_:
255
+ handle_process(
256
+ pt_files_i,
257
+ config.output_root,
258
+ config.cam_root,
259
+ )
260
+
261
+ print("\nAll files processed.")
pointcept/datasets/preprocessing/concerto/cap3d/preprocess_cap3d.sh ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ cam_root=""
4
+ point_cloud_root=""
5
+ output_root=""
6
+ num_workers=16
7
+
8
+ while getopts "c:p:o:n" opt; do
9
+ case $opt in
10
+ c) cam_root=$OPTARG ;;
11
+ p) point_cloud_root=$OPTARG ;;
12
+ o) output_root=$OPTARG ;;
13
+ n) num_workers=$OPTARG ;;
14
+ *) echo "Usage: $0 -c <cam_root> -p <point_cloud_root> -o <output_root> [-n <num_workers>]"; exit 1 ;;
15
+ esac
16
+ done
17
+
18
+ if [ -z "$cam_root" ] || [ -z "$point_cloud_root" ] || [ -z "$output_root" ]; then
19
+ echo "Error: Missing required arguments. Use -h for help."
20
+ echo "Usage: $0 -c <cam_root> -p <point_cloud_root> -o <output_root> [-n <num_workers>]"
21
+ exit 1
22
+ fi
23
+
24
+ for i in $(seq 0 $((num_workers - 1))); do
25
+ cmd="python pointcept/datasets/preprocessing/concerto/cap3d/preprocess_cap3d.py \
26
+ --cam_root $cam_root \
27
+ --point_cloud_root $point_cloud_root \
28
+ --output_root $output_root \
29
+ --num_workers $num_workers \
30
+ --thread_id $i"
31
+
32
+ eval "$cmd &"
33
+ done
34
+
35
+ wait
pointcept/datasets/preprocessing/concerto/cap3d/splits.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ co_path = os.path.join(dataset_path, "correspondences")
9
+ im_path = os.path.join(dataset_path, "images")
10
+ pc_path = dataset_path
11
+ splits = ["train"]
12
+ split_path = os.path.join(dataset_path, "splits")
13
+ os.makedirs(split_path, exist_ok=True)
14
+ for split in splits:
15
+ co_split_path = os.path.join(co_path, split)
16
+ im_split_path = os.path.join(im_path, split)
17
+ pc_split_path = os.path.join(pc_path, split)
18
+ split_names = [f.name for f in os.scandir(co_split_path) if f.is_dir()]
19
+ split_dict = {}
20
+ for name in split_names:
21
+ print(f"processing {name}")
22
+ co_name_split_path = os.path.join(co_split_path, name)
23
+ img_names = [
24
+ f.name.removesuffix(".npy")
25
+ for f in os.scandir(co_name_split_path)
26
+ if f.is_file() and f.name.endswith(".npy")
27
+ ]
28
+ img_names = sorted(img_names)
29
+ png_file_paths = [
30
+ os.path.join(im_split_path, name, img_name + ".png").replace(
31
+ dataset_path, "data/cap3d"
32
+ )
33
+ for img_name in img_names
34
+ ]
35
+ # Get the full paths of the .png files
36
+ co_file_paths = [
37
+ os.path.join(co_name_split_path, img_name + ".npy").replace(
38
+ dataset_path, "data/cap3d"
39
+ )
40
+ for img_name in img_names
41
+ ]
42
+ pc_file_path = os.path.join(pc_split_path, name + ".pt")
43
+ file_check = True
44
+ for png_file_path in png_file_paths:
45
+ if not os.path.isfile(png_file_path):
46
+ file_check = False
47
+ for co_file_path in co_file_paths:
48
+ if not os.path.isfile(co_file_path):
49
+ file_check = False
50
+ if not os.path.isfile(pc_file_path):
51
+ file_check = False
52
+ continue
53
+ if file_check:
54
+ split_dict[f"{name}"] = {}
55
+ split_dict[f"{name}"]["pointclouds"] = pc_file_path
56
+ split_dict[f"{name}"]["images"] = png_file_paths
57
+ split_dict[f"{name}"]["correspondences"] = co_file_paths
58
+ else:
59
+ split_dict[f"{name}"] = {}
60
+ split_dict[f"{name}"]["pointclouds"] = pc_file_path
61
+ split_dict[f"{name}"]["images"] = []
62
+ split_dict[f"{name}"]["correspondences"] = []
63
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
64
+ json.dump(split_dict, f, indent=4)
65
+
66
+
67
+ # Example usage
68
+ if __name__ == "__main__":
69
+ parser = argparse.ArgumentParser()
70
+ parser.add_argument(
71
+ "--dataset_root",
72
+ required=True,
73
+ help="Path to the ScanNet dataset containing scene folders",
74
+ )
75
+ config = parser.parse_args()
76
+ get_splits_paths(config.dataset_root)
pointcept/datasets/preprocessing/concerto/graspnet/preprocess_graspnet_poses.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for GraspNet
3
+
4
+ Author: Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+ import numpy as np
10
+ import argparse
11
+ from tqdm import tqdm
12
+
13
+
14
+ def split_poses(graspnet_root):
15
+ """
16
+ Splits the camera_poses.npy file into individual pose files for each scene.
17
+ """
18
+ scenes_path = os.path.join(graspnet_root, "scenes")
19
+ scene_names = sorted(
20
+ [
21
+ d
22
+ for d in os.listdir(scenes_path)
23
+ if os.path.isdir(os.path.join(scenes_path, d))
24
+ ]
25
+ )
26
+
27
+ print(f"Found {len(scene_names)} scenes. Starting preprocessing...")
28
+
29
+ for scene_name in tqdm(scene_names, desc="Processing Scenes"):
30
+ kinect_path = os.path.join(scenes_path, scene_name, "kinect")
31
+ poses_file = os.path.join(kinect_path, "camera_poses.npy")
32
+ if not os.path.exists(poses_file):
33
+ print(f"Warning: camera_poses.npy not found in {kinect_path}, skipping.")
34
+ continue
35
+
36
+ all_poses = np.load(poses_file) # Shape: (256, 4, 4)
37
+ output_pose_dir = os.path.join(kinect_path, "pose")
38
+ os.makedirs(output_pose_dir, exist_ok=True)
39
+
40
+ for i in range(all_poses.shape[0]):
41
+ pose = all_poses[i]
42
+ output_file = os.path.join(output_pose_dir, f"{i:04d}.npy")
43
+ np.save(output_file, pose)
44
+
45
+ print("Preprocessing finished. Individual pose files have been created.")
46
+
47
+
48
+ if __name__ == "__main__":
49
+ parser = argparse.ArgumentParser(description="Preprocess GraspNet camera poses.")
50
+ parser.add_argument(
51
+ "--dataset_root",
52
+ required=True,
53
+ help="Path to the root of the GraspNet dataset (the one containing the 'scenes' folder).",
54
+ )
55
+ args = parser.parse_args()
56
+ split_poses(args.dataset_root)
pointcept/datasets/preprocessing/concerto/graspnet/splits.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GraspNet : train: 0-99, test_seen: 100-189
2
+ # train: scene_0000 to scene_0100 (100 scenes)
3
+ # val: scene_0100 to scene_0189 (90 scenes)
4
+ import os
5
+ import json
6
+ import argparse
7
+ from tqdm import tqdm
8
+
9
+
10
+ def generate_graspnet_splits(dataset_path, group_num=4):
11
+ """
12
+ Generates train/val/test JSON split files for the GraspNet dataset.
13
+ """
14
+ scenes_dir = os.path.join(dataset_path, "scenes")
15
+ all_scenes = sorted(
16
+ [f for f in os.scandir(scenes_dir) if f.is_dir()], key=lambda f: f.name
17
+ )
18
+ all_scene_names = [f.name for f in all_scenes]
19
+
20
+ split_definitions = {
21
+ "train": [s for s in all_scene_names if 100 > int(s.split("_")[1]) >= 0],
22
+ "val": [s for s in all_scene_names if int(s.split("_")[1]) >= 100],
23
+ }
24
+
25
+ output_split_path = os.path.join(dataset_path, "splits")
26
+ os.makedirs(output_split_path, exist_ok=True)
27
+ print(f"JSON files will be saved in: {output_split_path}")
28
+
29
+ for split, scene_names in split_definitions.items():
30
+ print(f"Processing '{split}' split with {len(scene_names)} scenes...")
31
+ split_dict = {}
32
+ for scene_name in tqdm(scene_names, desc=f"Generating {split}.json"):
33
+ kinect_path = os.path.join(scenes_dir, scene_name, "kinect")
34
+
35
+ rgb_path = os.path.join(kinect_path, "rgb")
36
+ depth_path = os.path.join(kinect_path, "depth")
37
+ pose_path = os.path.join(kinect_path, "pose")
38
+ K_file_path_abs = os.path.join(kinect_path, "camK.npy")
39
+ K_file_path_rel = K_file_path_abs.replace(dataset_path, "data/graspnet")
40
+
41
+ rgb_files = [f for f in os.listdir(rgb_path) if f.endswith(".png")]
42
+ rgb_files = sorted(rgb_files, key=lambda x: int(os.path.splitext(x)[0]))
43
+
44
+ rgb_file_paths = [
45
+ os.path.join(rgb_path, f).replace(dataset_path, "data/graspnet")
46
+ for f in rgb_files
47
+ ]
48
+ depth_file_paths = [
49
+ os.path.join(depth_path, f).replace(dataset_path, "data/graspnet")
50
+ for f in rgb_files
51
+ ]
52
+ pose_file_paths = [
53
+ os.path.join(pose_path, f.replace(".png", ".npy")).replace(
54
+ dataset_path, "data/graspnet"
55
+ )
56
+ for f in rgb_files
57
+ ]
58
+
59
+ for i in range(0, len(rgb_file_paths), group_num):
60
+ if i + 4 > len(rgb_file_paths):
61
+ continue
62
+
63
+ chunk_key = f"{scene_name}_{i//group_num}"
64
+ split_dict[chunk_key] = {
65
+ "images": rgb_file_paths[i : i + group_num],
66
+ "depths": depth_file_paths[i : i + group_num],
67
+ "Ts": pose_file_paths[i : i + group_num],
68
+ "Ks": [K_file_path_rel] * group_num,
69
+ }
70
+
71
+ output_json_file = os.path.join(output_split_path, f"{split}.json")
72
+ with open(output_json_file, "w") as f:
73
+ json.dump(split_dict, f, indent=4)
74
+ print(f"Successfully created {output_json_file}")
75
+
76
+
77
+ if __name__ == "__main__":
78
+ parser = argparse.ArgumentParser()
79
+ parser.add_argument(
80
+ "--dataset_root",
81
+ required=True,
82
+ help="Path to the GraspNet dataset root (containing the 'scenes' folder).",
83
+ )
84
+ parser.add_argument(
85
+ "--group_num",
86
+ default=4,
87
+ type=int,
88
+ help="Group Num.",
89
+ )
90
+ config = parser.parse_args()
91
+ generate_graspnet_splits(config.dataset_root, config.group_num)
pointcept/datasets/preprocessing/concerto/hk/hkremote_download.sh ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Export variables so they can be accessed by subshells spawned by xargs
4
+ export BASE_URL="https://data11.map.gov.hk/api/3d-zip/OBJ/"
5
+ # To get API_KEY, visit https://3d.map.gov.hk/download-api. After you agree the license, your API_KEY will show in Example URL: https://download.map.gov.hk/api/3d-zip/[format]/[tile_sheet_num].zip?key=[API_KEY]
6
+ export API_KEY=${API_KEY}
7
+ export OUTPUT_DIR="hk_3d_maps"
8
+ export MAX_THREADS=8 # Set the number of concurrent downloads here
9
+
10
+ mkdir -p "$OUTPUT_DIR"
11
+ echo "Files will be downloaded to: $OUTPUT_DIR"
12
+ echo "Using $MAX_THREADS concurrent threads..."
13
+
14
+ # Define the download logic as a function
15
+ download_map() {
16
+ local MAP_INDEX=$1
17
+ local ZIP_FILENAME="${MAP_INDEX}.zip"
18
+ local OUTPUT_PATH="${OUTPUT_DIR}/${ZIP_FILENAME}"
19
+ local DOWNLOAD_URL="${BASE_URL}${ZIP_FILENAME}?key=${API_KEY}"
20
+
21
+ # Check existence
22
+ if [ -f "$OUTPUT_PATH" ]; then
23
+ echo "[SKIP] File already exists: ${ZIP_FILENAME}"
24
+ return 0
25
+ fi
26
+
27
+ echo "[START] Downloading: ${MAP_INDEX}"
28
+
29
+ # Use wget to download
30
+ # Added --timeout=10 and --tries=2 to prevent threads from hanging forever
31
+ wget --no-check-certificate -q --timeout=10 --tries=2 -O "$OUTPUT_PATH" "$DOWNLOAD_URL"
32
+
33
+ # Check wget exit code; $? == 0 means success
34
+ if [ $? -eq 0 ]; then
35
+ echo "[SUCCESS] Downloaded: ${ZIP_FILENAME}"
36
+ else
37
+ echo "[FAILED] Not found or failed: ${ZIP_FILENAME}"
38
+ # Delete the potentially empty file created by a failed download
39
+ rm -f "$OUTPUT_PATH"
40
+ fi
41
+ }
42
+
43
+ # Export the function so xargs subshells can use it
44
+ export -f download_map
45
+
46
+ # Generate the list of map indices and pipe them to xargs for parallel execution
47
+ # We use standard space-separated lists for the string variables for cleaner execution
48
+ (
49
+ for base in {1..15}; do
50
+ for quad_10k in NW NE SW SE; do
51
+ for grid_2k in {1..25}; do
52
+ for sub_grid_1k in A B C D; do
53
+ echo "${base}-${quad_10k}-${grid_2k}${sub_grid_1k}"
54
+ done
55
+ done
56
+ done
57
+ done
58
+ ) | xargs -P "$MAX_THREADS" -I {} bash -c 'download_map "$@"' _ {}
59
+
60
+ echo "All download tasks completed!"
pointcept/datasets/preprocessing/concerto/hk/preprocess_hk.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for HK Remote
3
+
4
+ Author: Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import open3d as o3d
9
+ import numpy as np
10
+ from pathlib import Path
11
+ import argparse
12
+ import multiprocessing as mp
13
+
14
+
15
+ def handle_process(obj_file_paths, output_directory):
16
+ num_points_to_sample = 10000000
17
+ for obj_file_path in obj_file_paths:
18
+ try:
19
+ print(f"Loading mesh from: {obj_file_path}")
20
+ parent_name = obj_file_path.parent.name
21
+ save_path = output_directory / parent_name
22
+ if save_path.exists():
23
+ print(f"exist {str(parent_name)}")
24
+ continue
25
+ mesh = o3d.io.read_triangle_mesh(obj_file_path, enable_post_processing=True)
26
+
27
+ if not mesh.has_vertices():
28
+ print("Error: Mesh could not be loaded.")
29
+ exit()
30
+
31
+ if not mesh.has_textures():
32
+ print(
33
+ "Warning: Mesh does not have textures. Sampled colors will be black or default."
34
+ )
35
+
36
+ mesh.compute_vertex_normals()
37
+ print("Mesh loaded successfully. Proceeding to sample points...")
38
+
39
+ pcd = mesh.sample_points_uniformly(number_of_points=num_points_to_sample)
40
+ points = np.asarray(pcd.points)
41
+ colors = np.asarray(pcd.colors)
42
+ if colors.shape[0] == 0:
43
+ colors = np.zeros_like(points)
44
+ normals = np.asarray(pcd.normals)
45
+ if normals.shape[0] == 0:
46
+ normals = np.zeros_like(points)
47
+
48
+ print(f"\nSuccessfully sampled {len(points)} points.")
49
+ print(f"Points array shape: {points.shape}") # (N, 3) for XYZ coordinates
50
+ print(
51
+ f"Colors array shape: {colors.shape}"
52
+ ) # (N, 3) for RGB values (0-1 range)
53
+ print(f"Normals array shape: {normals.shape}") # (N, 3) for normal vectors
54
+
55
+ output_coord_path = output_directory / parent_name / "coord.npy"
56
+ output_color_path = output_directory / parent_name / "color.npy"
57
+ output_normal_path = output_directory / parent_name / "normal.npy"
58
+ (output_directory / parent_name).mkdir(exist_ok=True)
59
+
60
+ np.save(output_coord_path, points)
61
+ np.save(output_color_path, (colors * 255).astype(np.int32))
62
+ np.save(output_normal_path, normals)
63
+
64
+ pcd = o3d.geometry.PointCloud()
65
+ pcd.points = o3d.utility.Vector3dVector(points)
66
+ pcd.colors = o3d.utility.Vector3dVector(colors)
67
+ pcd.normals = o3d.utility.Vector3dVector(normals)
68
+ print(
69
+ f"\nSampled point cloud saved to: {str(output_directory/parent_name)}"
70
+ )
71
+ except:
72
+ print(f"fail {parent_name}")
73
+
74
+
75
+ if __name__ == "__main__":
76
+ parser = argparse.ArgumentParser()
77
+ parser.add_argument(
78
+ "--dataset_root",
79
+ required=True,
80
+ help="Path to the ScanNet dataset containing scene folders",
81
+ )
82
+ parser.add_argument(
83
+ "--output_root",
84
+ required=True,
85
+ help="Output path where train/val folders will be located",
86
+ )
87
+ parser.add_argument(
88
+ "--num_workers",
89
+ default=mp.cpu_count(),
90
+ type=int,
91
+ help="Num workers for preprocessing.",
92
+ )
93
+ parser.add_argument(
94
+ "--thread_id",
95
+ default=0,
96
+ type=int,
97
+ help="Thread id for parallel processing",
98
+ )
99
+ config = parser.parse_args()
100
+ output_directory = Path(config.output_root) / "train"
101
+ root_directory = Path(config.dataset_root)
102
+ output_directory.mkdir(exist_ok=True)
103
+ obj_file_paths_generator = root_directory.rglob("*.obj")
104
+ obj_file_paths = list(obj_file_paths_generator)
105
+ print(f"Found {len(obj_file_paths)} .obj files.")
106
+
107
+ obj_file_paths_list = np.array_split(obj_file_paths, config.num_workers)
108
+ obj_file_paths_ = obj_file_paths_list[config.thread_id]
109
+
110
+ handle_process(obj_file_paths_, output_directory)
pointcept/datasets/preprocessing/concerto/hk/preprocess_hk.sh ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ dataset_root=""
4
+ output_root=""
5
+ num_workers=16
6
+ parse_depths=false
7
+
8
+ while getopts "d:o:n:p" opt; do
9
+ case $opt in
10
+ d) dataset_root=$OPTARG ;;
11
+ o) output_root=$OPTARG ;;
12
+ n) num_workers=$OPTARG ;;
13
+ *) echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>]"; exit 1 ;;
14
+ esac
15
+ done
16
+
17
+ if [ -z "$dataset_root" ] || [ -z "$output_root" ]; then
18
+ echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>]"
19
+ exit 1
20
+ fi
21
+
22
+ for i in $(seq 0 $((num_workers - 1))); do
23
+ cmd="python pointcept/datasets/preprocessing/concerto/hk/preprocess_hk.py --thread_id $i \
24
+ --num_workers $num_workers \
25
+ --dataset_root $dataset_root \
26
+ --output_root $output_root"
27
+ eval "$cmd &"
28
+ done
29
+
30
+ wait
pointcept/datasets/preprocessing/concerto/hk/splits.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ pc_path = dataset_path
9
+ splits = ["train"]
10
+ split_path = os.path.join(dataset_path, "splits")
11
+ os.makedirs(split_path, exist_ok=True)
12
+ for split in splits:
13
+ pc_split_path = os.path.join(pc_path, split)
14
+ split_names = [f.name for f in os.scandir(pc_split_path) if f.is_dir()]
15
+ pc_split_path = pc_split_path.replace(dataset_path, "data/hk")
16
+ split_dict = {}
17
+ for name in split_names:
18
+ split_dict[f"{name}"] = {}
19
+ split_dict[f"{name}"]["pointclouds"] = os.path.join(pc_split_path, name)
20
+ split_dict[f"{name}"]["images"] = []
21
+ split_dict[f"{name}"]["correspondences"] = []
22
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
23
+ json.dump(split_dict, f, indent=4)
24
+
25
+
26
+ # Example usage
27
+ if __name__ == "__main__":
28
+ parser = argparse.ArgumentParser()
29
+ parser.add_argument(
30
+ "--dataset_root",
31
+ required=True,
32
+ help="Path to the ScanNet dataset containing scene folders",
33
+ )
34
+ config = parser.parse_args()
35
+ get_splits_paths(config.dataset_root)
pointcept/datasets/preprocessing/concerto/hm3d/__init__.py ADDED
File without changes
pointcept/datasets/preprocessing/concerto/hm3d/common/utils.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright (c) Facebook, Inc. and its affiliates.
4
+ # This source code is licensed under the MIT license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import math
8
+ import random
9
+ from typing import List, Optional
10
+
11
+ import habitat_sim
12
+ import numpy as np
13
+ import quaternion as qt
14
+ from habitat_sim.utils.common import quat_from_angle_axis
15
+
16
+
17
+ def get_random_quaternion() -> qt.quaternion:
18
+ return quat_from_angle_axis(
19
+ math.radians(random.uniform(-180.0, 180.0)), np.array([0, 1.0, 0])
20
+ )
21
+
22
+
23
+ def convert_heading_to_quaternion(heading: float) -> qt.quaternion:
24
+ # heading angle in degrees
25
+ return quat_from_angle_axis(math.radians(heading), np.array([0, 1.0, 0]))
26
+
27
+
28
+ def make_habitat_configuration(scene_path: str, use_sensor: bool = False):
29
+ # simulator configuration
30
+ backend_cfg = habitat_sim.SimulatorConfiguration()
31
+ backend_cfg.scene_id = scene_path
32
+
33
+ # agent configuration
34
+ sensor_cfg = habitat_sim.CameraSensorSpec()
35
+ sensor_cfg.resolution = [1080, 960]
36
+ sensor_cfg.sensor_type = habitat_sim.SensorType.DEPTH
37
+ agent_cfg = habitat_sim.agent.AgentConfiguration()
38
+ agent_cfg.sensor_specifications = [sensor_cfg] if use_sensor else []
39
+
40
+ return habitat_sim.Configuration(backend_cfg, [agent_cfg])
41
+
42
+
43
+ def robust_load_sim(scene_path: str) -> habitat_sim.Simulator:
44
+ sim_cfg = make_habitat_configuration(scene_path, use_sensor=False)
45
+ hsim = habitat_sim.Simulator(sim_cfg)
46
+ if not hsim.pathfinder.is_loaded:
47
+ hsim.close()
48
+ sim_cfg = make_habitat_configuration(scene_path, use_sensor=True)
49
+ hsim = habitat_sim.Simulator(sim_cfg)
50
+ navmesh_settings = habitat_sim.NavMeshSettings()
51
+ navmesh_settings.set_defaults()
52
+ hsim.recompute_navmesh(hsim.pathfinder, navmesh_settings)
53
+ return hsim
54
+
55
+
56
+ def get_filtered_scenes(scenes: List[str], filter_scenes_path: str) -> List[str]:
57
+ """
58
+ Filter scenes to only include valid scenes.
59
+ """
60
+ with open(filter_scenes_path, "r") as fp:
61
+ filter_scenes = fp.readlines()
62
+ filter_scenes = [f.strip("\n") for f in filter_scenes]
63
+ filtered_scenes = []
64
+ for scene in scenes:
65
+ scene_name = scene.split("/")[-1][: -len(".glb")]
66
+ if scene_name in filter_scenes:
67
+ filtered_scenes.append(scene)
68
+ return filtered_scenes
69
+
70
+
71
+ def quaternion_to_list(q: qt.quaternion):
72
+ return q.imag.tolist() + [q.real]
73
+
74
+
75
+ # -------------------------------------------------------------------------------
76
+ # Functionality from habitat-lab
77
+ # -------------------------------------------------------------------------------
78
+ def calculate_meters_per_pixel(map_resolution: int, pathfinder=None):
79
+ r"""Calculate the meters_per_pixel for a given map resolution"""
80
+ lower_bound, upper_bound = pathfinder.get_bounds()
81
+ return min(
82
+ abs(upper_bound[coord] - lower_bound[coord]) / map_resolution
83
+ for coord in [0, 2]
84
+ )
85
+
86
+
87
+ def get_topdown_map(
88
+ pathfinder,
89
+ height: float,
90
+ map_resolution: int = 1024,
91
+ meters_per_pixel: Optional[float] = None,
92
+ ) -> np.ndarray:
93
+ r"""Return a top-down occupancy map for a sim. Note, this only returns valid
94
+ values for whatever floor the agent is currently on.
95
+ :param pathfinder: A habitat-sim pathfinder instances to get the map from
96
+ :param height: The height in the environment to make the topdown map
97
+ :param map_resolution: Length of the longest side of the map. Used to calculate :p:`meters_per_pixel`
98
+ :param draw_border: Whether or not to draw a border
99
+ :param meters_per_pixel: Overrides map_resolution an
100
+ :return: Image containing 0 if occupied, 1 if unoccupied, and 2 if border (if
101
+ the flag is set).
102
+ """
103
+
104
+ if meters_per_pixel is None:
105
+ meters_per_pixel = calculate_meters_per_pixel(
106
+ map_resolution, pathfinder=pathfinder
107
+ )
108
+
109
+ top_down_map = pathfinder.get_topdown_view(
110
+ meters_per_pixel=meters_per_pixel, height=height
111
+ ).astype(np.uint8)
112
+
113
+ return np.ascontiguousarray(top_down_map)
pointcept/datasets/preprocessing/concerto/hm3d/preprocessed_hm3d.py ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for hm3d using habitat-sim
3
+ https://github.com/facebookresearch/habitat-sim/blob/main/examples/tutorials/notebooks/ECCV_2020_Navigation.ipynb
4
+
5
+ Author: Yujia Zhang (yujia.zhang.cs@gmail.com)
6
+ Please cite our work if the code is helpful to you.
7
+ """
8
+
9
+ #!/usr/bin/env python3
10
+
11
+ # Copyright (c) Facebook, Inc. and its affiliates.
12
+ # This source code is licensed under the MIT license found in the
13
+ # LICENSE file in the root directory of this source tree.
14
+
15
+ import argparse
16
+ import glob
17
+ import itertools
18
+ import multiprocessing as mp
19
+ import os
20
+ from pathlib import Path
21
+ import random
22
+ from typing import Any, Dict, List, Optional, Tuple
23
+ import habitat_sim
24
+ import imageio
25
+ import numpy as np
26
+ from sklearn.cluster import DBSCAN
27
+ from scipy.spatial import cKDTree
28
+ import trimesh
29
+ import sys
30
+
31
+ sys.path.append("../../hm3d/")
32
+ from hm3d_constants import CLASS_LABELS_40
33
+
34
+
35
+ from common.utils import (
36
+ convert_heading_to_quaternion,
37
+ get_topdown_map,
38
+ )
39
+
40
+ MPCAT2INDEX = dict([(CLASS_LABELS_40[i], i) for i in range(40)])
41
+ MPCAT2INDEX["unlabeled"] = -1
42
+ CAT2INDEX = dict()
43
+ with open(Path("../../hm3d") / "hm3dsem_category_mappings.tsv") as f:
44
+ f.readline() # raw_category category mpcat40
45
+ lines = f.readlines()
46
+ for line in lines:
47
+ line = line.strip().split("\t")
48
+ if len(line) == 2:
49
+ # L2164: "\tunknown\tunlabeled"
50
+ line.insert(0, "")
51
+ CAT2INDEX[line[1]] = MPCAT2INDEX[line[2]]
52
+
53
+
54
+ def correspondenceGet(depth, K, T, img_size):
55
+ height, width = img_size
56
+ if np.isnan(T).any() or np.isinf(T).any():
57
+ return None
58
+ pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
59
+ pixel = pixel.reshape((-1, 2))
60
+ pixel = np.hstack((pixel, np.ones((pixel.shape[0], 1))))
61
+ depth = depth.reshape((-1, 1))
62
+ valid = ~np.isinf(depth).squeeze(-1)
63
+ coord = np.zeros_like(pixel, dtype=np.float32)
64
+ coord[valid] = depth[valid] * (np.linalg.inv(K) @ pixel[valid].T).T # coord_camera
65
+ coord[valid] = coord[valid] @ T[:3, :3].T + T[:3, 3] # column then row
66
+ pixel = pixel[valid]
67
+ coord = coord[valid]
68
+ if coord.shape[0] == 0:
69
+ return None
70
+ pixel = pixel[:, :2]
71
+ coord_dict = {"pixel": pixel, "coord": coord}
72
+ return coord_dict
73
+
74
+
75
+ def uv_to_texture_color(uv, texture):
76
+ width, height = texture.size
77
+ u = int(uv[0] * width)
78
+ v = int((1 - uv[1]) * height) # Flip y-axis for image coordinates
79
+ if 0 <= u < width and 0 <= v < height:
80
+ return texture.getpixel((u, v))
81
+ else:
82
+ return 0, 0, 0
83
+
84
+
85
+ def load_hex_mapping(mapping_path):
86
+ hex2label = {}
87
+ with open(mapping_path) as f:
88
+ f.readline() # remove 'HM3D Semantic Annotations\n'
89
+ lines = f.readlines() # get the left
90
+ for line in lines:
91
+ line = line.strip().split(",")
92
+ cat = line[2].strip('"')
93
+ if cat == "trashcan":
94
+ cat = "trash can"
95
+ elif cat == "fridge":
96
+ cat = "refrigerator"
97
+ hex2label[line[1]] = dict(instance=line[0], segment=CAT2INDEX[cat])
98
+ return hex2label
99
+
100
+
101
+ def make_habitat_configuration(
102
+ scene_path: str,
103
+ hfov: int = 90,
104
+ resolution: Tuple[int] = (300, 300),
105
+ stage_json_path: Optional[str] = None,
106
+ ):
107
+ # simulator configuration
108
+ backend_cfg = habitat_sim.SimulatorConfiguration()
109
+ if stage_json_path is not None:
110
+ backend_cfg.scene_dataset_config_file = stage_json_path
111
+ backend_cfg.scene_id = "habitat/" + scene_path.split("/")[-1]
112
+ else:
113
+ backend_cfg.scene_id = scene_path
114
+
115
+ # agent configuration
116
+ rgb_sensor_cfg = habitat_sim.CameraSensorSpec()
117
+ rgb_sensor_cfg.uuid = "rgba"
118
+ rgb_sensor_cfg.sensor_type = habitat_sim.SensorType.COLOR
119
+ rgb_sensor_cfg.resolution = resolution
120
+ rgb_sensor_cfg.hfov = hfov
121
+ rgb_sensor_cfg.position = [0.0, 0.0, 0.0]
122
+ depth_sensor_cfg = habitat_sim.CameraSensorSpec()
123
+ depth_sensor_cfg.uuid = "depth"
124
+ depth_sensor_cfg.sensor_type = habitat_sim.SensorType.DEPTH
125
+ depth_sensor_cfg.resolution = resolution
126
+ depth_sensor_cfg.hfov = hfov
127
+ depth_sensor_cfg.position = [0.0, 0.0, 0.0]
128
+
129
+ agent_cfg = habitat_sim.agent.AgentConfiguration()
130
+ agent_cfg.sensor_specifications = [rgb_sensor_cfg, depth_sensor_cfg]
131
+
132
+ return habitat_sim.Configuration(backend_cfg, [agent_cfg])
133
+
134
+
135
+ def robust_load_sim(scene_path: str, **kwargs: Any) -> habitat_sim.Simulator:
136
+ sim_cfg = make_habitat_configuration(scene_path, **kwargs)
137
+ hsim = habitat_sim.Simulator(sim_cfg)
138
+ if not hsim.pathfinder.is_loaded:
139
+ navmesh_settings = habitat_sim.NavMeshSettings()
140
+ navmesh_settings.set_defaults()
141
+ hsim.recompute_navmesh(hsim.pathfinder, navmesh_settings)
142
+ return hsim
143
+
144
+
145
+ def get_floor_heights(
146
+ sim: habitat_sim.Simulator, max_points_to_sample: int = 20000
147
+ ) -> List[Dict[str, float]]:
148
+ """Get heights of different floors in a scene. This is done in two steps.
149
+ (1) Randomly samples navigable points in the scene.
150
+ (2) Cluster the points based on discretized y coordinates to get floors.
151
+
152
+ Args:
153
+ sim: habitat simulator instance
154
+ max_points_to_sample: number of navigable points to randomly sample
155
+ """
156
+ nav_points = []
157
+ for _ in range(max_points_to_sample):
158
+ nav_points.append(sim.pathfinder.get_random_navigable_point())
159
+ nav_points = np.stack(nav_points, axis=0)
160
+ y_coors = np.around(nav_points[:, 1], decimals=1)
161
+ # cluster Y coordinates
162
+ clustering = DBSCAN(eps=0.2, min_samples=2000).fit(y_coors[:, np.newaxis])
163
+ c_labels = clustering.labels_
164
+ n_clusters = len(set(c_labels)) - (1 if -1 in c_labels else 0)
165
+ # get floor extents in Y
166
+ # each cluster corresponds to points from 1 floor
167
+ floor_extents = []
168
+ core_sample_y = y_coors[clustering.core_sample_indices_]
169
+ core_sample_labels = c_labels[clustering.core_sample_indices_]
170
+ for i in range(n_clusters):
171
+ floor_min = core_sample_y[core_sample_labels == i].min().item()
172
+ floor_max = core_sample_y[core_sample_labels == i].max().item()
173
+ floor_mean = core_sample_y[core_sample_labels == i].mean().item()
174
+ floor_extents.append({"min": floor_min, "max": floor_max, "mean": floor_mean})
175
+ floor_extents = sorted(floor_extents, key=lambda x: x["mean"])
176
+
177
+ # reject floors that have too few points
178
+ max_points = 0
179
+ for fext in floor_extents:
180
+ top_down_map = get_topdown_map(sim.pathfinder, fext["mean"])
181
+ max_points = max(np.count_nonzero(top_down_map), max_points)
182
+ clean_floor_extents = []
183
+ for fext in floor_extents:
184
+ top_down_map = get_topdown_map(sim.pathfinder, fext["mean"])
185
+ num_points = np.count_nonzero(top_down_map)
186
+ if num_points < 0.2 * max_points:
187
+ continue
188
+ clean_floor_extents.append(fext)
189
+
190
+ return clean_floor_extents
191
+
192
+
193
+ def get_navmesh_extents_at_y(
194
+ sim: habitat_sim.Simulator, y_bounds: Optional[Tuple[float]] = None
195
+ ) -> Tuple[float]:
196
+ if y_bounds is None:
197
+ lower_bound, upper_bound = sim.pathfinder.get_bounds()
198
+ else:
199
+ assert len(y_bounds) == 2
200
+ assert y_bounds[0] < y_bounds[1]
201
+ navmesh_vertices = np.array(sim.pathfinder.build_navmesh_vertices())
202
+ navmesh_vertices = navmesh_vertices[
203
+ (y_bounds[0] <= navmesh_vertices[:, 1])
204
+ & (navmesh_vertices[:, 1] <= y_bounds[1])
205
+ ]
206
+ lower_bound = navmesh_vertices.min(axis=0)
207
+ upper_bound = navmesh_vertices.max(axis=0)
208
+ return (lower_bound, upper_bound)
209
+
210
+
211
+ def get_dense_navmesh_vertices(
212
+ sim: habitat_sim.Simulator, sampling_resolution: float = 0.5
213
+ ) -> np.ndarray:
214
+
215
+ navmesh_vertices = []
216
+ floor_extents = get_floor_heights(sim)
217
+ for fext in floor_extents:
218
+ l_bound, u_bound = get_navmesh_extents_at_y(
219
+ sim, y_bounds=(fext["min"] - 0.5, fext["max"] + 0.5)
220
+ )
221
+ x_range = np.arange(l_bound[0].item(), u_bound[0].item(), sampling_resolution)
222
+ y = fext["mean"]
223
+ z_range = np.arange(l_bound[2].item(), u_bound[2].item(), sampling_resolution)
224
+ for x, z in itertools.product(x_range, z_range):
225
+ if sim.pathfinder.is_navigable(np.array([x, y, z])):
226
+ navmesh_vertices.append((np.array([x, y, z])))
227
+ if len(navmesh_vertices) > 0:
228
+ navmesh_vertices = np.stack(navmesh_vertices, axis=0)
229
+ else:
230
+ navmesh_vertices = np.zeros((0, 3))
231
+ return navmesh_vertices
232
+
233
+
234
+ def get_dense_mesh_vertices(
235
+ sim: habitat_sim.Simulator, room_bounds: dict, sampling_resolution: float = 0.5
236
+ ) -> np.ndarray:
237
+ navmesh_vertices = []
238
+ for room_id, room_bound in room_bounds.items():
239
+ x_range = np.arange(
240
+ room_bound["min_x"] + 0.5, room_bound["max_x"] - 0.5, sampling_resolution
241
+ )
242
+ y_range = np.arange(
243
+ room_bound["min_y"] + 0.5, room_bound["max_y"] - 0.5, sampling_resolution
244
+ )
245
+ z_range = np.arange(
246
+ room_bound["min_z"] + 0.5, room_bound["max_z"] - 0.5, sampling_resolution
247
+ )
248
+ for x, y, z in itertools.product(x_range, y_range, z_range):
249
+ navmesh_vertices.append((np.array([x, y, z])))
250
+ if len(navmesh_vertices) > 0:
251
+ navmesh_vertices = np.stack(navmesh_vertices, axis=0)
252
+ else:
253
+ navmesh_vertices = np.zeros((0, 3))
254
+ return navmesh_vertices
255
+
256
+
257
+ def get_scene_name(scene_path, dataset):
258
+ if dataset == "replica":
259
+ scene_name = scene_path.split("/")[-2].split(".")[0]
260
+ else:
261
+ scene_name = scene_path.split("/")[-1].split(".")[0]
262
+ return scene_name
263
+
264
+
265
+ def plySave(coords, colors, save_path):
266
+ ply_path = save_path / "point_cloud.ply"
267
+ trimesh.points.PointCloud(coords, colors=colors).export(ply_path)
268
+
269
+
270
+ def _aux_fn(args):
271
+ (
272
+ density,
273
+ scene_path,
274
+ save_prefix,
275
+ hfov,
276
+ resolution,
277
+ sampling_resolution,
278
+ num_rotations,
279
+ sim_kwargs,
280
+ parse_pointclouds,
281
+ parse_depths,
282
+ ) = args
283
+ if sim_kwargs is None:
284
+ sim_kwargs = {}
285
+ sim = robust_load_sim(scene_path, hfov=hfov, resolution=resolution, **sim_kwargs)
286
+ save_prefix = Path(save_prefix)
287
+ rgb_save_prefix = save_prefix / "images"
288
+ pc_save_prefix = save_prefix
289
+ intrinsic_save_path = Path(rgb_save_prefix) / "intrinsic"
290
+ intrinsic_save_path.mkdir(parents=True, exist_ok=True)
291
+ np.save(intrinsic_save_path / "K.npy", K)
292
+ scene_path = Path(scene_path)
293
+ scene_label_path = scene_path.with_suffix(".semantic.glb")
294
+ scene_mapping_path = scene_path.with_suffix(".semantic.txt")
295
+ scene_name = scene_path.parent.name.replace("-", "_")
296
+ scene_id = scene_name.split("_")[0]
297
+ labeled = True if scene_label_path.is_file() else False
298
+
299
+ # test split (900-1000) is reserved by official
300
+ if 0 <= int(scene_id) < 800:
301
+ split = "train"
302
+ else:
303
+ split = "val"
304
+ print(f"Parsing scene {scene_name} in {split} split...")
305
+ scene = trimesh.load(scene_path)
306
+ if labeled:
307
+ labeled_scene_ = trimesh.load(scene_label_path)
308
+ labeled_scene = trimesh.Scene()
309
+ for name, mesh in labeled_scene_.geometry.items():
310
+ # some case, name in labeled scene and scene is not matched, so only use chunk id
311
+ labeled_scene.add_geometry(mesh, geom_name=name.split("_")[0])
312
+ del labeled_scene_
313
+ hex2label = load_hex_mapping(scene_mapping_path)
314
+
315
+ room_dict = {}
316
+ room_bound = {}
317
+ for name, mesh in scene.geometry.items():
318
+ room_id = "_".join(name.split("_")[1:3]).replace("group", "").replace("sub", "")
319
+ if room_id not in room_dict.keys():
320
+ room_dict[room_id] = trimesh.Scene()
321
+ room_bound[room_id] = {
322
+ "min_x": np.inf,
323
+ "max_x": -np.inf,
324
+ "min_y": np.inf,
325
+ "max_y": -np.inf,
326
+ "min_z": np.inf,
327
+ "max_z": -np.inf,
328
+ }
329
+ room_dict[room_id].add_geometry(mesh, geom_name=name.split("_")[0])
330
+ bounding_box = mesh.bounds
331
+ room_bound[room_id]["min_x"] = min(
332
+ room_bound[room_id]["min_x"], bounding_box[0][0]
333
+ )
334
+ room_bound[room_id]["max_x"] = max(
335
+ room_bound[room_id]["max_x"], bounding_box[1][0]
336
+ )
337
+ room_bound[room_id]["min_y"] = min(
338
+ room_bound[room_id]["min_y"], bounding_box[0][1]
339
+ )
340
+ room_bound[room_id]["max_y"] = max(
341
+ room_bound[room_id]["max_y"], bounding_box[1][1]
342
+ )
343
+ room_bound[room_id]["min_z"] = min(
344
+ room_bound[room_id]["min_z"], bounding_box[0][2]
345
+ )
346
+ room_bound[room_id]["max_z"] = max(
347
+ room_bound[room_id]["max_z"], bounding_box[1][2]
348
+ )
349
+
350
+ # Get dense navmesh vertices
351
+ navmesh_vertices = get_dense_mesh_vertices(
352
+ sim, room_bound, sampling_resolution=sampling_resolution
353
+ )
354
+ room_vertices = dict()
355
+ for navmesh_vertex in navmesh_vertices:
356
+ for room_id, bound in room_bound.items():
357
+ if (
358
+ bound["min_x"] < navmesh_vertex[0] < bound["max_x"]
359
+ and bound["min_z"] < navmesh_vertex[2] < bound["max_z"]
360
+ and bound["min_y"] < navmesh_vertex[1] < bound["max_y"]
361
+ ):
362
+ if room_id not in room_vertices.keys():
363
+ room_vertices[room_id] = []
364
+ room_vertices[room_id].append(navmesh_vertex)
365
+ break
366
+
367
+ for room_id, scene in room_dict.items():
368
+ if room_id not in room_vertices.keys():
369
+ continue
370
+ np.random.seed(int(scene_id + room_id.replace("_", "")))
371
+ room_coord = []
372
+ room_color = []
373
+ room_normal = []
374
+ if labeled:
375
+ room_label_color = []
376
+
377
+ for name in scene.geometry.keys():
378
+ mesh = scene.geometry[name]
379
+ num_points = int(np.sum(mesh.area_faces) / density**2)
380
+ if num_points == 0:
381
+ continue
382
+ coords, face_indices = mesh.sample(num_points, return_index=True)
383
+ faces = mesh.faces[face_indices]
384
+ triangles = mesh.vertices[faces]
385
+ bary_coords = trimesh.triangles.points_to_barycentric(triangles, coords)
386
+ uv_coords = mesh.visual.uv[faces]
387
+ sampled_uvs = np.einsum("ijk,ij->ik", uv_coords, bary_coords)
388
+
389
+ pbr_material = mesh.visual.material
390
+ texture_image = pbr_material.baseColorTexture
391
+ if texture_image is None:
392
+ continue
393
+ colors = np.array(
394
+ [uv_to_texture_color(uv, texture_image) for uv in sampled_uvs]
395
+ )
396
+ normals = mesh.vertex_normals[faces]
397
+ normals = np.einsum("ijk,ij->ik", normals, bary_coords)
398
+ room_coord.append(coords)
399
+ room_color.append(colors)
400
+ room_normal.append(normals)
401
+
402
+ if labeled:
403
+ labeled_mash = labeled_scene.geometry[name]
404
+ label_texture_image = labeled_mash.visual.material.baseColorTexture
405
+ label_color = np.array(
406
+ [uv_to_texture_color(uv, label_texture_image) for uv in sampled_uvs]
407
+ )
408
+ room_label_color.append(label_color)
409
+ if len(room_coord) == 0:
410
+ continue
411
+
412
+ room_coord = np.concatenate(room_coord, axis=0).astype("float32")
413
+ if parse_pointclouds:
414
+ room_color = np.concatenate(room_color, axis=0).astype("uint8")
415
+ room_normal = np.concatenate(room_normal, axis=0).astype("float32")
416
+ data_dict = dict(coord=room_coord, color=room_color, normal=room_normal)
417
+
418
+ if labeled:
419
+ room_label_color = np.concatenate(room_label_color, axis=0)
420
+ instance_label_color = np.unique(room_label_color, axis=0)
421
+ room_instance = -np.ones(len(room_label_color), dtype="int16")
422
+ room_segment = -np.ones(len(room_label_color), dtype="int16")
423
+ for i in range(len(instance_label_color)):
424
+ label_color = instance_label_color[i]
425
+ label_hex = "{c[0]:02x}{c[1]:02x}{c[2]:02x}".format(c=label_color)
426
+ mask = np.all(room_label_color == label_color, axis=-1)
427
+ room_instance[mask] = i
428
+ if label_hex.upper() in hex2label.keys():
429
+ room_segment[mask] = hex2label[label_hex.upper()]["segment"]
430
+ data_dict["instance"] = room_instance
431
+ data_dict["segment"] = room_segment
432
+ pc_save_path_single = (
433
+ Path(pc_save_prefix) / split / "_".join([scene_name, room_id])
434
+ )
435
+ os.makedirs(pc_save_path_single, exist_ok=True)
436
+ print(f"Saving data to {pc_save_path_single}...")
437
+ for key, value in data_dict.items():
438
+ np.save(pc_save_path_single / f"{key}.npy", value)
439
+
440
+ agent = sim.get_agent(0)
441
+ rgb_paths = []
442
+ depth_paths = []
443
+ if not sim.pathfinder.is_loaded:
444
+ sim.close()
445
+ return rgb_paths, depth_paths
446
+
447
+ vertex = sum(room_vertices[room_id]) / len(room_vertices[room_id])
448
+
449
+ T0 = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]])
450
+ Ts = []
451
+ loc_ = vertex + np.random.uniform(-0.25, 0.25, size=vertex.shape)
452
+ count = 0
453
+ loc = loc_ @ T0[:3, :3].T
454
+ init_angle = random.uniform(0, 90)
455
+ pose_save_path = (
456
+ Path(rgb_save_prefix) / split / "_".join([scene_name, room_id]) / "pose"
457
+ )
458
+ pose_save_path.mkdir(parents=True, exist_ok=True)
459
+ rgb_path = (
460
+ Path(rgb_save_prefix) / split / "_".join([scene_name, room_id]) / "color"
461
+ )
462
+ os.makedirs(rgb_path, exist_ok=True)
463
+ if parse_depths:
464
+ depth_path = (
465
+ Path(rgb_save_prefix)
466
+ / split
467
+ / "_".join([scene_name, room_id])
468
+ / "depth"
469
+ )
470
+ os.makedirs(depth_path, exist_ok=True)
471
+ for heading in np.linspace(init_angle, init_angle + 360, num_rotations + 1)[
472
+ :-1
473
+ ]:
474
+ rot = convert_heading_to_quaternion(heading)
475
+ angle = np.radians(-heading) # Replace 45 with the desired angle in degrees
476
+ R_y = np.array(
477
+ [
478
+ [np.cos(angle), 0, np.sin(angle)],
479
+ [0, 1, 0],
480
+ [-np.sin(angle), 0, np.cos(angle)],
481
+ ]
482
+ )
483
+ T = np.eye(4)
484
+ T[:3, :3] = T0[:3, :3] @ R_y
485
+ T[:3, 3] = loc_
486
+ Ts.append(T)
487
+ np.save(pose_save_path / f"{count}.npy", T)
488
+ agent_state = agent.get_state()
489
+ agent_state.position = loc
490
+ agent_state.rotation = rot
491
+ agent.set_state(agent_state, reset_sensors=True)
492
+ obs = sim.get_sensor_observations()
493
+ rgb = obs["rgba"][..., :3]
494
+ depth = obs["depth"]
495
+ rgb_path_ = rgb_path / f"{count}.png"
496
+ imageio.imwrite(rgb_path_, rgb)
497
+ if parse_depths:
498
+ depth_path_ = depth_path / f"{count}.png"
499
+ depth_img = (depth * 1000).astype(np.uint16) # Convert to mm
500
+ imageio.imwrite(depth_path_, depth_img)
501
+ coord_dict = correspondenceGet(depth, K, T, RESOLUTION)
502
+ if coord_dict is None:
503
+ correspondences = np.ones((1, 3))
504
+ else:
505
+ pixels_ = coord_dict["pixel"]
506
+ coords_ = coord_dict["coord"]
507
+ tree = cKDTree(room_coord)
508
+ dis, idx = tree.query(coords_, k=1)
509
+ idx_valid = idx[dis < 0.01]
510
+ pixels_valid = pixels_[dis < 0.01]
511
+ co_save_path = (
512
+ Path(rgb_save_prefix)
513
+ / split
514
+ / "_".join([scene_name, room_id])
515
+ / "correspondence"
516
+ )
517
+ co_save_path.mkdir(parents=True, exist_ok=True)
518
+ correspondences = np.hstack((pixels_valid, idx_valid.reshape(-1, 1)))
519
+ np.save(co_save_path / "{}.npy".format(count), correspondences)
520
+ count += 1
521
+ sim.close()
522
+
523
+
524
+ HFOV = 90
525
+ HFOV_rad = np.deg2rad(HFOV)
526
+ RESOLUTION = [720, 720]
527
+ NUM_ROTATIONS = 4
528
+ F = RESOLUTION[1] / (2 * np.tan(HFOV_rad / 2))
529
+ CX, CY = RESOLUTION[1] / 2, RESOLUTION[0] / 2
530
+ K = np.array([[F, 0, CX], [0, F, CY], [0, 0, 1]])
531
+
532
+ if __name__ == "__main__":
533
+ parser = argparse.ArgumentParser()
534
+ parser.add_argument(
535
+ "--dataset_root",
536
+ required=True,
537
+ help="Path to the ScanNet dataset containing scene folders",
538
+ )
539
+ parser.add_argument(
540
+ "--output_root",
541
+ required=True,
542
+ help="Output path where train/val folders will be located",
543
+ )
544
+ parser.add_argument(
545
+ "--parse_pointclouds",
546
+ action="store_true",
547
+ help="Parse point clouds from the scenes",
548
+ )
549
+ parser.add_argument(
550
+ "--parse_depths",
551
+ action="store_true",
552
+ help="Parse depths from the scenes",
553
+ )
554
+ parser.add_argument(
555
+ "--density",
556
+ default=0.02,
557
+ type=float,
558
+ help="Sampling density on mesh surface (m)",
559
+ )
560
+ parser.add_argument(
561
+ "--num_workers",
562
+ default=mp.cpu_count(),
563
+ type=int,
564
+ help="Num workers for preprocessing.",
565
+ )
566
+ parser.add_argument(
567
+ "--worker_id",
568
+ default=0,
569
+ type=int,
570
+ help="Num workers for preprocessing.",
571
+ )
572
+ args = parser.parse_args()
573
+
574
+ scene_list = glob.glob(os.path.join(args.dataset_root, "*", "*.glb"))
575
+ scene_list = [scene for scene in scene_list if not scene.endswith("semantic.glb")]
576
+ scene_list = [scene for scene in scene_list if not scene.endswith("basis.glb")]
577
+ scene_list = sorted(scene_list, key=lambda x: int(x.split("/")[-2].split("-")[0]))
578
+ assert len(scene_list) == 900
579
+ print(
580
+ f"Found {len(scene_list)} scenes in {args.dataset_root},using {args.num_workers} workers."
581
+ )
582
+ scene_list = [scene_list[i :: args.num_workers] for i in range(args.num_workers)][
583
+ args.worker_id
584
+ ]
585
+
586
+ pc_train_dir = os.path.join(args.output_root, "pointclouds", "train")
587
+ pc_val_dir = os.path.join(args.output_root, "pointclouds", "val")
588
+ existing_scene_ids = set()
589
+
590
+ for pc_dir in [pc_train_dir, pc_val_dir]:
591
+ if os.path.exists(pc_dir):
592
+ for folder in os.listdir(pc_dir):
593
+ if "_" in folder:
594
+ scene_id = folder.split("_")[0]
595
+ existing_scene_ids.add(scene_id)
596
+
597
+ scene_list = [
598
+ scene
599
+ for scene in scene_list
600
+ if scene.split("/")[-2].split("-")[0] not in existing_scene_ids
601
+ ]
602
+ print(f"Processing {len(scene_list)} scenes...")
603
+
604
+ for scene_path in scene_list:
605
+ inputs = (
606
+ args.density,
607
+ scene_path,
608
+ args.output_root,
609
+ HFOV,
610
+ RESOLUTION,
611
+ 0.5,
612
+ NUM_ROTATIONS,
613
+ None,
614
+ args.parse_pointclouds,
615
+ args.parse_depths,
616
+ )
617
+ _aux_fn(inputs)
pointcept/datasets/preprocessing/concerto/hm3d/preprocessed_hm3d.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ dataset_root=""
4
+ output_root=""
5
+ num_workers=8
6
+ parse_depths=false
7
+ parse_pointclouds=false
8
+
9
+ while getopts "d:o:n:pc" opt; do
10
+ case $opt in
11
+ d) dataset_root=$OPTARG ;;
12
+ o) output_root=$OPTARG ;;
13
+ n) num_workers=$OPTARG ;;
14
+ p) parse_depths=true ;;
15
+ c) parse_pointclouds=true ;;
16
+ *) echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"; exit 1 ;;
17
+ esac
18
+ done
19
+
20
+ if [ -z "$dataset_root" ] || [ -z "$output_root" ]; then
21
+ echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"
22
+ exit 1
23
+ fi
24
+
25
+ for i in $(seq 0 $((num_workers - 1))); do
26
+ cmd="python pointcept/datasets/preprocessing/concerto/hm3d/preprocess_hm3d.py --worker_id $i \
27
+ --num_workers $num_workers \
28
+ --dataset_root $dataset_root \
29
+ --output_root $output_root"
30
+
31
+ if $parse_depths; then
32
+ cmd="$cmd --parse_depths"
33
+ fi
34
+
35
+ if $parse_pointclouds; then
36
+ cmd="$cmd --parse_pointclouds"
37
+ fi
38
+
39
+ eval "$cmd &"
40
+ done
41
+
42
+ wait
pointcept/datasets/preprocessing/concerto/hm3d/splits.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ im_path = os.path.join(dataset_path, "images")
9
+ pc_path = dataset_path
10
+ splits = ["train", "val"]
11
+ split_path = os.path.join(dataset_path, "splits")
12
+ os.makedirs(split_path, exist_ok=True)
13
+ for split in splits:
14
+ im_split_path = os.path.join(im_path, split)
15
+ pc_split_path = os.path.join(pc_path, split).replace(dataset_path, "data/hm3d")
16
+ split_names = [f.name for f in os.scandir(im_split_path) if f.is_dir()]
17
+ split_dict = {}
18
+ for name in split_names:
19
+ im_split_name_path = os.path.join(im_split_path, name, "color")
20
+ co_split_name_path = os.path.join(im_split_path, name, "correspondence")
21
+ png_files = [
22
+ f
23
+ for f in os.listdir(im_split_name_path)
24
+ if f.endswith(".png") and (not f.startswith("."))
25
+ ]
26
+ png_files = sorted(png_files, key=lambda x: int(x.split(".")[0]))
27
+ # Get the full paths of the .png files
28
+ png_file_paths = [
29
+ os.path.join(im_split_name_path, f).replace(dataset_path, "data/hm3d")
30
+ for f in png_files
31
+ ]
32
+ co_file_paths = [
33
+ os.path.join(co_split_name_path, f.replace(".png", ".npy")).replace(
34
+ dataset_path, "data/hm3d"
35
+ )
36
+ for f in png_files
37
+ ]
38
+ for i in range(0, len(png_file_paths), 4):
39
+ split_dict[f"{name}_{i//4}"] = {}
40
+ split_dict[f"{name}_{i//4}"]["pointclouds"] = os.path.join(
41
+ pc_split_path, name
42
+ )
43
+ split_dict[f"{name}_{i//4}"]["images"] = png_file_paths[i : i + 4]
44
+ split_dict[f"{name}_{i//4}"]["correspondences"] = co_file_paths[
45
+ i : i + 4
46
+ ]
47
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
48
+ json.dump(split_dict, f, indent=4)
49
+
50
+
51
+ # Example usage
52
+ if __name__ == "__main__":
53
+ parser = argparse.ArgumentParser()
54
+ parser.add_argument(
55
+ "--dataset_root",
56
+ required=True,
57
+ help="Path to the ScanNet dataset containing scene folders",
58
+ )
59
+ config = parser.parse_args()
60
+ get_splits_paths(config.dataset_root)
pointcept/datasets/preprocessing/concerto/nuscenes/preprocess_nuscenes_info.py ADDED
@@ -0,0 +1,687 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for nuScenes Informantion
3
+ modified from OpenPCDet (https://github.com/open-mmlab/OpenPCDet)
4
+
5
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Yujia Zhang (yujia.zhang.cs@gmail.com)
6
+ Please cite our work if the code is helpful to you.
7
+ """
8
+
9
+ import os
10
+ from pathlib import Path
11
+ import numpy as np
12
+ import argparse
13
+ import tqdm
14
+ import pickle
15
+ from functools import reduce
16
+ from pyquaternion import Quaternion
17
+ from nuscenes.nuscenes import NuScenes
18
+ from nuscenes.utils import splits
19
+ from nuscenes.utils.geometry_utils import transform_matrix
20
+
21
+ map_name_from_general_to_detection = {
22
+ "human.pedestrian.adult": "pedestrian",
23
+ "human.pedestrian.child": "pedestrian",
24
+ "human.pedestrian.wheelchair": "ignore",
25
+ "human.pedestrian.stroller": "ignore",
26
+ "human.pedestrian.personal_mobility": "ignore",
27
+ "human.pedestrian.police_officer": "pedestrian",
28
+ "human.pedestrian.construction_worker": "pedestrian",
29
+ "animal": "ignore",
30
+ "vehicle.car": "car",
31
+ "vehicle.motorcycle": "motorcycle",
32
+ "vehicle.bicycle": "bicycle",
33
+ "vehicle.bus.bendy": "bus",
34
+ "vehicle.bus.rigid": "bus",
35
+ "vehicle.truck": "truck",
36
+ "vehicle.construction": "construction_vehicle",
37
+ "vehicle.emergency.ambulance": "ignore",
38
+ "vehicle.emergency.police": "ignore",
39
+ "vehicle.trailer": "trailer",
40
+ "movable_object.barrier": "barrier",
41
+ "movable_object.trafficcone": "traffic_cone",
42
+ "movable_object.pushable_pullable": "ignore",
43
+ "movable_object.debris": "ignore",
44
+ "static_object.bicycle_rack": "ignore",
45
+ }
46
+
47
+
48
+ cls_attr_dist = {
49
+ "barrier": {
50
+ "cycle.with_rider": 0,
51
+ "cycle.without_rider": 0,
52
+ "pedestrian.moving": 0,
53
+ "pedestrian.sitting_lying_down": 0,
54
+ "pedestrian.standing": 0,
55
+ "vehicle.moving": 0,
56
+ "vehicle.parked": 0,
57
+ "vehicle.stopped": 0,
58
+ },
59
+ "bicycle": {
60
+ "cycle.with_rider": 2791,
61
+ "cycle.without_rider": 8946,
62
+ "pedestrian.moving": 0,
63
+ "pedestrian.sitting_lying_down": 0,
64
+ "pedestrian.standing": 0,
65
+ "vehicle.moving": 0,
66
+ "vehicle.parked": 0,
67
+ "vehicle.stopped": 0,
68
+ },
69
+ "bus": {
70
+ "cycle.with_rider": 0,
71
+ "cycle.without_rider": 0,
72
+ "pedestrian.moving": 0,
73
+ "pedestrian.sitting_lying_down": 0,
74
+ "pedestrian.standing": 0,
75
+ "vehicle.moving": 9092,
76
+ "vehicle.parked": 3294,
77
+ "vehicle.stopped": 3881,
78
+ },
79
+ "car": {
80
+ "cycle.with_rider": 0,
81
+ "cycle.without_rider": 0,
82
+ "pedestrian.moving": 0,
83
+ "pedestrian.sitting_lying_down": 0,
84
+ "pedestrian.standing": 0,
85
+ "vehicle.moving": 114304,
86
+ "vehicle.parked": 330133,
87
+ "vehicle.stopped": 46898,
88
+ },
89
+ "construction_vehicle": {
90
+ "cycle.with_rider": 0,
91
+ "cycle.without_rider": 0,
92
+ "pedestrian.moving": 0,
93
+ "pedestrian.sitting_lying_down": 0,
94
+ "pedestrian.standing": 0,
95
+ "vehicle.moving": 882,
96
+ "vehicle.parked": 11549,
97
+ "vehicle.stopped": 2102,
98
+ },
99
+ "ignore": {
100
+ "cycle.with_rider": 307,
101
+ "cycle.without_rider": 73,
102
+ "pedestrian.moving": 0,
103
+ "pedestrian.sitting_lying_down": 0,
104
+ "pedestrian.standing": 0,
105
+ "vehicle.moving": 165,
106
+ "vehicle.parked": 400,
107
+ "vehicle.stopped": 102,
108
+ },
109
+ "motorcycle": {
110
+ "cycle.with_rider": 4233,
111
+ "cycle.without_rider": 8326,
112
+ "pedestrian.moving": 0,
113
+ "pedestrian.sitting_lying_down": 0,
114
+ "pedestrian.standing": 0,
115
+ "vehicle.moving": 0,
116
+ "vehicle.parked": 0,
117
+ "vehicle.stopped": 0,
118
+ },
119
+ "pedestrian": {
120
+ "cycle.with_rider": 0,
121
+ "cycle.without_rider": 0,
122
+ "pedestrian.moving": 157444,
123
+ "pedestrian.sitting_lying_down": 13939,
124
+ "pedestrian.standing": 46530,
125
+ "vehicle.moving": 0,
126
+ "vehicle.parked": 0,
127
+ "vehicle.stopped": 0,
128
+ },
129
+ "traffic_cone": {
130
+ "cycle.with_rider": 0,
131
+ "cycle.without_rider": 0,
132
+ "pedestrian.moving": 0,
133
+ "pedestrian.sitting_lying_down": 0,
134
+ "pedestrian.standing": 0,
135
+ "vehicle.moving": 0,
136
+ "vehicle.parked": 0,
137
+ "vehicle.stopped": 0,
138
+ },
139
+ "trailer": {
140
+ "cycle.with_rider": 0,
141
+ "cycle.without_rider": 0,
142
+ "pedestrian.moving": 0,
143
+ "pedestrian.sitting_lying_down": 0,
144
+ "pedestrian.standing": 0,
145
+ "vehicle.moving": 3421,
146
+ "vehicle.parked": 19224,
147
+ "vehicle.stopped": 1895,
148
+ },
149
+ "truck": {
150
+ "cycle.with_rider": 0,
151
+ "cycle.without_rider": 0,
152
+ "pedestrian.moving": 0,
153
+ "pedestrian.sitting_lying_down": 0,
154
+ "pedestrian.standing": 0,
155
+ "vehicle.moving": 21339,
156
+ "vehicle.parked": 55626,
157
+ "vehicle.stopped": 11097,
158
+ },
159
+ }
160
+
161
+
162
+ def get_available_scenes(nusc):
163
+ available_scenes = []
164
+ for scene in nusc.scene:
165
+ scene_token = scene["token"]
166
+ scene_rec = nusc.get("scene", scene_token)
167
+ sample_rec = nusc.get("sample", scene_rec["first_sample_token"])
168
+ sd_rec = nusc.get("sample_data", sample_rec["data"]["LIDAR_TOP"])
169
+ has_more_frames = True
170
+ scene_not_exist = False
171
+ while has_more_frames:
172
+ lidar_path, boxes, _ = nusc.get_sample_data(sd_rec["token"])
173
+ if not Path(lidar_path).exists():
174
+ scene_not_exist = True
175
+ break
176
+ else:
177
+ break
178
+ if scene_not_exist:
179
+ continue
180
+ available_scenes.append(scene)
181
+ return available_scenes
182
+
183
+
184
+ def get_sample_data(nusc, sample_data_token, selected_anntokens=None):
185
+ """
186
+ Returns the data path as well as all annotations related to that sample_data.
187
+ Note that the boxes are transformed into the current sensor"s coordinate frame.
188
+ Args:
189
+ nusc:
190
+ sample_data_token: Sample_data token.
191
+ selected_anntokens: If provided only return the selected annotation.
192
+
193
+ Returns:
194
+
195
+ """
196
+ # Retrieve sensor & pose records
197
+ sd_record = nusc.get("sample_data", sample_data_token)
198
+ cs_record = nusc.get("calibrated_sensor", sd_record["calibrated_sensor_token"])
199
+ sensor_record = nusc.get("sensor", cs_record["sensor_token"])
200
+ pose_record = nusc.get("ego_pose", sd_record["ego_pose_token"])
201
+
202
+ data_path = nusc.get_sample_data_path(sample_data_token)
203
+
204
+ if sensor_record["modality"] == "camera":
205
+ cam_intrinsic = np.array(cs_record["camera_intrinsic"])
206
+ else:
207
+ cam_intrinsic = None
208
+
209
+ # Retrieve all sample annotations and map to sensor coordinate system.
210
+ if selected_anntokens is not None:
211
+ boxes = list(map(nusc.get_box, selected_anntokens))
212
+ else:
213
+ boxes = nusc.get_boxes(sample_data_token)
214
+
215
+ # Make list of Box objects including coord system transforms.
216
+ box_list = []
217
+ for box in boxes:
218
+ box.velocity = nusc.box_velocity(box.token)
219
+ # Move box to ego vehicle coord system
220
+ box.translate(-np.array(pose_record["translation"]))
221
+ box.rotate(Quaternion(pose_record["rotation"]).inverse)
222
+
223
+ # Move box to sensor coord system
224
+ box.translate(-np.array(cs_record["translation"]))
225
+ box.rotate(Quaternion(cs_record["rotation"]).inverse)
226
+
227
+ box_list.append(box)
228
+
229
+ return data_path, box_list, cam_intrinsic
230
+
231
+
232
+ def quaternion_yaw(q: Quaternion) -> float:
233
+ """
234
+ Calculate the yaw angle from a quaternion.
235
+ Note that this only works for a quaternion that represents a box in lidar or global coordinate frame.
236
+ It does not work for a box in the camera frame.
237
+ :param q: Quaternion of interest.
238
+ :return: Yaw angle in radians.
239
+ """
240
+
241
+ # Project into xy plane.
242
+ v = np.dot(q.rotation_matrix, np.array([1, 0, 0]))
243
+
244
+ # Measure yaw using arctan.
245
+ yaw = np.arctan2(v[1], v[0])
246
+
247
+ return yaw
248
+
249
+
250
+ def obtain_sensor2top(
251
+ nusc, sensor_token, l2e_t, l2e_r_mat, e2g_t, e2g_r_mat, sensor_type="lidar"
252
+ ):
253
+ """Obtain the info with RT matric from general sensor to Top LiDAR.
254
+
255
+ Args:
256
+ nusc (class): Dataset class in the nuScenes dataset.
257
+ sensor_token (str): Sample data token corresponding to the
258
+ specific sensor type.
259
+ l2e_t (np.ndarray): Translation from lidar to ego in shape (1, 3).
260
+ l2e_r_mat (np.ndarray): Rotation matrix from lidar to ego
261
+ in shape (3, 3).
262
+ e2g_t (np.ndarray): Translation from ego to global in shape (1, 3).
263
+ e2g_r_mat (np.ndarray): Rotation matrix from ego to global
264
+ in shape (3, 3).
265
+ sensor_type (str): Sensor to calibrate. Default: "lidar".
266
+
267
+ Returns:
268
+ sweep (dict): Sweep information after transformation.
269
+ """
270
+ sd_rec = nusc.get("sample_data", sensor_token)
271
+ cs_record = nusc.get("calibrated_sensor", sd_rec["calibrated_sensor_token"])
272
+ pose_record = nusc.get("ego_pose", sd_rec["ego_pose_token"])
273
+ data_path = str(nusc.get_sample_data_path(sd_rec["token"]))
274
+ # if os.getcwd() in data_path: # path from lyftdataset is absolute path
275
+ # data_path = data_path.split(f"{os.getcwd()}/")[-1] # relative path
276
+ sweep = {
277
+ "data_path": data_path,
278
+ "type": sensor_type,
279
+ "sample_data_token": sd_rec["token"],
280
+ "sensor2ego_translation": cs_record["translation"],
281
+ "sensor2ego_rotation": cs_record["rotation"],
282
+ "ego2global_translation": pose_record["translation"],
283
+ "ego2global_rotation": pose_record["rotation"],
284
+ "timestamp": sd_rec["timestamp"],
285
+ }
286
+ l2e_r_s = sweep["sensor2ego_rotation"]
287
+ l2e_t_s = sweep["sensor2ego_translation"]
288
+ e2g_r_s = sweep["ego2global_rotation"]
289
+ e2g_t_s = sweep["ego2global_translation"]
290
+
291
+ # obtain the RT from sensor to Top LiDAR
292
+ # sweep->ego->global->ego'->lidar
293
+ l2e_r_s_mat = Quaternion(l2e_r_s).rotation_matrix
294
+ e2g_r_s_mat = Quaternion(e2g_r_s).rotation_matrix
295
+ R = (l2e_r_s_mat.T @ e2g_r_s_mat.T) @ (
296
+ np.linalg.inv(e2g_r_mat).T @ np.linalg.inv(l2e_r_mat).T
297
+ )
298
+ T = (l2e_t_s @ e2g_r_s_mat.T + e2g_t_s) @ (
299
+ np.linalg.inv(e2g_r_mat).T @ np.linalg.inv(l2e_r_mat).T
300
+ )
301
+ T -= (
302
+ e2g_t @ (np.linalg.inv(e2g_r_mat).T @ np.linalg.inv(l2e_r_mat).T)
303
+ + l2e_t @ np.linalg.inv(l2e_r_mat).T
304
+ ).squeeze(0)
305
+ sweep["sensor2lidar_rotation"] = R.T # points @ R.T + T
306
+ sweep["sensor2lidar_translation"] = T
307
+ return sweep
308
+
309
+
310
+ def find_closest_cam_sd_token(nusc, start_cam_sd_token, target_timestamp):
311
+ """Find the camera sample_data closest in time to a LiDAR sweep."""
312
+ start_cam_sd = nusc.get("sample_data", start_cam_sd_token)
313
+ closest_cam_sd = start_cam_sd
314
+ closest_time_diff = abs(start_cam_sd["timestamp"] - target_timestamp)
315
+
316
+ current_cam_sd = start_cam_sd
317
+ while current_cam_sd["next"]:
318
+ next_cam_sd = nusc.get("sample_data", current_cam_sd["next"])
319
+ time_diff = abs(next_cam_sd["timestamp"] - target_timestamp)
320
+ if time_diff >= closest_time_diff:
321
+ break
322
+ closest_cam_sd = next_cam_sd
323
+ closest_time_diff = time_diff
324
+ current_cam_sd = next_cam_sd
325
+
326
+ current_cam_sd = start_cam_sd
327
+ while current_cam_sd["prev"]:
328
+ prev_cam_sd = nusc.get("sample_data", current_cam_sd["prev"])
329
+ time_diff = abs(prev_cam_sd["timestamp"] - target_timestamp)
330
+ if time_diff >= closest_time_diff:
331
+ break
332
+ closest_cam_sd = prev_cam_sd
333
+ closest_time_diff = time_diff
334
+ current_cam_sd = prev_cam_sd
335
+
336
+ return closest_cam_sd["token"]
337
+
338
+
339
+ def fill_trainval_infos(
340
+ data_path, nusc, train_scenes, test=False, max_sweeps=10, with_camera=False
341
+ ):
342
+ train_nusc_infos = []
343
+ val_nusc_infos = []
344
+ progress_bar = tqdm.tqdm(
345
+ total=len(nusc.sample), desc="create_info", dynamic_ncols=True
346
+ )
347
+
348
+ ref_chan = "LIDAR_TOP" # The radar channel from which we track back n sweeps to aggregate the point cloud.
349
+ chan = "LIDAR_TOP" # The reference channel of the current sample_rec that the point clouds are mapped to.
350
+
351
+ camera_types = [
352
+ "CAM_FRONT",
353
+ "CAM_FRONT_RIGHT",
354
+ "CAM_FRONT_LEFT",
355
+ "CAM_BACK",
356
+ "CAM_BACK_LEFT",
357
+ "CAM_BACK_RIGHT",
358
+ ]
359
+
360
+ for index, sample in enumerate(nusc.sample):
361
+ progress_bar.update()
362
+
363
+ ref_sd_token = sample["data"][ref_chan]
364
+ ref_sd_rec = nusc.get("sample_data", ref_sd_token)
365
+ ref_cs_rec = nusc.get(
366
+ "calibrated_sensor", ref_sd_rec["calibrated_sensor_token"]
367
+ )
368
+ ref_pose_rec = nusc.get("ego_pose", ref_sd_rec["ego_pose_token"])
369
+ ref_time = 1e-6 * ref_sd_rec["timestamp"]
370
+
371
+ ref_lidar_path, ref_boxes, _ = get_sample_data(nusc, ref_sd_token)
372
+
373
+ ref_cam_front_token = sample["data"]["CAM_FRONT"]
374
+ ref_cam_path, _, ref_cam_intrinsic = nusc.get_sample_data(ref_cam_front_token)
375
+
376
+ # Homogeneous transform from ego car frame to reference frame
377
+ ref_from_car = transform_matrix(
378
+ ref_cs_rec["translation"], Quaternion(ref_cs_rec["rotation"]), inverse=True
379
+ )
380
+
381
+ # Homogeneous transformation matrix from global to _current_ ego car frame
382
+ car_from_global = transform_matrix(
383
+ ref_pose_rec["translation"],
384
+ Quaternion(ref_pose_rec["rotation"]),
385
+ inverse=True,
386
+ )
387
+ info = {
388
+ "lidar_path": Path(ref_lidar_path).relative_to(data_path).__str__(),
389
+ "lidar_token": ref_sd_token,
390
+ "cam_front_path": Path(ref_cam_path).relative_to(data_path).__str__(),
391
+ "cam_intrinsic": ref_cam_intrinsic,
392
+ "token": sample["token"],
393
+ "sweeps": [],
394
+ "ref_from_car": ref_from_car,
395
+ "car_from_global": car_from_global,
396
+ "timestamp": ref_time,
397
+ }
398
+ if with_camera:
399
+ info["cams"] = dict()
400
+ l2e_r = ref_cs_rec["rotation"]
401
+ l2e_t = (ref_cs_rec["translation"],)
402
+ e2g_r = ref_pose_rec["rotation"]
403
+ e2g_t = ref_pose_rec["translation"]
404
+ l2e_r_mat = Quaternion(l2e_r).rotation_matrix
405
+ e2g_r_mat = Quaternion(e2g_r).rotation_matrix
406
+
407
+ for cam in camera_types:
408
+ cam_token = sample["data"][cam]
409
+ cam_path, _, camera_intrinsics = nusc.get_sample_data(cam_token)
410
+ cam_info = obtain_sensor2top(
411
+ nusc, cam_token, l2e_t, l2e_r_mat, e2g_t, e2g_r_mat, cam
412
+ )
413
+ cam_info["data_path"] = (
414
+ Path(cam_info["data_path"]).relative_to(data_path).__str__()
415
+ )
416
+ cam_info.update(camera_intrinsics=camera_intrinsics)
417
+ info["cams"].update({cam: cam_info})
418
+
419
+ sample_data_token = sample["data"][chan]
420
+ curr_sd_rec = nusc.get("sample_data", sample_data_token)
421
+
422
+ sweeps = []
423
+ while len(sweeps) < max_sweeps - 1:
424
+ if curr_sd_rec["prev"] == "":
425
+ if len(sweeps) == 0:
426
+ sweep = {
427
+ "lidar_path": Path(ref_lidar_path)
428
+ .relative_to(data_path)
429
+ .__str__(),
430
+ "sample_data_token": curr_sd_rec["token"],
431
+ "transform_matrix": None,
432
+ "time_lag": curr_sd_rec["timestamp"] * 0,
433
+ "is_key_frame": curr_sd_rec["is_key_frame"],
434
+ }
435
+ if with_camera:
436
+ sweep_sample = nusc.get("sample", curr_sd_rec["sample_token"])
437
+ cam_infos = dict()
438
+ for cam in camera_types:
439
+ cam_token = find_closest_cam_sd_token(
440
+ nusc,
441
+ sweep_sample["data"][cam],
442
+ curr_sd_rec["timestamp"],
443
+ )
444
+ cam_path, _, camera_intrinsics = nusc.get_sample_data(
445
+ cam_token
446
+ )
447
+ cam_info = obtain_sensor2top(
448
+ nusc,
449
+ cam_token,
450
+ l2e_t,
451
+ l2e_r_mat,
452
+ e2g_t,
453
+ e2g_r_mat,
454
+ cam,
455
+ )
456
+ cam_info["data_path"] = (
457
+ Path(cam_info["data_path"])
458
+ .relative_to(data_path)
459
+ .__str__()
460
+ )
461
+ cam_info.update(camera_intrinsics=camera_intrinsics)
462
+ cam_infos.update({cam: cam_info})
463
+ sweep.update({"cams": cam_infos})
464
+ sweeps.append(sweep)
465
+ else:
466
+ sweeps.append(sweeps[-1])
467
+ else:
468
+ curr_sd_rec = nusc.get("sample_data", curr_sd_rec["prev"])
469
+ if with_camera:
470
+ sweep_sample = nusc.get("sample", curr_sd_rec["sample_token"])
471
+ cam_infos = dict()
472
+ for cam in camera_types:
473
+ cam_token = find_closest_cam_sd_token(
474
+ nusc,
475
+ sweep_sample["data"][cam],
476
+ curr_sd_rec["timestamp"],
477
+ )
478
+ cam_path, _, camera_intrinsics = nusc.get_sample_data(cam_token)
479
+ cam_info = obtain_sensor2top(
480
+ nusc, cam_token, l2e_t, l2e_r_mat, e2g_t, e2g_r_mat, cam
481
+ )
482
+ cam_info["data_path"] = (
483
+ Path(cam_info["data_path"]).relative_to(data_path).__str__()
484
+ )
485
+ cam_info.update(camera_intrinsics=camera_intrinsics)
486
+ cam_infos.update({cam: cam_info})
487
+ # Get past pose
488
+ current_pose_rec = nusc.get("ego_pose", curr_sd_rec["ego_pose_token"])
489
+ global_from_car = transform_matrix(
490
+ current_pose_rec["translation"],
491
+ Quaternion(current_pose_rec["rotation"]),
492
+ inverse=False,
493
+ )
494
+
495
+ # Homogeneous transformation matrix from sensor coordinate frame to ego car frame.
496
+ current_cs_rec = nusc.get(
497
+ "calibrated_sensor", curr_sd_rec["calibrated_sensor_token"]
498
+ )
499
+ car_from_current = transform_matrix(
500
+ current_cs_rec["translation"],
501
+ Quaternion(current_cs_rec["rotation"]),
502
+ inverse=False,
503
+ )
504
+
505
+ tm = reduce(
506
+ np.dot,
507
+ [ref_from_car, car_from_global, global_from_car, car_from_current],
508
+ )
509
+
510
+ lidar_path = nusc.get_sample_data_path(curr_sd_rec["token"])
511
+
512
+ time_lag = ref_time - 1e-6 * curr_sd_rec["timestamp"]
513
+
514
+ sweep = {
515
+ "lidar_path": Path(lidar_path).relative_to(data_path).__str__(),
516
+ "sample_data_token": curr_sd_rec["token"],
517
+ "transform_matrix": tm,
518
+ "global_from_car": global_from_car,
519
+ "car_from_current": car_from_current,
520
+ "time_lag": time_lag,
521
+ "is_key_frame": curr_sd_rec["is_key_frame"],
522
+ }
523
+ if with_camera:
524
+ sweep.update({"cams": cam_infos})
525
+
526
+ sweeps.append(sweep)
527
+
528
+ info["sweeps"] = sweeps
529
+
530
+ assert len(info["sweeps"]) == max_sweeps - 1, (
531
+ f"sweep {curr_sd_rec['token']} only has {len(info['sweeps'])} sweeps, "
532
+ f"you should duplicate to sweep num {max_sweeps - 1}"
533
+ )
534
+
535
+ if not test:
536
+ # processing gt bbox
537
+ annotations = [
538
+ nusc.get("sample_annotation", token) for token in sample["anns"]
539
+ ]
540
+
541
+ # the filtering gives 0.5~1 map improvement
542
+ num_lidar_pts = np.array([anno["num_lidar_pts"] for anno in annotations])
543
+ num_radar_pts = np.array([anno["num_radar_pts"] for anno in annotations])
544
+ mask = num_lidar_pts + num_radar_pts > 0
545
+
546
+ locs = np.array([b.center for b in ref_boxes]).reshape(-1, 3)
547
+ dims = np.array([b.wlh for b in ref_boxes]).reshape(-1, 3)[
548
+ :, [1, 0, 2]
549
+ ] # wlh == > dxdydz (lwh)
550
+ velocity = np.array([b.velocity for b in ref_boxes]).reshape(-1, 3)
551
+ rots = np.array([quaternion_yaw(b.orientation) for b in ref_boxes]).reshape(
552
+ -1, 1
553
+ )
554
+ names = np.array([b.name for b in ref_boxes])
555
+ tokens = np.array([b.token for b in ref_boxes])
556
+ gt_boxes = np.concatenate([locs, dims, rots, velocity[:, :2]], axis=1)
557
+
558
+ assert len(annotations) == len(gt_boxes) == len(velocity)
559
+
560
+ info["gt_boxes"] = gt_boxes[mask, :]
561
+ info["gt_boxes_velocity"] = velocity[mask, :]
562
+ info["gt_names"] = np.array(
563
+ [map_name_from_general_to_detection[name] for name in names]
564
+ )[mask]
565
+ info["gt_boxes_token"] = tokens[mask]
566
+ info["num_lidar_pts"] = num_lidar_pts[mask]
567
+ info["num_radar_pts"] = num_radar_pts[mask]
568
+
569
+ # processing gt segment
570
+ segment_path = nusc.get("lidarseg", ref_sd_token)["filename"]
571
+ info["gt_segment_path"] = segment_path
572
+
573
+ if sample["scene_token"] in train_scenes:
574
+ train_nusc_infos.append(info)
575
+ else:
576
+ val_nusc_infos.append(info)
577
+
578
+ progress_bar.close()
579
+ return train_nusc_infos, val_nusc_infos
580
+
581
+
582
+ if __name__ == "__main__":
583
+ parser = argparse.ArgumentParser()
584
+ parser.add_argument(
585
+ "--dataset_root", required=True, help="Path to the nuScenes dataset."
586
+ )
587
+ parser.add_argument(
588
+ "--output_root",
589
+ required=True,
590
+ help="Output path where processed information located.",
591
+ )
592
+ parser.add_argument(
593
+ "--max_sweeps", default=10, type=int, help="Max number of sweeps. Default: 10."
594
+ )
595
+ parser.add_argument(
596
+ "--with_camera",
597
+ action="store_true",
598
+ default=False,
599
+ help="Whether use camera or not.",
600
+ )
601
+ config = parser.parse_args()
602
+
603
+ print(f"Loading nuScenes tables for version v1.0-trainval...")
604
+ nusc_trainval = NuScenes(
605
+ version="v1.0-trainval", dataroot=config.dataset_root, verbose=False
606
+ )
607
+ available_scenes_trainval = get_available_scenes(nusc_trainval)
608
+ available_scene_names_trainval = [s["name"] for s in available_scenes_trainval]
609
+ print("total scene num:", len(nusc_trainval.scene))
610
+ print("exist scene num:", len(available_scenes_trainval))
611
+ assert len(available_scenes_trainval) == len(nusc_trainval.scene) == 850
612
+
613
+ print(f"Loading nuScenes tables for version v1.0-test...")
614
+ nusc_test = NuScenes(
615
+ version="v1.0-test", dataroot=config.dataset_root, verbose=False
616
+ )
617
+ available_scenes_test = get_available_scenes(nusc_test)
618
+ available_scene_names_test = [s["name"] for s in available_scenes_test]
619
+ print("total scene num:", len(nusc_test.scene))
620
+ print("exist scene num:", len(available_scenes_test))
621
+ assert len(available_scenes_test) == len(nusc_test.scene) == 150
622
+
623
+ train_scenes = splits.train
624
+ train_scenes = set(
625
+ [
626
+ available_scenes_trainval[available_scene_names_trainval.index(s)]["token"]
627
+ for s in train_scenes
628
+ ]
629
+ )
630
+ test_scenes = splits.test
631
+ test_scenes = set(
632
+ [
633
+ available_scenes_test[available_scene_names_test.index(s)]["token"]
634
+ for s in test_scenes
635
+ ]
636
+ )
637
+ print(f"Filling trainval information...")
638
+ train_nusc_infos, val_nusc_infos = fill_trainval_infos(
639
+ config.dataset_root,
640
+ nusc_trainval,
641
+ train_scenes,
642
+ test=False,
643
+ max_sweeps=config.max_sweeps,
644
+ with_camera=config.with_camera,
645
+ )
646
+ print(f"Filling test information...")
647
+ test_nusc_infos, _ = fill_trainval_infos(
648
+ config.dataset_root,
649
+ nusc_test,
650
+ test_scenes,
651
+ test=True,
652
+ max_sweeps=config.max_sweeps,
653
+ with_camera=config.with_camera,
654
+ )
655
+
656
+ print(f"Saving nuScenes information...")
657
+ os.makedirs(os.path.join(config.output_root, "info"), exist_ok=True)
658
+ print(
659
+ f"train sample: {len(train_nusc_infos)}, val sample: {len(val_nusc_infos)}, test sample: {len(test_nusc_infos)}"
660
+ )
661
+ with open(
662
+ os.path.join(
663
+ config.output_root,
664
+ "info",
665
+ f"nuscenes_infos_{config.max_sweeps}sweeps_train.pkl",
666
+ ),
667
+ "wb",
668
+ ) as f:
669
+ pickle.dump(train_nusc_infos, f)
670
+ with open(
671
+ os.path.join(
672
+ config.output_root,
673
+ "info",
674
+ f"nuscenes_infos_{config.max_sweeps}sweeps_val.pkl",
675
+ ),
676
+ "wb",
677
+ ) as f:
678
+ pickle.dump(val_nusc_infos, f)
679
+ with open(
680
+ os.path.join(
681
+ config.output_root,
682
+ "info",
683
+ f"nuscenes_infos_{config.max_sweeps}sweeps_test.pkl",
684
+ ),
685
+ "wb",
686
+ ) as f:
687
+ pickle.dump(test_nusc_infos, f)
pointcept/datasets/preprocessing/concerto/re10k/preprocess_re10k.py ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for RE10K using VGGT(https://github.com/facebookresearch/vggt)
3
+
4
+ Author: Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from torchvision.utils import save_image
10
+ import torch
11
+ from io import BytesIO
12
+ from einops import rearrange, repeat
13
+ from PIL import Image
14
+ from tqdm import tqdm
15
+ import torchvision.transforms as tf
16
+ import argparse
17
+ import os
18
+ import cv2
19
+ import numpy as np
20
+ from scipy.spatial.transform import Rotation as R
21
+ import open3d as o3d
22
+ from torchvision.utils import save_image
23
+
24
+ from vggt.models.vggt import VGGT
25
+ from vggt.utils.pose_enc import pose_encoding_to_extri_intri
26
+ import camtools as ct
27
+ from torchvision.transforms import transforms as T
28
+
29
+ _alignment = 14
30
+ target_size = 518
31
+
32
+ generator = torch.Generator()
33
+ generator.manual_seed(123)
34
+
35
+
36
+ def extract_and_align_ground_plane(
37
+ pcd,
38
+ height_percentile=20,
39
+ ransac_distance_threshold=0.01,
40
+ ransac_n=3,
41
+ ransac_iterations=1000,
42
+ max_angle_degree=40,
43
+ max_trials=6,
44
+ ):
45
+ points = np.asarray(pcd.points)
46
+ z_vals = points[:, 2]
47
+ z_thresh = np.percentile(z_vals, height_percentile)
48
+ low_indices = np.where(z_vals <= z_thresh)[0]
49
+
50
+ remaining_indices = low_indices.copy()
51
+
52
+ for trial in range(max_trials):
53
+ if len(remaining_indices) < ransac_n:
54
+ raise ValueError("Not enough points left to fit a plane.")
55
+
56
+ low_pcd = pcd.select_by_index(remaining_indices)
57
+
58
+ plane_model, inliers = low_pcd.segment_plane(
59
+ distance_threshold=ransac_distance_threshold,
60
+ ransac_n=ransac_n,
61
+ num_iterations=ransac_iterations,
62
+ )
63
+ a, b, c, d = plane_model
64
+ normal = np.array([a, b, c])
65
+ normal /= np.linalg.norm(normal)
66
+
67
+ angle = np.arccos(np.clip(np.dot(normal, [0, 0, 1]), -1.0, 1.0)) * 180 / np.pi
68
+ if angle <= max_angle_degree:
69
+ inliers_global = remaining_indices[inliers]
70
+
71
+ target = np.array([0, 0, 1])
72
+ axis = np.cross(normal, target)
73
+ axis_norm = np.linalg.norm(axis)
74
+
75
+ if axis_norm < 1e-6:
76
+ rotation_matrix = np.eye(3)
77
+ else:
78
+ axis /= axis_norm
79
+ rot_angle = np.arccos(np.clip(np.dot(normal, target), -1.0, 1.0))
80
+ rotation = R.from_rotvec(axis * rot_angle)
81
+ rotation_matrix = rotation.as_matrix()
82
+
83
+ rotated_points = points @ rotation_matrix.T
84
+ ground_points_z = rotated_points[inliers_global, 2]
85
+ offset = np.mean(ground_points_z)
86
+ rotated_points[:, 2] -= offset
87
+
88
+ aligned_pcd = o3d.geometry.PointCloud()
89
+ aligned_pcd.points = o3d.utility.Vector3dVector(rotated_points)
90
+ if pcd.has_colors():
91
+ aligned_pcd.colors = pcd.colors
92
+ if pcd.has_normals():
93
+ rotated_normals = np.asarray(pcd.normals) @ rotation_matrix.T
94
+ aligned_pcd.normals = o3d.utility.Vector3dVector(rotated_normals)
95
+
96
+ return aligned_pcd, inliers_global, rotation_matrix, offset
97
+
98
+ else:
99
+ rejected_indices = remaining_indices[inliers]
100
+ remaining_indices = np.setdiff1d(remaining_indices, rejected_indices)
101
+
102
+ raise ValueError("Failed to find a valid ground plane within max trials.")
103
+
104
+
105
+ def rotx(x, theta=90):
106
+ """
107
+ Rotate x by theta degrees around the x-axis
108
+ """
109
+ theta = np.deg2rad(theta)
110
+ rot_matrix = np.array(
111
+ [
112
+ [1, 0, 0, 0],
113
+ [0, np.cos(theta), -np.sin(theta), 0],
114
+ [0, np.sin(theta), np.cos(theta), 0],
115
+ [0, 0, 0, 1],
116
+ ]
117
+ )
118
+ return rot_matrix @ x
119
+
120
+
121
+ def Coord2zup(points, extrinsics):
122
+ """
123
+ Convert the dust3r coordinate system to the z-up coordinate system
124
+ """
125
+ points = np.concatenate([points, np.ones([points.shape[0], 1])], axis=1).T
126
+ points = rotx(points, -90)[:3].T
127
+ t = np.min(points, axis=0)
128
+ points -= t
129
+ extrinsics = rotx(extrinsics, -90)
130
+ extrinsics[:, :3, 3] -= t.T
131
+ return points, extrinsics
132
+
133
+
134
+ def resize_images_intrinsic(images, intrinsics, size, crop_size):
135
+ h, w = size
136
+ crop_h, crop_w = crop_size
137
+ intrinsics[:, 0, 0] = crop_w
138
+ intrinsics[:, 1, 1] = crop_h
139
+ cam_trans = torch.tensor(
140
+ [[w / crop_w, 0, 0], [0, h / crop_h, 0], [0, 0, 1]], dtype=torch.float32
141
+ )
142
+ intrinsics = torch.stack([cam_trans @ intrinsics_i for intrinsics_i in intrinsics])
143
+ return images, intrinsics
144
+
145
+
146
+ def calDelta(ang1, ang2, dist1, dist2):
147
+ alpha = 20
148
+ angD = ang1 - ang2
149
+ distD = dist1 - dist2
150
+ ang_s = np.linalg.norm(angD)
151
+ dist_s = np.linalg.norm(distD)
152
+ return ang_s + alpha * dist_s, ang_s, dist_s
153
+
154
+
155
+ def convert_poses(poses):
156
+ b, _ = poses.shape
157
+
158
+ # Convert the intrinsics to a 3x3 normalized K matrix.
159
+ intrinsics = torch.eye(3, dtype=torch.float32)
160
+ intrinsics = repeat(intrinsics, "h w -> b h w", b=b).clone()
161
+ fx, fy, cx, cy = poses[:, :4].T
162
+ intrinsics[:, 0, 0] = fx
163
+ intrinsics[:, 1, 1] = fy
164
+ intrinsics[:, 0, 2] = cx
165
+ intrinsics[:, 1, 2] = cy
166
+
167
+ # Convert the extrinsics to a 4x4 OpenCV-style W2C matrix.
168
+ w2c = repeat(torch.eye(4, dtype=torch.float32), "h w -> b h w", b=b).clone()
169
+ w2c[:, :3] = rearrange(poses[:, 6:], "b (h w) -> b h w", h=3, w=4)
170
+ return w2c.inverse(), intrinsics
171
+
172
+
173
+ def convert_images(images):
174
+ torch_images = []
175
+ for image in images:
176
+ image = Image.open(BytesIO(image.numpy().tobytes()))
177
+ torch_images.append(tf.ToTensor()(image))
178
+ return torch.stack(torch_images)
179
+
180
+
181
+ def plyGet(
182
+ images,
183
+ scene_dir,
184
+ pc_outputdir,
185
+ im_outputdir,
186
+ device,
187
+ dtype,
188
+ vggt_model,
189
+ depth_points="points",
190
+ conf=0.0,
191
+ parse_depths=False,
192
+ ):
193
+ # T0 = T0.numpy()
194
+ print(f"Processing scene: {scene_dir}")
195
+ height, width = images.shape[-2:]
196
+
197
+ # Skip every n frames
198
+ print(f"Processing {images.shape[0]} images")
199
+
200
+ image_num = images.shape[0]
201
+ # Load and preprocess images
202
+ images = images.to(device)
203
+
204
+ with torch.no_grad():
205
+ with torch.cuda.amp.autocast(dtype=dtype):
206
+ # Predict attributes including cameras, depth maps, and point maps.
207
+ predictions = vggt_model(images)
208
+
209
+ # Convert pose encoding to extrinsic and intrinsic matrices
210
+ Ts, Ks = pose_encoding_to_extri_intri(predictions["pose_enc"], images.shape[-2:])
211
+ Ts = Ts[0].detach().cpu().numpy()
212
+ Ks = Ks[0].detach().cpu().numpy()
213
+ Ts = ct.convert.pad_0001(Ts)
214
+ Ts_inv = np.linalg.inv(Ts)
215
+ Cs = np.array([ct.convert.T_to_C(T) for T in Ts]) # (n, 3)
216
+
217
+ # correspondance
218
+ pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
219
+ pixel = pixel.reshape((-1, 2))
220
+ pixel_ = []
221
+ for i in range(image_num):
222
+ pixel_id = np.hstack((pixel, i * np.ones((pixel.shape[0], 1))))
223
+ pixel_.append(pixel_id)
224
+ pixel = np.concatenate(pixel_, axis=0)
225
+
226
+ if depth_points == "points":
227
+ world_points = predictions["world_points"].detach().cpu().numpy()
228
+ world_points_conf = predictions["world_points_conf"].detach().cpu().numpy()
229
+ img_depths = predictions["depth"].detach().cpu().numpy().squeeze(-1)
230
+ else:
231
+ img_depths = predictions["depth"].detach().cpu().numpy().squeeze(-1)
232
+ img_depths_conf = predictions["depth_conf"].detach().cpu().numpy()
233
+ world_points = [
234
+ ct.project.im_depth_to_point_cloud(img_depth, K, T)
235
+ for img_depth, K, T in zip(img_depths[0], Ks, Ts)
236
+ ]
237
+ world_points = np.stack(world_points).reshape(
238
+ 1, image_num, images.shape[-2], images.shape[-1], -1
239
+ )
240
+ world_points_conf = img_depths_conf
241
+
242
+ masks = world_points_conf > conf
243
+ points_masks = masks.reshape((image_num * images.shape[-2] * images.shape[-1],))
244
+
245
+ # Compute view direction for each pixel
246
+ # (b n h w c) - (n, 3)
247
+ view_dirs = world_points - rearrange(Cs, "n c -> 1 n 1 1 c")
248
+ view_dirs = rearrange(view_dirs, "b n h w c -> (b n h w) c")
249
+ view_dirs = view_dirs / np.linalg.norm(view_dirs, axis=-1, keepdims=True)
250
+
251
+ # Extract points and colors
252
+ # [1, 8, 3, 294, 518]
253
+ images = images.detach().cpu().unsqueeze(0).numpy()
254
+ points = rearrange(world_points, "b n h w c -> (b n h w) c")
255
+ colors = rearrange(images, "b n c h w -> (b n h w) c")
256
+
257
+ # masks
258
+ points = points[points_masks]
259
+ colors = colors[points_masks]
260
+ pixel = pixel[points_masks]
261
+ pixel = np.concatenate((pixel, np.arange(points.shape[0]).reshape(-1, 1)), axis=-1)
262
+
263
+ points, Ts_inv = Coord2zup(points, Ts_inv)
264
+ scale = 3 / (points[:, 2].max() - points[:, 2].min())
265
+ points *= scale
266
+ Ts_inv[:, :3, 3] *= scale
267
+
268
+ # Create a point cloud
269
+ pcd = o3d.geometry.PointCloud()
270
+ pcd.points = o3d.utility.Vector3dVector(points)
271
+ pcd.colors = o3d.utility.Vector3dVector(colors)
272
+ pcd.estimate_normals()
273
+ try:
274
+ pcd, inliers, rotation_matrix, offset = extract_and_align_ground_plane(pcd)
275
+ except:
276
+ print("cannot find ground")
277
+ return
278
+ T_pcd = np.eye(4)
279
+ T_pcd[:3, :3] = rotation_matrix
280
+ T_pcd[2, 3] = -offset
281
+ Ts_inv = T_pcd @ Ts_inv
282
+ os.makedirs(pc_outputdir, exist_ok=True)
283
+ os.makedirs(im_outputdir, exist_ok=True)
284
+
285
+ # Filp normals such that normals always point to camera
286
+ # Compute the dot product between the normal and the view direction
287
+ # If the dot product is less than 0, flip the normal
288
+ normals = np.asarray(pcd.normals)
289
+ view_dirs = np.asarray(view_dirs)
290
+ view_dirs = view_dirs[points_masks]
291
+ dot_product = np.sum(normals * view_dirs, axis=-1)
292
+ flip_mask = dot_product > 0
293
+ normals[flip_mask] = -normals[flip_mask]
294
+
295
+ # Normalize normals a nd m
296
+ normals = normals / np.linalg.norm(normals, axis=-1, keepdims=True)
297
+ colors = (colors * 255).astype(np.uint8)
298
+
299
+ np.save(Path(pc_outputdir) / "coord.npy", points)
300
+ np.save(Path(pc_outputdir) / "color.npy", colors)
301
+ np.save(Path(pc_outputdir) / "normal.npy", normals)
302
+
303
+ correspondence_path = os.path.join(im_outputdir, "correspondence")
304
+ img_output_path = os.path.join(im_outputdir, "color")
305
+ Ts_output_path = os.path.join(im_outputdir, "pose")
306
+ Ks_output_path = os.path.join(im_outputdir, "intrinsic")
307
+ os.makedirs(img_output_path, exist_ok=True)
308
+ os.makedirs(Ts_output_path, exist_ok=True)
309
+ os.makedirs(Ks_output_path, exist_ok=True)
310
+ os.makedirs(correspondence_path, exist_ok=True)
311
+ for id in range(image_num):
312
+ pixels_correspondence = pixel[pixel[:, 2] == id]
313
+ pixels_correspondence = pixels_correspondence[:, [0, 1, 3]]
314
+ np.save(
315
+ os.path.join(im_outputdir, "correspondence", f"{id}.npy"),
316
+ pixels_correspondence,
317
+ )
318
+ images = torch.tensor(images.squeeze(0))
319
+ if parse_depths:
320
+ depth_output_path = os.path.join(im_outputdir, "depth")
321
+ os.makedirs(depth_output_path, exist_ok=True)
322
+ img_depths = img_depths.squeeze(0)[..., np.newaxis] * scale * 1000
323
+ for id in range(images.shape[0]):
324
+ save_image(images[id], os.path.join(img_output_path, f"{id}.png"))
325
+ np.save(os.path.join(Ts_output_path, f"{id}.npy"), Ts_inv[id])
326
+ np.save(os.path.join(Ks_output_path, f"{id}.npy"), Ks[id])
327
+ if parse_depths:
328
+ cv2.imwrite(
329
+ os.path.join(depth_output_path, f"{id}.png"),
330
+ img_depths[id].astype(np.uint16),
331
+ )
332
+
333
+
334
+ def parse_scene(
335
+ chunk_path,
336
+ vggt_model,
337
+ num_context_views,
338
+ outputdir,
339
+ device,
340
+ frame_gap,
341
+ overlap_range,
342
+ dtype,
343
+ conf,
344
+ parse_depths,
345
+ ):
346
+ print("loading from ", chunk_path)
347
+ split = chunk_path.parts[-2]
348
+ # Load the chunk.
349
+ chunk = torch.load(chunk_path)
350
+ for run_idx in range(len(chunk)):
351
+ example = chunk[run_idx]
352
+ extrinsics, intrinsics = convert_poses(example["cameras"])
353
+ scene = example["key"]
354
+
355
+ # Load the images.
356
+ context_images = example["images"]
357
+ context_images = convert_images(context_images)
358
+
359
+ v, _, _ = extrinsics.shape
360
+ ang_list = np.array(
361
+ [
362
+ R.from_matrix(ex_i[:3, :3].cpu().numpy()).as_euler("xyz", degrees=True)
363
+ for ex_i in extrinsics
364
+ ]
365
+ )
366
+ dist_list = np.array(extrinsics[:, :3, 3].reshape((-1, 3)).cpu())
367
+ context_indices = torch.randperm(v, generator=generator)
368
+ for context_index in tqdm(context_indices, "Finding context pair"):
369
+ choose_index = [context_index.item()]
370
+ while len(choose_index) < num_context_views:
371
+ # Step away from context view until the minimum overlap threshold is met.
372
+ valid_indices = []
373
+ for step in (1, -1):
374
+ if step == 1:
375
+ context_index = max(choose_index)
376
+ else:
377
+ context_index = min(choose_index)
378
+ min_distance = frame_gap[0]
379
+ max_distance = frame_gap[1]
380
+ current_index = context_index + step * min_distance
381
+
382
+ while 0 <= current_index < v:
383
+ # Compute overlap.
384
+ overlap, overlap_a, overlap_b = calDelta(
385
+ ang_list[context_index],
386
+ ang_list[current_index],
387
+ dist_list[context_index],
388
+ dist_list[current_index],
389
+ )
390
+ # print("similarity",overlap_a,overlap_b)
391
+ min_overlap = overlap_range[0]
392
+ max_overlap = overlap_range[1]
393
+ if min_overlap <= overlap <= max_overlap:
394
+ valid_indices.append((current_index, overlap_a, overlap_b))
395
+ delta = np.abs(current_index - context_index)
396
+
397
+ # Stop once the camera has panned away too much.
398
+ if overlap < min_overlap or delta > max_distance:
399
+ break
400
+
401
+ current_index += step
402
+ if valid_indices:
403
+ # Pick a random valid view. Index the resulting views.
404
+ num_options = len(valid_indices)
405
+ chosen = torch.randint(
406
+ 0, num_options, size=tuple(), generator=generator
407
+ )
408
+ chosen, overlap_a, overlap_b = valid_indices[chosen]
409
+ choose_index.append(chosen)
410
+ else:
411
+ break
412
+ if len(choose_index) < num_context_views:
413
+ continue
414
+ context_left = min(choose_index)
415
+ context_right = max(choose_index)
416
+ delta = context_right - context_left
417
+ choose_index.sort()
418
+ # Pick non-repeated random target views.
419
+
420
+ extrinsics = extrinsics[choose_index]
421
+ intrinsics = intrinsics[choose_index]
422
+ img_height, img_width = context_images.shape[-2:]
423
+ img_width_new = target_size
424
+ img_height_new = (
425
+ round(img_height * (img_width_new / img_width) / _alignment)
426
+ * _alignment
427
+ )
428
+ transform = T.Compose(
429
+ [
430
+ T.Resize((img_height_new, img_width_new)),
431
+ ]
432
+ )
433
+ imgs_list = transform(context_images)
434
+ if img_height_new > target_size:
435
+ start_y = (img_height_new - target_size) // 2
436
+ context_images = context_images[
437
+ :, :, start_y : start_y + target_size, :
438
+ ]
439
+ imgs_list, intrinsics_list = resize_images_intrinsic(
440
+ imgs_list,
441
+ intrinsics,
442
+ (img_height_new, img_width_new),
443
+ (target_size, target_size),
444
+ )
445
+ pc_outputdir = os.path.join(outputdir, split, scene)
446
+ im_outputdir = os.path.join(outputdir, "images", split, scene)
447
+ plyGet(
448
+ imgs_list[choose_index],
449
+ scene,
450
+ pc_outputdir,
451
+ im_outputdir,
452
+ device,
453
+ dtype,
454
+ vggt_model,
455
+ depth_points="points",
456
+ conf=conf,
457
+ parse_depths=parse_depths,
458
+ )
459
+ break
460
+
461
+
462
+ if __name__ == "__main__":
463
+ parser = argparse.ArgumentParser()
464
+ parser.add_argument(
465
+ "--dataset_root",
466
+ required=True,
467
+ help="Path to the ScanNet dataset containing scene folders",
468
+ )
469
+ parser.add_argument(
470
+ "--output_root",
471
+ required=True,
472
+ help="Output path where train/val folders will be located",
473
+ )
474
+ parser.add_argument(
475
+ "--num_context_views",
476
+ default=4,
477
+ type=int,
478
+ help="num_context_views",
479
+ )
480
+ parser.add_argument(
481
+ "--frame_gap",
482
+ default=[15, 135],
483
+ type=int,
484
+ help="frame_gap",
485
+ )
486
+ parser.add_argument(
487
+ "--overlap_range",
488
+ default=[5, 40],
489
+ type=int,
490
+ help="overlap_range",
491
+ )
492
+ parser.add_argument(
493
+ "--conf",
494
+ default=0.0,
495
+ type=float,
496
+ help="overlap_range",
497
+ )
498
+ parser.add_argument(
499
+ "--num_workers",
500
+ default=1,
501
+ type=int,
502
+ help="num_workers",
503
+ )
504
+ parser.add_argument(
505
+ "--thread_id",
506
+ default=0,
507
+ type=int,
508
+ help="thread_id",
509
+ )
510
+ parser.add_argument(
511
+ "--device",
512
+ default="cuda",
513
+ type=str,
514
+ help="device",
515
+ )
516
+ parser.add_argument(
517
+ "--splits",
518
+ default=["train", "test"],
519
+ nargs="+",
520
+ choices=["train", "test"],
521
+ help="Splits need to process.",
522
+ )
523
+ parser.add_argument(
524
+ "--parse_depths", action="store_true", help="Whether parse depths"
525
+ )
526
+ cfg = parser.parse_args()
527
+ VGGT_model = VGGT().to(cfg.device)
528
+ _URL = "https://huggingface.co/facebook/VGGT-1B/resolve/main/model.pt"
529
+ state_dict = torch.hub.load_state_dict_from_url(_URL)
530
+ VGGT_model.load_state_dict(state_dict)
531
+ dtype = (
532
+ torch.bfloat16 if torch.cuda.get_device_capability()[0] >= 8 else torch.float16
533
+ )
534
+
535
+ chunk_paths = []
536
+ for split in cfg.splits:
537
+ root = Path(cfg.dataset_root) / split
538
+ root_chunks = [entry for entry in root.iterdir() if ".json" not in str(entry)]
539
+ root_chunks = sorted(root_chunks)
540
+ chunk_paths.extend(root_chunks)
541
+ chunk_paths_list = np.array_split(chunk_paths, cfg.num_workers)
542
+ chunk_paths_ = chunk_paths_list[cfg.thread_id]
543
+
544
+ for chunk_path in chunk_paths_:
545
+ parse_scene(
546
+ chunk_path,
547
+ VGGT_model,
548
+ cfg.num_context_views,
549
+ cfg.output_root,
550
+ cfg.device,
551
+ cfg.frame_gap,
552
+ cfg.overlap_range,
553
+ dtype,
554
+ cfg.conf,
555
+ cfg.parse_depths,
556
+ )
pointcept/datasets/preprocessing/concerto/re10k/splits.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ im_path = os.path.join(dataset_path, "images")
9
+ pc_path = dataset_path
10
+ splits = ["train", "test"]
11
+ split_path = os.path.join(dataset_path, "splits")
12
+ os.makedirs(split_path, exist_ok=True)
13
+ for split in splits:
14
+ im_split_path = os.path.join(im_path, split)
15
+ pc_split_path = os.path.join(pc_path, split).replace(
16
+ dataset_path, "data/re10k_align"
17
+ )
18
+ split_names = [f.name for f in os.scandir(im_split_path) if f.is_dir()]
19
+ split_dict = {}
20
+ for name in split_names:
21
+ im_split_name_path = os.path.join(im_split_path, name, "color")
22
+ co_split_name_path = os.path.join(im_split_path, name, "correspondence")
23
+ png_files = [
24
+ f for f in os.listdir(im_split_name_path) if f.endswith(".png")
25
+ ]
26
+ png_files = sorted(png_files, key=lambda x: int(x.split(".")[0]))
27
+ # Get the full paths of the .png files
28
+ png_file_paths = [
29
+ os.path.join(im_split_name_path, f).replace(
30
+ dataset_path, "data/re10k_align"
31
+ )
32
+ for f in png_files
33
+ ]
34
+ co_file_paths = [
35
+ os.path.join(co_split_name_path, f.replace(".png", ".npy")).replace(
36
+ dataset_path, "data/re10k_align"
37
+ )
38
+ for f in png_files
39
+ ]
40
+ split_dict[f"{name}"] = {}
41
+ split_dict[f"{name}"]["pointclouds"] = os.path.join(pc_split_path, name)
42
+ split_dict[f"{name}"]["images"] = png_file_paths
43
+ split_dict[f"{name}"]["correspondences"] = co_file_paths
44
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
45
+ json.dump(split_dict, f, indent=4)
46
+
47
+
48
+ # Example usage
49
+ if __name__ == "__main__":
50
+ parser = argparse.ArgumentParser()
51
+ parser.add_argument(
52
+ "--dataset_root",
53
+ required=True,
54
+ help="Path to the re10k dataset containing scene folders",
55
+ )
56
+ config = parser.parse_args()
57
+ get_splits_paths(config.dataset_root)
pointcept/datasets/preprocessing/concerto/s3dis/preprocess_s3dis.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for S3DIS
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import os
9
+ import argparse
10
+ import glob
11
+ import numpy as np
12
+ import re
13
+ import json
14
+ from PIL import Image
15
+ from scipy.spatial import cKDTree
16
+ from pathlib import Path
17
+ import camtools as ct
18
+ import shutil
19
+
20
+ try:
21
+ import open3d
22
+ except ImportError:
23
+ import warnings
24
+
25
+ warnings.warn("Please install open3d for parsing normal")
26
+
27
+ try:
28
+ import trimesh
29
+ except ImportError:
30
+ import warnings
31
+
32
+ warnings.warn("Please install trimesh for parsing normal")
33
+
34
+ area_mesh_dict = {}
35
+
36
+
37
+ def correspondenceGet(mesh, K, T, img_size, coords_gt):
38
+ height, width = img_size
39
+ if np.isnan(T).any() or np.isinf(T).any():
40
+ return None
41
+ depth = ct.raycast.mesh_to_im_depth(
42
+ mesh=mesh, K=K, T=np.linalg.inv(T), height=height, width=width
43
+ )
44
+ pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
45
+ pixel = pixel.reshape((-1, 2))
46
+ pixel = np.hstack((pixel, np.ones((pixel.shape[0], 1))))
47
+ depth = depth.reshape((-1, 1))
48
+ valid = ~np.isinf(depth).squeeze(-1)
49
+ coord = np.zeros_like(pixel, dtype=np.float32)
50
+ coord[valid] = depth[valid] * (np.linalg.inv(K) @ pixel[valid].T).T # coord_camera
51
+ coord[valid] = coord[valid] @ T[:3, :3].T + T[:3, 3] # column then row
52
+ pixel = pixel[valid]
53
+ coord = coord[valid]
54
+ if coord.shape[0] == 0:
55
+ return None
56
+ pixel = pixel[:, :2]
57
+ coord_dict = {"pixel": pixel, "coord": coord}
58
+ return coord_dict
59
+
60
+
61
+ def correspondenceSave(mesh, output_dir, img_size, coords_gt, Ks, Ts):
62
+ output_dir = Path(output_dir)
63
+ (output_dir / "correspondence").mkdir(exist_ok=True)
64
+ Ks = Ks[:, :3, :3]
65
+ coords_gt_ = coords_gt
66
+ pixels_ = []
67
+ coords_ = []
68
+ index_gt = [
69
+ img_name.split(".")[0]
70
+ for img_name in os.listdir(str(output_dir / "color"))
71
+ if img_name.endswith(".png")
72
+ ]
73
+ index_gt.sort(key=lambda x: int(x.split("_frame_")[1].split("_domain_")[0]))
74
+ for i, (K, T) in enumerate(zip(Ks, Ts)):
75
+ coord_dict = correspondenceGet(mesh, K, T, img_size, coords_gt)
76
+ if coord_dict is None:
77
+ correspondences = -np.ones((1, 3))
78
+ else:
79
+ pixels_ = coord_dict["pixel"]
80
+ coords_ = coord_dict["coord"]
81
+
82
+ tree = cKDTree(coords_gt_)
83
+ dis, idx = tree.query(coords_, k=1)
84
+ idx_valid = idx[dis < 0.01]
85
+ pixels_valid = pixels_[dis < 0.01]
86
+ correspondences = np.hstack((pixels_valid, idx_valid.reshape(-1, 1)))
87
+ np.save(
88
+ output_dir / "correspondence" / "{}.npy".format(index_gt[i]),
89
+ correspondences,
90
+ )
91
+
92
+
93
+ def parse_room(
94
+ room,
95
+ angle,
96
+ rgb_gap,
97
+ raw_root,
98
+ dataset_root,
99
+ output_root,
100
+ align_angle=True,
101
+ parse_normal=False,
102
+ parse_pointclouds=False,
103
+ parse_depths=False,
104
+ ):
105
+ print("Parsing: {}".format(room))
106
+ classes = [
107
+ "ceiling",
108
+ "floor",
109
+ "wall",
110
+ "beam",
111
+ "column",
112
+ "window",
113
+ "door",
114
+ "table",
115
+ "chair",
116
+ "sofa",
117
+ "bookcase",
118
+ "board",
119
+ "clutter",
120
+ ]
121
+ class2label = {cls: i for i, cls in enumerate(classes)}
122
+ source_dir = os.path.join(dataset_root, room)
123
+ output_root_pointclouds = output_root
124
+ output_root_imgs = os.path.join(output_root, "images")
125
+ os.makedirs(output_root_imgs, exist_ok=True)
126
+ save_path_imgs = os.path.join(output_root_imgs, room)
127
+ os.makedirs(save_path_imgs, exist_ok=True)
128
+ object_path_list = sorted(glob.glob(os.path.join(source_dir, "Annotations/*.txt")))
129
+
130
+ room_coords = []
131
+ room_colors = []
132
+ room_normals = []
133
+ room_semantic_gt = []
134
+ room_instance_gt = []
135
+
136
+ for object_id, object_path in enumerate(object_path_list):
137
+ object_name = os.path.basename(object_path).split("_")[0]
138
+ obj = np.loadtxt(object_path)
139
+ coords = obj[:, :3]
140
+ colors = obj[:, 3:6]
141
+ # note: in some room there is 'stairs' class
142
+ class_name = object_name if object_name in classes else "clutter"
143
+ semantic_gt = np.repeat(class2label[class_name], coords.shape[0])
144
+ semantic_gt = semantic_gt.reshape([-1, 1])
145
+ instance_gt = np.repeat(object_id, coords.shape[0])
146
+ instance_gt = instance_gt.reshape([-1, 1])
147
+
148
+ room_coords.append(coords)
149
+ room_colors.append(colors)
150
+ room_semantic_gt.append(semantic_gt)
151
+ room_instance_gt.append(instance_gt)
152
+
153
+ room_coords = np.ascontiguousarray(np.vstack(room_coords))
154
+ room_colors = np.ascontiguousarray(np.vstack(room_colors))
155
+
156
+ if parse_normal:
157
+ x_min, z_max, y_min = np.min(room_coords, axis=0)
158
+ x_max, z_min, y_max = np.max(room_coords, axis=0)
159
+ z_max = -z_max
160
+ z_min = -z_min
161
+ max_bound = np.array([x_max, y_max, z_max]) + 0.1
162
+ min_bound = np.array([x_min, y_min, z_min]) - 0.1
163
+ bbox = open3d.geometry.AxisAlignedBoundingBox(
164
+ min_bound=min_bound, max_bound=max_bound
165
+ )
166
+ # crop room
167
+ room_mesh = (
168
+ area_mesh_dict[os.path.dirname(room)]
169
+ .crop(bbox)
170
+ .transform(
171
+ np.array([[1, 0, 0, 0], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
172
+ )
173
+ )
174
+ vertices = np.array(room_mesh.vertices)
175
+ faces = np.array(room_mesh.triangles)
176
+ vertex_normals = np.array(room_mesh.vertex_normals)
177
+ o3d_room_mesh = room_mesh
178
+ room_mesh = trimesh.Trimesh(
179
+ vertices=vertices, faces=faces, vertex_normals=vertex_normals
180
+ )
181
+ closest_points, distances, face_id = room_mesh.nearest.on_surface(room_coords)
182
+ room_normals = room_mesh.face_normals[face_id]
183
+ Ks = []
184
+ Ts = []
185
+
186
+ area = room.split("/")[0].lower()
187
+ part = room.split("/")[1]
188
+ if area == "area_5":
189
+ camera_pose_paths = sorted(
190
+ glob.glob(
191
+ os.path.join(raw_root, f"{area}*", "data", "pose", f"*_{part}_*.json")
192
+ )
193
+ )
194
+ camera_rgb_paths = sorted(
195
+ glob.glob(
196
+ os.path.join(raw_root, f"{area}*", "data", "rgb", f"*_{part}_*.png")
197
+ )
198
+ )
199
+ camera_depth_paths = sorted(
200
+ glob.glob(
201
+ os.path.join(raw_root, f"{area}*", "data", "depth", f"*_{part}_*.png")
202
+ )
203
+ )
204
+ else:
205
+ camera_pose_paths = sorted(
206
+ glob.glob(os.path.join(raw_root, area, "data", "pose", f"*_{part}_*.json"))
207
+ )
208
+ camera_rgb_paths = sorted(
209
+ glob.glob(os.path.join(raw_root, area, "data", "rgb", f"*_{part}_*.png"))
210
+ )
211
+ camera_depth_paths = sorted(
212
+ glob.glob(os.path.join(raw_root, area, "data", "depth", f"*_{part}_*.png"))
213
+ )
214
+ camera_pose_names = set(
215
+ [i.split(f"_{part}_")[0].split("camera_")[1] for i in camera_pose_paths]
216
+ )
217
+ camera_rgb_names = set(
218
+ [i.split(f"_{part}_")[0].split("camera_")[1] for i in camera_rgb_paths]
219
+ )
220
+ camera_names = camera_pose_names.intersection(camera_rgb_names)
221
+ for camera_name in camera_names:
222
+ camera_pose_paths_withname = [
223
+ path for path in camera_pose_paths if camera_name in path
224
+ ]
225
+ camera_rgb_paths_withname = [
226
+ path for path in camera_rgb_paths if camera_name in path
227
+ ]
228
+ camera_depth_paths_withname = [
229
+ path for path in camera_depth_paths if camera_name in path
230
+ ]
231
+ pose_frame_ids = set(
232
+ [re.search(r"frame_(\d+)", s).group(1) for s in camera_pose_paths_withname]
233
+ )
234
+ rgb_frame_ids = set(
235
+ [re.search(r"frame_(\d+)", s).group(1) for s in camera_rgb_paths_withname]
236
+ )
237
+ frame_ids = pose_frame_ids.intersection(rgb_frame_ids)
238
+ frame_ids_selected = sorted(list(map(int, list(frame_ids))))[::rgb_gap]
239
+ camera_pose_prefix = camera_pose_paths_withname[0].split("frame_")[0] + "frame_"
240
+ camera_pose_postfix = (
241
+ "_domain" + camera_pose_paths_withname[0].split("_domain")[1]
242
+ )
243
+ camera_pose_path_selected = [
244
+ camera_pose_prefix + str(id) + camera_pose_postfix
245
+ for id in frame_ids_selected
246
+ ]
247
+ camera_rgb_prefix = camera_rgb_paths_withname[0].split("frame_")[0] + "frame_"
248
+ camera_rgb_postfix = (
249
+ "_domain" + camera_rgb_paths_withname[0].split("_domain")[1]
250
+ )
251
+ camera_rgb_path_selected = [
252
+ camera_rgb_prefix + str(id) + camera_rgb_postfix
253
+ for id in frame_ids_selected
254
+ ]
255
+ camera_depth_prefix = (
256
+ camera_depth_paths_withname[0].split("frame_")[0] + "frame_"
257
+ )
258
+ camera_depth_postfix = (
259
+ "_domain" + camera_depth_paths_withname[0].split("_domain")[1]
260
+ )
261
+ camera_depth_path_selected = [
262
+ camera_depth_prefix + str(id) + camera_depth_postfix
263
+ for id in frame_ids_selected
264
+ ]
265
+ image_shape = Image.open(camera_rgb_path_selected[0]).size
266
+ save_path_imgs_camera = os.path.join(save_path_imgs, camera_name)
267
+ imgs_output_path = os.path.join(save_path_imgs_camera, "color")
268
+ os.makedirs(imgs_output_path, exist_ok=True)
269
+ for img_path in camera_rgb_path_selected:
270
+ shutil.copy2(img_path, imgs_output_path)
271
+ if parse_depths:
272
+ depths_output_path = os.path.join(save_path_imgs_camera, "depth")
273
+ os.makedirs(depths_output_path, exist_ok=True)
274
+ for depth_path in camera_depth_path_selected:
275
+ shutil.copy2(depth_path, depths_output_path)
276
+ Ks = []
277
+ Ts = []
278
+ Ks_output_path = os.path.join(save_path_imgs_camera, "intrinsic")
279
+ Ts_output_path = os.path.join(save_path_imgs_camera, "pose")
280
+ os.makedirs(Ks_output_path, exist_ok=True)
281
+ os.makedirs(Ts_output_path, exist_ok=True)
282
+ for id, posepath in enumerate(camera_pose_path_selected):
283
+ with open(posepath, "r") as f:
284
+ data = json.load(f)
285
+ k_matrix = np.array(data["camera_k_matrix"])
286
+ rt_matrix = np.array(data["camera_rt_matrix"])
287
+ T_matrix = np.eye(4)
288
+ T_matrix[:3, :] = rt_matrix
289
+ Ks.append(k_matrix)
290
+ Ts.append(np.linalg.inv(T_matrix))
291
+ np.save(os.path.join(Ks_output_path, f"{id}.npy"), k_matrix)
292
+ np.save(os.path.join(Ts_output_path, f"{id}.npy"), T_matrix)
293
+ Ks = np.array(Ks)
294
+ Ts = np.array(Ts)
295
+ correspondenceSave(
296
+ o3d_room_mesh,
297
+ save_path_imgs_camera,
298
+ (image_shape[1], image_shape[0]),
299
+ room_coords,
300
+ Ks,
301
+ Ts,
302
+ )
303
+
304
+ if align_angle:
305
+ angle = (2 - angle / 180) * np.pi
306
+ rot_cos, rot_sin = np.cos(angle), np.sin(angle)
307
+ rot_t = np.array([[rot_cos, -rot_sin, 0], [rot_sin, rot_cos, 0], [0, 0, 1]])
308
+ room_center = (np.max(room_coords, axis=0) + np.min(room_coords, axis=0)) / 2
309
+ room_coords = (room_coords - room_center) @ np.transpose(rot_t) + room_center
310
+ if parse_normal:
311
+ room_normals = room_normals @ np.transpose(rot_t)
312
+
313
+ if parse_pointclouds:
314
+ os.makedirs(output_root_pointclouds, exist_ok=True)
315
+ save_path_pointclouds = os.path.join(output_root_pointclouds, room)
316
+ os.makedirs(save_path_pointclouds, exist_ok=True)
317
+ room_semantic_gt = np.ascontiguousarray(np.vstack(room_semantic_gt))
318
+ room_instance_gt = np.ascontiguousarray(np.vstack(room_instance_gt))
319
+ np.save(
320
+ os.path.join(save_path_pointclouds, "coord.npy"),
321
+ room_coords.astype(np.float32),
322
+ )
323
+ np.save(
324
+ os.path.join(save_path_pointclouds, "color.npy"),
325
+ room_colors.astype(np.uint8),
326
+ )
327
+ np.save(
328
+ os.path.join(save_path_pointclouds, "segment.npy"),
329
+ room_semantic_gt.astype(np.int16),
330
+ )
331
+ np.save(
332
+ os.path.join(save_path_pointclouds, "instance.npy"),
333
+ room_instance_gt.astype(np.int16),
334
+ )
335
+
336
+ if parse_normal:
337
+ np.save(
338
+ os.path.join(save_path_pointclouds, "normal.npy"),
339
+ room_normals.astype(np.float32),
340
+ )
341
+
342
+
343
+ if __name__ == "__main__":
344
+ parser = argparse.ArgumentParser()
345
+ parser.add_argument(
346
+ "--splits",
347
+ required=True,
348
+ nargs="+",
349
+ choices=["Area_1", "Area_2", "Area_3", "Area_4", "Area_5", "Area_6"],
350
+ help="Splits need to process ([Area_1, Area_2, Area_3, Area_4, Area_5, Area_6]).",
351
+ )
352
+ parser.add_argument(
353
+ "--dataset_root", required=True, help="Path to Stanford3dDataset_v1.2 dataset"
354
+ )
355
+ parser.add_argument(
356
+ "--output_root",
357
+ required=True,
358
+ help="Output path where area folders will be located",
359
+ )
360
+ parser.add_argument(
361
+ "--pointclouds_root",
362
+ default="data/s3dis",
363
+ type=str,
364
+ help="Input path where previous pointclouds folder located",
365
+ )
366
+ parser.add_argument(
367
+ "--raw_root",
368
+ default=None,
369
+ help="Path to Stanford2d3dDataset_noXYZ dataset (optional)",
370
+ )
371
+ parser.add_argument(
372
+ "--rgb_gap",
373
+ default=5,
374
+ help="gap of rgb (optional)",
375
+ )
376
+ parser.add_argument(
377
+ "--align_angle", action="store_true", help="Whether align room angles"
378
+ )
379
+ parser.add_argument(
380
+ "--parse_normal", action="store_true", help="Whether process normal"
381
+ )
382
+ parser.add_argument(
383
+ "--parse_pointclouds", action="store_true", help="Whether parse point clouds"
384
+ )
385
+ parser.add_argument(
386
+ "--parse_depths", action="store_true", help="Whether parse depths"
387
+ )
388
+ parser.add_argument(
389
+ "--num_workers", default=1, type=int, help="Num workers for preprocessing."
390
+ )
391
+ parser.add_argument(
392
+ "--thread_id",
393
+ default=0,
394
+ type=int,
395
+ help="Thread id for parallel processing",
396
+ )
397
+ args = parser.parse_args()
398
+
399
+ if args.parse_normal:
400
+ assert args.raw_root is not None
401
+
402
+ room_list = []
403
+ angle_list = []
404
+
405
+ # Load room information
406
+ print("Loading room information ...")
407
+ for split in args.splits:
408
+ area_info = np.loadtxt(
409
+ os.path.join(
410
+ args.dataset_root,
411
+ split,
412
+ f"{split}_alignmentAngle.txt",
413
+ ),
414
+ dtype=str,
415
+ )
416
+ room_list += [os.path.join(split, room_info[0]) for room_info in area_info]
417
+ angle_list += [int(room_info[1]) for room_info in area_info]
418
+
419
+ if args.parse_normal:
420
+ # load raw mesh file to extract normal
421
+ print("Loading raw mesh file ...")
422
+ for split in args.splits:
423
+ if split != "Area_5":
424
+ mesh_dir = os.path.join(args.raw_root, split.lower(), "3d", "rgb.obj")
425
+ mesh = open3d.io.read_triangle_mesh(mesh_dir)
426
+ mesh.triangle_uvs.clear()
427
+ # trimesh_mesh = trimesh.load(mesh_dir, process=False)
428
+ else:
429
+ mesh_a_dir = os.path.join(
430
+ args.raw_root, f"{split.lower()}a", "3d", "rgb.obj"
431
+ )
432
+ mesh_b_dir = os.path.join(
433
+ args.raw_root, f"{split.lower()}b", "3d", "rgb.obj"
434
+ )
435
+ mesh_a = open3d.io.read_triangle_mesh(mesh_a_dir)
436
+ mesh_a.triangle_uvs.clear()
437
+ # trimesh_mesh_a = trimesh.load(mesh_a_dir, process=False)
438
+ mesh_b = open3d.io.read_triangle_mesh(mesh_b_dir)
439
+ mesh_b.triangle_uvs.clear()
440
+ # trimesh_mesh_b = trimesh.load(mesh_b_dir, process=False)
441
+ mesh_b = mesh_b.transform(
442
+ np.array(
443
+ [
444
+ [0, 0, -1, -4.09703582],
445
+ [0, 1, 0, 0],
446
+ [1, 0, 0, -6.22617759],
447
+ [0, 0, 0, 1],
448
+ ]
449
+ )
450
+ )
451
+ mesh = mesh_a + mesh_b
452
+ # trimesh_mesh = trimesh_mesh_a +trimesh_mesh_b
453
+ area_mesh_dict[split] = mesh
454
+ # trimesh_area_mesh_dict[split] = trimesh_mesh
455
+ print(f"{split} mesh is loaded")
456
+
457
+ room_list_list = np.array_split(room_list, args.num_workers)
458
+ room_list_ = room_list_list[args.thread_id]
459
+
460
+ for i in range(len(room_list_)):
461
+ parse_room(
462
+ room_list_[i],
463
+ angle_list[i],
464
+ args.rgb_gap,
465
+ args.raw_root,
466
+ args.dataset_root,
467
+ args.output_root,
468
+ args.align_angle,
469
+ args.parse_normal,
470
+ args.parse_pointclouds,
471
+ args.parse_depths,
472
+ )
pointcept/datasets/preprocessing/concerto/s3dis/preprocess_s3dis.sh ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ dataset_root=""
4
+ output_root=""
5
+ num_workers=6
6
+ parse_depths=false
7
+ parse_pointclouds=false
8
+
9
+ while getopts "d:o:n:r:pc" opt; do
10
+ case $opt in
11
+ d) dataset_root=$OPTARG ;;
12
+ o) output_root=$OPTARG ;;
13
+ n) num_workers=$OPTARG ;;
14
+ r) raw_root=$OPTARG ;;
15
+ p) parse_depths=true ;;
16
+ c) parse_pointclouds=true ;;
17
+ *) echo "Usage: $0 -d <dataset_root> -o <output_root> -r <raw_root> [-n <num_workers>] [-p] [-c]"; exit 1 ;;
18
+ esac
19
+ done
20
+
21
+ if [ -z "$dataset_root" ] || [ -z "$output_root" ] || [ -z "$raw_root" ]; then
22
+ echo "Usage: $0 -d <dataset_root> -o <output_root> -r <raw_root> [-n <num_workers>] [-p] [-c]"
23
+ exit 1
24
+ fi
25
+
26
+ for i in $(seq 1 $((num_workers))); do
27
+ cmd="python pointcept/datasets/preprocessing/concerto/s3dis/preprocess_s3dis.py --splits Area_$i \
28
+ --num_workers 1 \
29
+ --thread_id 0 \
30
+ --dataset_root $dataset_root \
31
+ --output_root $output_root \
32
+ --raw_root $raw_root \
33
+ --parse_normal"
34
+ if $parse_depths; then
35
+ cmd="$cmd --parse_depths"
36
+ fi
37
+
38
+ if $parse_pointclouds; then
39
+ cmd="$cmd --parse_pointclouds"
40
+ fi
41
+
42
+ eval "$cmd &"
43
+ done
44
+
45
+ wait
pointcept/datasets/preprocessing/concerto/s3dis/splits.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ im_path = os.path.join(dataset_path, "images")
9
+ pc_path = dataset_path
10
+ splits = ["Area_1", "Area_2", "Area_3", "Area_4", "Area_5", "Area_6"]
11
+ split_path = os.path.join(dataset_path, "splits")
12
+ os.makedirs(split_path, exist_ok=True)
13
+ for split in splits:
14
+ im_split_path = os.path.join(im_path, split)
15
+ pc_split_path = os.path.join(pc_path, split).replace(dataset_path, "data/s3dis")
16
+ split_names = [f.name for f in os.scandir(im_split_path) if f.is_dir()]
17
+ split_dict = {}
18
+ for name in split_names:
19
+ cam_split_name_path = os.path.join(im_split_path, name)
20
+ cam_names = [f.name for f in os.scandir(cam_split_name_path) if f.is_dir()]
21
+ for cam_name in cam_names:
22
+ im_split_name_path = os.path.join(
23
+ cam_split_name_path, cam_name, "color"
24
+ )
25
+ co_split_name_path = os.path.join(
26
+ cam_split_name_path, cam_name, "correspondence"
27
+ )
28
+ png_files = [
29
+ f for f in os.listdir(im_split_name_path) if f.endswith(".png")
30
+ ]
31
+ png_files = sorted(
32
+ png_files,
33
+ key=lambda x: int(
34
+ x.split(".")[0].split("_frame_")[1].split("_domain_")[0]
35
+ ),
36
+ )
37
+ # Get the full paths of the .png files
38
+ png_file_paths = [
39
+ os.path.join(im_split_name_path, f).replace(
40
+ dataset_path, "data/s3dis"
41
+ )
42
+ for f in png_files
43
+ ]
44
+ co_file_paths = [
45
+ os.path.join(co_split_name_path, f.replace(".png", ".npy")).replace(
46
+ dataset_path, "data/s3dis"
47
+ )
48
+ for f in png_files
49
+ ]
50
+ for i in range(0, len(png_file_paths), 4):
51
+ split_dict[f"{name}_{cam_name}_{i//4}"] = {}
52
+ split_dict[f"{name}_{cam_name}_{i//4}"]["pointclouds"] = (
53
+ os.path.join(pc_split_path, name)
54
+ )
55
+ split_dict[f"{name}_{cam_name}_{i//4}"]["images"] = png_file_paths[
56
+ i : i + 4
57
+ ]
58
+ split_dict[f"{name}_{cam_name}_{i//4}"]["correspondences"] = (
59
+ co_file_paths[i : i + 4]
60
+ )
61
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
62
+ json.dump(split_dict, f, indent=4)
63
+
64
+
65
+ # Example usage
66
+ if __name__ == "__main__":
67
+ parser = argparse.ArgumentParser()
68
+ parser.add_argument(
69
+ "--dataset_root",
70
+ required=True,
71
+ help="Path to the S3DIS dataset containing scene folders",
72
+ )
73
+ config = parser.parse_args()
74
+ get_splits_paths(config.dataset_root)
pointcept/datasets/preprocessing/concerto/scannet/preprocess_scannet.py ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for ScanNet 20/200
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import warnings
9
+
10
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
11
+
12
+ import os
13
+ import camtools as ct
14
+ import open3d as o3d
15
+ from scipy.spatial import cKDTree
16
+ import struct
17
+ import zlib
18
+ import imageio
19
+ import cv2
20
+ import argparse
21
+ import glob
22
+ import json
23
+ import plyfile
24
+ import numpy as np
25
+ import pandas as pd
26
+ import multiprocessing as mp
27
+ from pathlib import Path
28
+
29
+ # Load external constants
30
+ import sys
31
+
32
+ sys.path.append("pointcept/datasets/preprocessing/scannet/meta_data")
33
+ from scannet200_constants import VALID_CLASS_IDS_200, VALID_CLASS_IDS_20
34
+
35
+ CLOUD_FILE_PFIX = "_vh_clean_2"
36
+ SEGMENTS_FILE_PFIX = ".0.010000.segs.json"
37
+ AGGREGATIONS_FILE_PFIX = ".aggregation.json"
38
+ CLASS_IDS200 = VALID_CLASS_IDS_200
39
+ CLASS_IDS20 = VALID_CLASS_IDS_20
40
+ IGNORE_INDEX = -1
41
+
42
+ COMPRESSION_TYPE_COLOR = {-1: "unknown", 0: "raw", 1: "png", 2: "jpeg"}
43
+ COMPRESSION_TYPE_DEPTH = {
44
+ -1: "unknown",
45
+ 0: "raw_ushort",
46
+ 1: "zlib_ushort",
47
+ 2: "occi_ushort",
48
+ }
49
+
50
+
51
+ class RGBDFrame:
52
+ def load(self, file_handle):
53
+ self.camera_to_world = np.asarray(
54
+ struct.unpack("f" * 16, file_handle.read(16 * 4)), dtype=np.float32
55
+ ).reshape(4, 4)
56
+ self.timestamp_color = struct.unpack("Q", file_handle.read(8))[0]
57
+ self.timestamp_depth = struct.unpack("Q", file_handle.read(8))[0]
58
+ self.color_size_bytes = struct.unpack("Q", file_handle.read(8))[0]
59
+ self.depth_size_bytes = struct.unpack("Q", file_handle.read(8))[0]
60
+ self.color_data = b"".join(
61
+ struct.unpack(
62
+ "c" * self.color_size_bytes, file_handle.read(self.color_size_bytes)
63
+ )
64
+ )
65
+ self.depth_data = b"".join(
66
+ struct.unpack(
67
+ "c" * self.depth_size_bytes, file_handle.read(self.depth_size_bytes)
68
+ )
69
+ )
70
+
71
+ def decompress_depth(self, compression_type):
72
+ if compression_type == "zlib_ushort":
73
+ return self.decompress_depth_zlib()
74
+ else:
75
+ raise
76
+
77
+ def decompress_depth_zlib(self):
78
+ return zlib.decompress(self.depth_data)
79
+
80
+ def decompress_color(self, compression_type):
81
+ if compression_type == "jpeg":
82
+ return self.decompress_color_jpeg()
83
+ else:
84
+ raise
85
+
86
+ def decompress_color_jpeg(self):
87
+ return imageio.imread(self.color_data)
88
+
89
+
90
+ class SensorData:
91
+ def __init__(self, filename):
92
+ self.version = 4
93
+ self.load(filename)
94
+
95
+ def load(self, filename):
96
+ with open(filename, "rb") as f:
97
+ version = struct.unpack("I", f.read(4))[0]
98
+ assert self.version == version
99
+ strlen = struct.unpack("Q", f.read(8))[0]
100
+ self.sensor_name = b"".join(struct.unpack("c" * strlen, f.read(strlen)))
101
+ self.intrinsic_color = np.asarray(
102
+ struct.unpack("f" * 16, f.read(16 * 4)), dtype=np.float32
103
+ ).reshape(4, 4)
104
+ self.extrinsic_color = np.asarray(
105
+ struct.unpack("f" * 16, f.read(16 * 4)), dtype=np.float32
106
+ ).reshape(4, 4)
107
+ self.intrinsic_depth = np.asarray(
108
+ struct.unpack("f" * 16, f.read(16 * 4)), dtype=np.float32
109
+ ).reshape(4, 4)
110
+ self.extrinsic_depth = np.asarray(
111
+ struct.unpack("f" * 16, f.read(16 * 4)), dtype=np.float32
112
+ ).reshape(4, 4)
113
+ self.color_compression_type = COMPRESSION_TYPE_COLOR[
114
+ struct.unpack("i", f.read(4))[0]
115
+ ]
116
+ self.depth_compression_type = COMPRESSION_TYPE_DEPTH[
117
+ struct.unpack("i", f.read(4))[0]
118
+ ]
119
+ self.color_width = struct.unpack("I", f.read(4))[0]
120
+ self.color_height = struct.unpack("I", f.read(4))[0]
121
+ self.depth_width = struct.unpack("I", f.read(4))[0]
122
+ self.depth_height = struct.unpack("I", f.read(4))[0]
123
+ self.depth_shift = struct.unpack("f", f.read(4))[0]
124
+ num_frames = struct.unpack("Q", f.read(8))[0]
125
+ self.frames = []
126
+ for i in range(num_frames):
127
+ frame = RGBDFrame()
128
+ frame.load(f)
129
+ self.frames.append(frame)
130
+
131
+ def export_depth_images(self, output_path, image_size=None, frame_skip=1):
132
+ if not os.path.exists(output_path):
133
+ os.makedirs(output_path)
134
+ print(
135
+ "exporting", len(self.frames) // frame_skip, " depth frames to", output_path
136
+ )
137
+ for f in range(0, len(self.frames), frame_skip):
138
+ if os.path.exists((os.path.join(output_path, str(f) + ".png"))):
139
+ continue
140
+ if f % 100 == 0:
141
+ print(
142
+ "exporting",
143
+ f,
144
+ "th depth frames to",
145
+ os.path.join(output_path, str(f) + ".png"),
146
+ )
147
+
148
+ depth_data = self.frames[f].decompress_depth(self.depth_compression_type)
149
+ depth = np.fromstring(depth_data, dtype=np.uint16).reshape(
150
+ self.depth_height, self.depth_width
151
+ )
152
+ if image_size is not None:
153
+ depth = cv2.resize(
154
+ depth,
155
+ (image_size[1], image_size[0]),
156
+ interpolation=cv2.INTER_NEAREST,
157
+ )
158
+ imageio.imwrite(os.path.join(output_path, str(f) + ".png"), depth)
159
+
160
+ def export_color_images(self, output_path, image_size=None, frame_skip=1):
161
+ if not os.path.exists(output_path):
162
+ os.makedirs(output_path)
163
+ print(
164
+ "exporting", len(self.frames) // frame_skip, "color frames to", output_path
165
+ )
166
+ for f in range(0, len(self.frames), frame_skip):
167
+ if os.path.exists((os.path.join(output_path, str(f) + ".png"))):
168
+ continue
169
+ if f % 100 == 0:
170
+ print(
171
+ "exporting",
172
+ f,
173
+ "th color frames to",
174
+ os.path.join(output_path, str(f) + ".png"),
175
+ )
176
+ color = self.frames[f].decompress_color(self.color_compression_type)
177
+ if image_size is not None:
178
+ color = cv2.resize(
179
+ color,
180
+ (image_size[1], image_size[0]),
181
+ interpolation=cv2.INTER_NEAREST,
182
+ )
183
+ # imageio.imwrite(os.path.join(output_path, str(f) + '.jpg'), color)
184
+ imageio.imwrite(os.path.join(output_path, str(f) + ".png"), color)
185
+
186
+ def save_mat_to_file(self, matrix, filename):
187
+ with open(filename, "w") as f:
188
+ for line in matrix:
189
+ np.savetxt(f, line[np.newaxis], fmt="%f")
190
+
191
+ def export_poses(self, output_path, frame_skip=1):
192
+ if not os.path.exists(output_path):
193
+ os.makedirs(output_path)
194
+ print(
195
+ "exporting", len(self.frames) // frame_skip, "camera poses to", output_path
196
+ )
197
+ for f in range(0, len(self.frames), frame_skip):
198
+ np.save(
199
+ os.path.join(output_path, str(f) + ".npy"),
200
+ self.frames[f].camera_to_world,
201
+ )
202
+
203
+ def export_intrinsics(self, output_path):
204
+ if not os.path.exists(output_path):
205
+ os.makedirs(output_path)
206
+ print("exporting camera intrinsics to", output_path)
207
+ np.save(os.path.join(output_path, "intrinsic.npy"), self.intrinsic_color)
208
+
209
+
210
+ def reader(
211
+ filename,
212
+ output_path,
213
+ frame_skip,
214
+ export_color_images=False,
215
+ export_depth_images=False,
216
+ export_poses=False,
217
+ export_intrinsics=False,
218
+ ):
219
+ if not os.path.exists(output_path):
220
+ os.makedirs(output_path)
221
+
222
+ # load the data
223
+ print("loading %s..." % filename)
224
+ sd = SensorData(filename)
225
+ if export_depth_images:
226
+ sd.export_depth_images(
227
+ os.path.join(output_path, "depth"), frame_skip=frame_skip
228
+ )
229
+ if export_color_images:
230
+ sd.export_color_images(
231
+ os.path.join(output_path, "color"), frame_skip=frame_skip
232
+ )
233
+ if export_poses:
234
+ sd.export_poses(os.path.join(output_path, "pose"), frame_skip=frame_skip)
235
+ if export_intrinsics:
236
+ sd.export_intrinsics(os.path.join(output_path, "intrinsic"))
237
+ return sd.color_height, sd.color_width
238
+
239
+
240
+ def read_plymesh(filepath):
241
+ """Read ply file and return it as numpy array. Returns None if emtpy."""
242
+ with open(filepath, "rb") as f:
243
+ plydata = plyfile.PlyData.read(f)
244
+ if plydata.elements:
245
+ vertices = pd.DataFrame(plydata["vertex"].data).values
246
+ faces = np.stack(plydata["face"].data["vertex_indices"], axis=0)
247
+ return vertices, faces
248
+
249
+
250
+ # Map the raw category id to the point cloud
251
+ def point_indices_from_group(seg_indices, group, labels_pd):
252
+ group_segments = np.array(group["segments"])
253
+ label = group["label"]
254
+
255
+ # Map the category name to id
256
+ label_id20 = labels_pd[labels_pd["raw_category"] == label]["nyu40id"]
257
+ label_id20 = int(label_id20.iloc[0]) if len(label_id20) > 0 else 0
258
+ label_id200 = labels_pd[labels_pd["raw_category"] == label]["id"]
259
+ label_id200 = int(label_id200.iloc[0]) if len(label_id200) > 0 else 0
260
+
261
+ # Only store for the valid categories
262
+ if label_id20 in CLASS_IDS20:
263
+ label_id20 = CLASS_IDS20.index(label_id20)
264
+ else:
265
+ label_id20 = IGNORE_INDEX
266
+
267
+ if label_id200 in CLASS_IDS200:
268
+ label_id200 = CLASS_IDS200.index(label_id200)
269
+ else:
270
+ label_id200 = IGNORE_INDEX
271
+
272
+ # get points, where segment indices (points labelled with segment ids) are in the group segment list
273
+ point_idx = np.where(np.isin(seg_indices, group_segments))[0]
274
+ return point_idx, label_id20, label_id200
275
+
276
+
277
+ def face_normal(vertex, face):
278
+ v01 = vertex[face[:, 1]] - vertex[face[:, 0]]
279
+ v02 = vertex[face[:, 2]] - vertex[face[:, 0]]
280
+ vec = np.cross(v01, v02)
281
+ length = np.sqrt(np.sum(vec**2, axis=1, keepdims=True)) + 1.0e-8
282
+ nf = vec / length
283
+ area = length * 0.5
284
+ return nf, area
285
+
286
+
287
+ def vertex_normal(vertex, face):
288
+ nf, area = face_normal(vertex, face)
289
+ nf = nf * area
290
+
291
+ nv = np.zeros_like(vertex)
292
+ for i in range(face.shape[0]):
293
+ nv[face[i]] += nf[i]
294
+
295
+ length = np.sqrt(np.sum(nv**2, axis=1, keepdims=True)) + 1.0e-8
296
+ nv = nv / length
297
+ return nv
298
+
299
+
300
+ def correspondenceGet(mesh, K, T, img_size, coords_gt):
301
+ height, width = img_size
302
+ if np.isnan(T).any() or np.isinf(T).any():
303
+ return None
304
+
305
+ depth = ct.raycast.mesh_to_im_depth(
306
+ mesh=mesh, K=K, T=np.linalg.inv(T), height=height, width=width
307
+ )
308
+
309
+ pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
310
+ pixel = pixel.reshape((-1, 2))
311
+ pixel = np.hstack((pixel, np.ones((pixel.shape[0], 1))))
312
+ depth = depth.reshape((-1, 1))
313
+ valid = ~np.isinf(depth).squeeze(-1)
314
+ coord = np.zeros_like(pixel, dtype=np.float32)
315
+ coord[valid] = depth[valid] * (np.linalg.inv(K) @ pixel[valid].T).T # coord_camera
316
+ coord[valid] = coord[valid] @ T[:3, :3].T + T[:3, 3] # column then row
317
+
318
+ pixel = pixel[valid]
319
+ coord = coord[valid]
320
+ if coord.shape[0] == 0:
321
+ return None
322
+ pixel = pixel[:, :2]
323
+ coord_dict = {"pixel": pixel, "coord": coord}
324
+ return coord_dict
325
+
326
+
327
+ def correspondenceSave(mesh, scene_dir, coords_gt, output_dir, img_size):
328
+ os.makedirs(output_dir, exist_ok=True)
329
+ scene_dir = Path(scene_dir)
330
+ index_gt = [
331
+ img_name.split(".")[0]
332
+ for img_name in os.listdir(str(scene_dir / "color"))
333
+ if img_name.endswith(".png")
334
+ ]
335
+ index_gt = sorted(index_gt, key=lambda x: int(x))
336
+
337
+ Ks_path = str(scene_dir / "intrinsic" / "intrinsic.npy")
338
+ Ts_path = str(scene_dir / "pose")
339
+ Ts_files = sorted(
340
+ [f for f in os.listdir(Ts_path) if f.endswith(".npy")],
341
+ key=lambda x: int(x.split(".")[0]),
342
+ )
343
+
344
+ print(f"total pose num:{len(Ts_files)}")
345
+ Ts = []
346
+ for Ts_file in Ts_files:
347
+ file_path = os.path.join(Ts_path, Ts_file)
348
+ Ts_ = np.load(file_path)
349
+ Ts.append(Ts_)
350
+ Ts = np.stack(Ts)
351
+ Ks = np.load(Ks_path)
352
+
353
+ Ks = np.tile(Ks, (Ts.shape[0], 1, 1))
354
+ Ks = Ks[:, :3, :3]
355
+ coords_gt_ = coords_gt
356
+ pixels_ = []
357
+ coords_ = []
358
+
359
+ for i, (K, T) in enumerate(zip(Ks, Ts)):
360
+ coord_dict = correspondenceGet(mesh, K, T, img_size, coords_gt)
361
+ if coord_dict is None:
362
+ correspondences = -np.ones((1, 3))
363
+ else:
364
+ pixels_ = coord_dict["pixel"]
365
+ coords_ = coord_dict["coord"]
366
+ tree = cKDTree(coords_gt_)
367
+ dis, idx = tree.query(coords_, k=1)
368
+ idx_valid = idx[dis < 0.01]
369
+ pixels_valid = pixels_[dis < 0.01]
370
+ correspondences = np.hstack((pixels_valid, idx_valid.reshape(-1, 1)))
371
+ np.save(Path(output_dir) / "{}.npy".format(index_gt[i]), correspondences)
372
+
373
+
374
+ def handle_process(
375
+ scene_path,
376
+ output_path,
377
+ pointclouds_root,
378
+ labels_pd,
379
+ train_scenes,
380
+ val_scenes,
381
+ frame_gap=75,
382
+ parse_pointclouds=True,
383
+ parse_normals=True,
384
+ export_depth_images=True,
385
+ ):
386
+ pc_output_path = output_path
387
+ im_output_path = os.path.join(output_path, "images")
388
+ scene_id = os.path.basename(scene_path)
389
+ mesh_path = os.path.join(scene_path, f"{scene_id}{CLOUD_FILE_PFIX}.ply")
390
+ segments_file = os.path.join(
391
+ scene_path, f"{scene_id}{CLOUD_FILE_PFIX}{SEGMENTS_FILE_PFIX}"
392
+ )
393
+ aggregations_file = os.path.join(scene_path, f"{scene_id}{AGGREGATIONS_FILE_PFIX}")
394
+
395
+ if scene_id in train_scenes:
396
+ pc_output_path = os.path.join(pc_output_path, "train", f"{scene_id}")
397
+ pc_input_path = os.path.join(pointclouds_root, "train", f"{scene_id}")
398
+ im_output_path = os.path.join(im_output_path, "train", f"{scene_id}")
399
+ split_name = "train"
400
+ elif scene_id in val_scenes:
401
+ pc_output_path = os.path.join(pc_output_path, "val", f"{scene_id}")
402
+ pc_input_path = os.path.join(pointclouds_root, "val", f"{scene_id}")
403
+ im_output_path = os.path.join(im_output_path, "val", f"{scene_id}")
404
+ split_name = "val"
405
+ else:
406
+ pc_output_path = os.path.join(pc_output_path, "test", f"{scene_id}")
407
+ pc_input_path = os.path.join(pointclouds_root, "test", f"{scene_id}")
408
+ im_output_path = os.path.join(im_output_path, "test", f"{scene_id}")
409
+ split_name = "test"
410
+
411
+ print(f"Processing: {scene_id} in {split_name}")
412
+
413
+ if parse_pointclouds:
414
+ vertices, faces = read_plymesh(mesh_path)
415
+ coords = vertices[:, :3]
416
+ colors = vertices[:, 3:6]
417
+ save_dict = dict(
418
+ coord=coords.astype(np.float32),
419
+ color=colors.astype(np.uint8),
420
+ )
421
+ # Parse Normals
422
+ if parse_normals:
423
+ save_dict["normal"] = vertex_normal(coords, faces).astype(np.float32)
424
+
425
+ # Load segments file
426
+ if split_name != "test":
427
+ with open(segments_file) as f:
428
+ segments = json.load(f)
429
+ seg_indices = np.array(segments["segIndices"])
430
+
431
+ # Load Aggregations file
432
+ with open(aggregations_file) as f:
433
+ aggregation = json.load(f)
434
+ seg_groups = np.array(aggregation["segGroups"])
435
+
436
+ # Generate new labels
437
+ semantic_gt20 = np.ones((vertices.shape[0]), dtype=np.int16) * IGNORE_INDEX
438
+ semantic_gt200 = np.ones((vertices.shape[0]), dtype=np.int16) * IGNORE_INDEX
439
+ instance_ids = np.ones((vertices.shape[0]), dtype=np.int16) * IGNORE_INDEX
440
+ for group in seg_groups:
441
+ point_idx, label_id20, label_id200 = point_indices_from_group(
442
+ seg_indices, group, labels_pd
443
+ )
444
+
445
+ semantic_gt20[point_idx] = label_id20
446
+ semantic_gt200[point_idx] = label_id200
447
+ instance_ids[point_idx] = group["id"]
448
+
449
+ semantic_gt20 = semantic_gt20.astype(int)
450
+ semantic_gt200 = semantic_gt200.astype(int)
451
+ instance_ids = instance_ids.astype(int)
452
+
453
+ save_dict["segment20"] = semantic_gt20
454
+ save_dict["segment200"] = semantic_gt200
455
+ save_dict["instance"] = instance_ids
456
+
457
+ # Concatenate with original cloud
458
+ processed_vertices = np.hstack((semantic_gt200, instance_ids))
459
+
460
+ if np.any(np.isnan(processed_vertices)) or not np.all(
461
+ np.isfinite(processed_vertices)
462
+ ):
463
+ raise ValueError(f"Find NaN in Scene: {scene_id}")
464
+
465
+ # Save pointcloud data
466
+ os.makedirs(pc_output_path, exist_ok=True)
467
+ for key in save_dict.keys():
468
+ np.save(os.path.join(pc_output_path, f"{key}.npy"), save_dict[key])
469
+ else:
470
+ coords = np.load(os.path.join(pc_input_path, "coord.npy"))
471
+ save_dict = dict(
472
+ coord=coords.astype(np.float32),
473
+ )
474
+
475
+ # Save img data
476
+ os.makedirs(im_output_path, exist_ok=True)
477
+ sens_dir = os.path.join(scene_path, scene_id + ".sens")
478
+ print(f"Parsing sens data{sens_dir}")
479
+ h, w = reader(
480
+ sens_dir,
481
+ im_output_path,
482
+ frame_gap,
483
+ export_color_images=True,
484
+ export_depth_images=export_depth_images,
485
+ export_poses=True,
486
+ export_intrinsics=True,
487
+ )
488
+ mesh = o3d.io.read_triangle_mesh(mesh_path)
489
+ correspondenceSave(
490
+ mesh,
491
+ im_output_path,
492
+ save_dict["coord"],
493
+ os.path.join(im_output_path, "correspondence"),
494
+ (h, w),
495
+ )
496
+
497
+
498
+ if __name__ == "__main__":
499
+ parser = argparse.ArgumentParser()
500
+ parser.add_argument(
501
+ "--dataset_root",
502
+ required=True,
503
+ help="Path to the ScanNet dataset containing scene folders",
504
+ )
505
+ parser.add_argument(
506
+ "--output_root",
507
+ required=True,
508
+ help="Output path where train/val folders will be located",
509
+ )
510
+ parser.add_argument(
511
+ "--pointclouds_root",
512
+ default="data/scannet",
513
+ type=str,
514
+ help="Input path where previous pointclouds folder located",
515
+ )
516
+ parser.add_argument(
517
+ "--frame_gap", default=75, type=int, help="Frame gap for processing"
518
+ )
519
+ parser.add_argument(
520
+ "--parse_pointclouds", action="store_true", help="Whether parse point clouds"
521
+ )
522
+ parser.add_argument(
523
+ "--parse_normals", action="store_true", help="Whether parse point normals"
524
+ )
525
+ parser.add_argument(
526
+ "--parse_depths", action="store_true", help="Whether parse depths"
527
+ )
528
+ parser.add_argument(
529
+ "--num_workers",
530
+ default=mp.cpu_count(),
531
+ type=int,
532
+ help="Num workers for preprocessing.",
533
+ )
534
+ parser.add_argument(
535
+ "--thread_id",
536
+ default=0,
537
+ type=int,
538
+ help="Thread id for parallel processing",
539
+ )
540
+ config = parser.parse_args()
541
+ meta_root = Path("pointcept/datasets/preprocessing/scannet") / "meta_data"
542
+
543
+ # Load label map
544
+ labels_pd = pd.read_csv(
545
+ meta_root / "scannetv2-labels.combined.tsv",
546
+ sep="\t",
547
+ header=0,
548
+ )
549
+
550
+ # Load train/val splits
551
+ with open(meta_root / "scannetv2_train.txt") as train_file:
552
+ train_scenes = train_file.read().splitlines()
553
+ with open(meta_root / "scannetv2_val.txt") as val_file:
554
+ val_scenes = val_file.read().splitlines()
555
+
556
+ # Load scene paths
557
+ scene_paths = sorted(glob.glob(config.dataset_root + "/scans*/scene*"))
558
+ scene_paths_list = np.array_split(scene_paths, config.num_workers)
559
+ scene_paths_ = scene_paths_list[config.thread_id]
560
+ # Preprocess data.
561
+ print("Processing scenes...")
562
+ for scene_paths_i in scene_paths_:
563
+ handle_process(
564
+ scene_paths_i,
565
+ config.output_root,
566
+ config.pointclouds_root,
567
+ labels_pd,
568
+ train_scenes,
569
+ val_scenes,
570
+ config.frame_gap,
571
+ config.parse_pointclouds,
572
+ config.parse_normals,
573
+ config.parse_depths,
574
+ )
pointcept/datasets/preprocessing/concerto/scannet/preprocess_scannet.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ dataset_root=""
4
+ output_root=""
5
+ num_workers=16
6
+ parse_depths=false
7
+ parse_pointclouds=false
8
+
9
+ while getopts "d:o:n:pc" opt; do
10
+ case $opt in
11
+ d) dataset_root=$OPTARG ;;
12
+ o) output_root=$OPTARG ;;
13
+ n) num_workers=$OPTARG ;;
14
+ p) parse_depths=true ;;
15
+ c) parse_pointclouds=true ;;
16
+ *) echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"; exit 1 ;;
17
+ esac
18
+ done
19
+
20
+ if [ -z "$dataset_root" ] || [ -z "$output_root" ]; then
21
+ echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"
22
+ exit 1
23
+ fi
24
+
25
+ for i in $(seq 0 $((num_workers - 1))); do
26
+ cmd="python pointcept/datasets/preprocessing/concerto/scannet/preprocess_scannet.py --thread_id $i \
27
+ --num_workers $num_workers \
28
+ --dataset_root $dataset_root \
29
+ --output_root $output_root \
30
+ --parse_normal"
31
+ if $parse_depths; then
32
+ cmd="$cmd --parse_depths"
33
+ fi
34
+
35
+ if $parse_pointclouds; then
36
+ cmd="$cmd --parse_pointclouds"
37
+ fi
38
+
39
+ eval "$cmd &"
40
+ done
41
+
42
+ wait
pointcept/datasets/preprocessing/concerto/scannet/splits.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ im_path = os.path.join(dataset_path, "images")
9
+ pc_path = dataset_path
10
+ splits = ["train", "val", "test"]
11
+ split_path = os.path.join(dataset_path, "splits")
12
+ os.makedirs(split_path, exist_ok=True)
13
+ for split in splits:
14
+ im_split_path = os.path.join(im_path, split)
15
+ pc_split_path = os.path.join(pc_path, split).replace(
16
+ dataset_path, "data/scannet"
17
+ )
18
+ split_names = [f.name for f in os.scandir(im_split_path) if f.is_dir()]
19
+ split_dict = {}
20
+ for name in split_names:
21
+ im_split_name_path = os.path.join(im_split_path, name, "color")
22
+ co_split_name_path = os.path.join(im_split_path, name, "correspondence")
23
+ png_files = [
24
+ f for f in os.listdir(im_split_name_path) if f.endswith(".png")
25
+ ]
26
+ png_files = sorted(png_files, key=lambda x: int(x.split(".")[0]))
27
+ # Get the full paths of the .png files
28
+ png_file_paths = [
29
+ os.path.join(im_split_name_path, f).replace(
30
+ dataset_path, "data/scannet"
31
+ )
32
+ for f in png_files
33
+ ]
34
+ co_file_paths = [
35
+ os.path.join(co_split_name_path, f.replace(".png", ".npy")).replace(
36
+ dataset_path, "data/scannet"
37
+ )
38
+ for f in png_files
39
+ ]
40
+ for i in range(0, len(png_file_paths), 4):
41
+ split_dict[f"{name}_{i//4}"] = {}
42
+ split_dict[f"{name}_{i//4}"]["pointclouds"] = os.path.join(
43
+ pc_split_path, name
44
+ )
45
+ split_dict[f"{name}_{i//4}"]["images"] = png_file_paths[i : i + 4]
46
+ split_dict[f"{name}_{i//4}"]["correspondences"] = co_file_paths[
47
+ i : i + 4
48
+ ]
49
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
50
+ json.dump(split_dict, f, indent=4)
51
+
52
+
53
+ # Example usage
54
+ if __name__ == "__main__":
55
+ parser = argparse.ArgumentParser()
56
+ parser.add_argument(
57
+ "--dataset_root",
58
+ required=True,
59
+ help="Path to the ScanNet dataset containing scene folders",
60
+ )
61
+ config = parser.parse_args()
62
+ get_splits_paths(config.dataset_root)
pointcept/datasets/preprocessing/concerto/scannetpp/preprocess_scannetpp.py ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for ScanNet 20/200
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import warnings
9
+
10
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
11
+ import argparse
12
+ import json
13
+ import torch
14
+ import shutil
15
+ import os
16
+ import torch
17
+ import camtools as ct
18
+ import open3d as o3d
19
+ from scipy.spatial import cKDTree
20
+ import cv2
21
+ import numpy as np
22
+ import pandas as pd
23
+ import multiprocessing as mp
24
+ from collections import OrderedDict
25
+ from scipy.spatial.transform import Rotation
26
+ from pathlib import Path
27
+ import re
28
+
29
+ try:
30
+ import pointseg
31
+ except:
32
+ # Pointseg is located in libs/pointseg
33
+ warnings.warn("Pointseg is not installed, superpoint segmentation will be skipped.")
34
+ pointseg = None
35
+
36
+ REGEXPR_DSLR = re.compile(r".*DSC(?P<frameid>\d+).JPG$")
37
+ REGEXPR_IPHONE = re.compile(r"frame_(?P<frameid>\d+).jpg$")
38
+
39
+
40
+ def pose_from_qwxyz_txyz(elems):
41
+ qw, qx, qy, qz, tx, ty, tz = map(float, elems)
42
+ pose = np.eye(4)
43
+ pose[:3, :3] = Rotation.from_quat((qx, qy, qz, qw)).as_matrix()
44
+ pose[:3, 3] = (tx, ty, tz)
45
+ c2w = np.linalg.inv(pose) # returns cam2world
46
+ return torch.tensor(c2w, dtype=torch.float32)
47
+
48
+
49
+ def get_frame_number(name, cam_type="dslr"):
50
+ if cam_type == "dslr":
51
+ regex_expr = REGEXPR_DSLR
52
+ elif cam_type == "iphone":
53
+ regex_expr = REGEXPR_IPHONE
54
+ else:
55
+ raise NotImplementedError(f"wrong {cam_type=} for get_frame_number")
56
+ matches = re.match(regex_expr, name)
57
+ return matches["frameid"]
58
+
59
+
60
+ def colmap_to_opencv_intrinsics(K):
61
+ """
62
+ Modify camera intrinsics to follow a different convention.
63
+ Coordinates of the center of the top-left pixels are by default:
64
+ - (0.5, 0.5) in Colmap
65
+ - (0,0) in OpenCV
66
+ """
67
+ K = K.copy()
68
+ K[0, 2] -= 0.5
69
+ K[1, 2] -= 0.5
70
+ return K
71
+
72
+
73
+ def opencv_to_colmap_intrinsics(K):
74
+ """
75
+ Modify camera intrinsics to follow a different convention.
76
+ Coordinates of the center of the top-left pixels are by default:
77
+ - (0.5, 0.5) in Colmap
78
+ - (0,0) in OpenCV
79
+ """
80
+ K = K.copy()
81
+ K[0, 2] += 0.5
82
+ K[1, 2] += 0.5
83
+ return K
84
+
85
+
86
+ def load_sfm(sfm_dir, cam_type="dslr"):
87
+ # load cameras
88
+ with open(os.path.join(sfm_dir, "cameras.txt"), "r") as f:
89
+ raw = f.read().splitlines()[3:] # skip header
90
+
91
+ intrinsics = {}
92
+ for camera in raw:
93
+ camera = camera.split(" ")
94
+ intrinsics[int(camera[0])] = [camera[1]] + [float(cam) for cam in camera[2:]]
95
+ ins = intrinsics[int(camera[0])]
96
+ w, h, ins = undistort_ins(ins)
97
+ # load images
98
+ with open(os.path.join(sfm_dir, "images.txt"), "r") as f:
99
+ raw = f.read().splitlines()
100
+ raw = [line for line in raw if not line.startswith("#")] # skip header
101
+
102
+ img_idx = {}
103
+ img_infos = {}
104
+ for image, points in zip(raw[0::2], raw[1::2]):
105
+ image = image.split(" ")
106
+ points = points.split(" ")
107
+
108
+ idx = image[0]
109
+ img_name = image[-1]
110
+ assert img_name not in img_idx, "duplicate db image: " + img_name
111
+ img_idx[img_name] = idx # register image name
112
+
113
+ # current_points2D = {int(i): (float(x), float(y))
114
+ # for i, x, y in zip(points[2::3], points[0::3], points[1::3]) if i != '-1'}
115
+ img_infos[idx] = dict(
116
+ intrinsics=ins,
117
+ path=img_name,
118
+ frame_id=get_frame_number(img_name, cam_type),
119
+ cam_to_world=pose_from_qwxyz_txyz(image[1:-2]),
120
+ width=w,
121
+ height=h,
122
+ )
123
+
124
+ return img_idx, img_infos
125
+
126
+
127
+ def undistort_ins(intrinsics):
128
+ camera_type = intrinsics[0]
129
+
130
+ width = int(intrinsics[1])
131
+ height = int(intrinsics[2])
132
+ fx = intrinsics[3]
133
+ fy = intrinsics[4]
134
+ cx = intrinsics[5]
135
+ cy = intrinsics[6]
136
+ distortion = np.array(intrinsics[7:])
137
+
138
+ K = np.zeros([3, 3])
139
+ K[0, 0] = fx
140
+ K[0, 2] = cx
141
+ K[1, 1] = fy
142
+ K[1, 2] = cy
143
+ K[2, 2] = 1
144
+
145
+ K = colmap_to_opencv_intrinsics(K)
146
+ if camera_type == "OPENCV_FISHEYE":
147
+ assert len(distortion) == 4
148
+
149
+ new_K = cv2.fisheye.estimateNewCameraMatrixForUndistortRectify(
150
+ K,
151
+ distortion,
152
+ (width, height),
153
+ np.eye(3),
154
+ balance=0.0,
155
+ )
156
+ # Make the cx and cy to be the center of the image
157
+ new_K[0, 2] = width / 2.0
158
+ new_K[1, 2] = height / 2.0
159
+
160
+ map1, map2 = cv2.fisheye.initUndistortRectifyMap(
161
+ K, distortion, np.eye(3), new_K, (width, height), cv2.CV_32FC1
162
+ )
163
+ else:
164
+ new_K, _ = cv2.getOptimalNewCameraMatrix(
165
+ K, distortion, (width, height), 1, (width, height), True
166
+ )
167
+ map1, map2 = cv2.initUndistortRectifyMap(
168
+ K, distortion, np.eye(3), new_K, (width, height), cv2.CV_32FC1
169
+ )
170
+
171
+ # undistorted_image = cv2.remap(rgb, map1, map2, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
172
+ # undistorted_mask = cv2.remap(mask, map1, map2, interpolation=cv2.INTER_LINEAR,
173
+ # borderMode=cv2.BORDER_CONSTANT, borderValue=255)
174
+ new_K = opencv_to_colmap_intrinsics(new_K)
175
+ new_K = torch.tensor(new_K, dtype=torch.float32)
176
+ return width, height, new_K
177
+
178
+
179
+ def correspondenceGet(mesh, K, T, img_size, coords_gt):
180
+ height, width = img_size
181
+ if np.isnan(T).any() or np.isinf(T).any():
182
+ return None
183
+ depth = ct.raycast.mesh_to_im_depth(
184
+ mesh=mesh, K=K, T=np.linalg.inv(T), height=height, width=width
185
+ )
186
+ pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
187
+ pixel = pixel.reshape((-1, 2))
188
+ pixel = np.hstack((pixel, np.ones((pixel.shape[0], 1))))
189
+ depth = depth.reshape((-1, 1))
190
+ valid = ~np.isinf(depth).squeeze(-1)
191
+ coord = np.zeros_like(pixel, dtype=np.float32)
192
+ coord[valid] = depth[valid] * (np.linalg.inv(K) @ pixel[valid].T).T # coord_camera
193
+ coord[valid] = coord[valid] @ T[:3, :3].T + T[:3, 3] # column then row
194
+ pixel = pixel[valid]
195
+ coord = coord[valid]
196
+ if coord.shape[0] == 0:
197
+ return None
198
+ pixel = pixel[:, :2]
199
+ coord_dict = {"pixel": pixel, "coord": coord}
200
+ return coord_dict, depth
201
+
202
+
203
+ def correspondenceSave(
204
+ mesh, scene_dir, coords_gt, Ks, Ts, output_dir, img_size, parse_depths
205
+ ):
206
+ os.makedirs(output_dir / "correspondence", exist_ok=True)
207
+ scene_dir = Path(scene_dir)
208
+ index_gt = [
209
+ img_name.split(".")[0]
210
+ for img_name in os.listdir(str(scene_dir / "color"))
211
+ if img_name.endswith(".JPG")
212
+ ]
213
+ index_gt = sorted(index_gt, key=lambda x: int(x.split("DSC")[1]))
214
+
215
+ Ks = Ks[:, :3, :3]
216
+ coords_gt_ = coords_gt
217
+ pixels_ = []
218
+ coords_ = []
219
+
220
+ for i, (K, T) in enumerate(zip(Ks, Ts)):
221
+ try:
222
+ coord_dict, depth = correspondenceGet(mesh, K, T, img_size, coords_gt)
223
+ except:
224
+ print(output_dir)
225
+ if coord_dict is None:
226
+ correspondences = -np.ones((1, 3))
227
+ else:
228
+ pixels_ = coord_dict["pixel"]
229
+ coords_ = coord_dict["coord"]
230
+ tree = cKDTree(coords_gt_)
231
+ dis, idx = tree.query(coords_, k=1)
232
+ idx_valid = idx[dis < 0.01]
233
+ pixels_valid = pixels_[dis < 0.01]
234
+ correspondences = np.hstack((pixels_valid, idx_valid.reshape(-1, 1)))
235
+ np.save(
236
+ output_dir / "correspondence" / "{}.npy".format(index_gt[i]),
237
+ correspondences,
238
+ )
239
+ if parse_depths:
240
+ os.makedirs(output_dir / "depth", exist_ok=True)
241
+ depth = depth * 1000
242
+ depth = np.where(np.isinf(depth), 65535, depth).astype(np.uint16)
243
+ depth = depth.reshape(img_size)
244
+ cv2.imwrite(output_dir / "depth" / "{}.png".format(index_gt[i]), depth)
245
+
246
+
247
+ def handle_process(
248
+ name,
249
+ split,
250
+ dataset_path,
251
+ output_path,
252
+ pointclouds_root,
253
+ label_mapping,
254
+ class2idx,
255
+ ignore_index=-1,
256
+ frame_gap=20,
257
+ parse_pointclouds=False,
258
+ parse_depths=False,
259
+ ):
260
+ pc_output_path = output_path / split
261
+ im_output_path = output_path / "images" / split
262
+ scene_path = dataset_path / "data" / name
263
+ mesh_path = scene_path / "scans" / "mesh_aligned_0.05.ply"
264
+ segs_path = scene_path / "scans" / "segments.json"
265
+ anno_path = scene_path / "scans" / "segments_anno.json"
266
+ # load mesh vertices and colors
267
+ mesh = o3d.io.read_triangle_mesh(str(mesh_path))
268
+
269
+ if parse_pointclouds:
270
+ # extract mesh information
271
+ mesh.compute_vertex_normals(normalized=True)
272
+ coord = np.array(mesh.vertices).astype(np.float32)
273
+ color = (np.array(mesh.vertex_colors) * 255).astype(np.uint8)
274
+ normal = np.array(mesh.vertex_normals).astype(np.float32)
275
+ # extract superpoint information
276
+ if pointseg is not None:
277
+ vertices_sp = torch.from_numpy(np.array(mesh.vertices).astype(np.float32))
278
+ faces_sp = torch.from_numpy(np.array(mesh.triangles).astype(np.int64))
279
+ superpoint = pointseg.segment_mesh(vertices_sp, faces_sp).numpy()
280
+ else:
281
+ superpoint = None
282
+
283
+ # Save pointcloud data
284
+ os.makedirs(pc_output_path / name, exist_ok=True)
285
+ np.save(pc_output_path / name / "coord.npy", coord)
286
+ np.save(pc_output_path / name / "color.npy", color)
287
+ np.save(pc_output_path / name / "normal.npy", normal)
288
+ if superpoint is not None:
289
+ np.save(pc_output_path / "superpoint.npy", superpoint)
290
+
291
+ if split != "test":
292
+ # get label on vertices
293
+ # load segments = vertices per segment ID
294
+ with open(segs_path) as f:
295
+ segments = json.load(f)
296
+ # load anno = (instance, groups of segments)
297
+ with open(anno_path) as f:
298
+ anno = json.load(f)
299
+ seg_indices = np.array(segments["segIndices"], dtype=np.uint32)
300
+ num_vertices = len(seg_indices)
301
+ assert num_vertices == len(coord)
302
+ semantic_gt = np.ones((num_vertices, 3), dtype=np.int16) * ignore_index
303
+ instance_gt = np.ones((num_vertices, 3), dtype=np.int16) * ignore_index
304
+
305
+ # number of labels are used per vertex. initially 0
306
+ # increment each time a new label is added
307
+ instance_size = np.ones((num_vertices, 3), dtype=np.int16) * np.inf
308
+
309
+ # keep track of the size of the instance (#vertices) assigned to each vertex
310
+ # later, keep the label of the smallest instance for major label of vertices
311
+ # store inf initially so that we can pick the smallest instance
312
+ labels_used = np.zeros(num_vertices, dtype=np.int16)
313
+
314
+ for idx, instance in enumerate(anno["segGroups"]):
315
+ label = instance["label"]
316
+ instance["label_orig"] = label
317
+ # remap label
318
+ instance["label"] = label_mapping.get(label, None)
319
+ instance["label_index"] = class2idx.get(instance["label"], ignore_index)
320
+
321
+ if instance["label_index"] == ignore_index:
322
+ continue
323
+ # get all the vertices with segment index in this instance
324
+ # and max number of labels not yet applied
325
+ mask = np.isin(seg_indices, instance["segments"]) & (labels_used < 3)
326
+ size = mask.sum()
327
+ if size == 0:
328
+ continue
329
+
330
+ # get the position to add the label - 0, 1, 2
331
+ label_position = labels_used[mask]
332
+ semantic_gt[mask, label_position] = instance["label_index"]
333
+ # store all valid instance (include ignored instance)
334
+ instance_gt[mask, label_position] = instance["objectId"]
335
+ instance_size[mask, label_position] = size
336
+ labels_used[mask] += 1
337
+
338
+ # major label is the label of smallest instance for each vertex
339
+ # use major label for single class segmentation
340
+ # shift major label to the first column
341
+ mask = labels_used > 1
342
+ if mask.sum() > 0:
343
+ major_label_position = np.argmin(instance_size[mask], axis=1)
344
+
345
+ major_semantic_label = semantic_gt[mask, major_label_position]
346
+ semantic_gt[mask, major_label_position] = semantic_gt[:, 0][mask]
347
+ semantic_gt[:, 0][mask] = major_semantic_label
348
+
349
+ major_instance_label = instance_gt[mask, major_label_position]
350
+ instance_gt[mask, major_label_position] = instance_gt[:, 0][mask]
351
+ instance_gt[:, 0][mask] = major_instance_label
352
+
353
+ np.save(pc_output_path / name / "segment.npy", semantic_gt)
354
+ np.save(pc_output_path / name / "instance.npy", instance_gt)
355
+ else:
356
+ coord = np.load(pointclouds_root / split / name / "coord.npy")
357
+ rgb_path = scene_path / "dslr" / "undistorted_images"
358
+ colmap_path = scene_path / "dslr" / "colmap"
359
+ img_idxs, img_infos = load_sfm(str(colmap_path), cam_type="dslr")
360
+ rgb_path = sorted(
361
+ rgb_path.glob("*.JPG"), key=lambda x: int(str(x)[:-4].split("DSC")[1])
362
+ )[::frame_gap]
363
+ rgb_path = [str(path) for path in rgb_path]
364
+ color_output_path = im_output_path / name / "color"
365
+ os.makedirs(color_output_path, exist_ok=True)
366
+ for rgb_path_i in rgb_path:
367
+ shutil.copy(rgb_path_i, color_output_path)
368
+ imgnames = [path.split("/")[-1] for path in rgb_path]
369
+ img_idxs = [img_idxs[imgname] for imgname in imgnames]
370
+ img_infos = [img_infos[img_idx] for img_idx in img_idxs]
371
+ intrinsic_save_path = im_output_path / name / "intrinsic"
372
+ pose_save_path = im_output_path / name / "pose"
373
+ os.makedirs(intrinsic_save_path, exist_ok=True)
374
+ os.makedirs(pose_save_path, exist_ok=True)
375
+ Ks = []
376
+ Ts = []
377
+ for id, img_info in enumerate(img_infos):
378
+ np.save(intrinsic_save_path / f"{id}.npy", img_info["intrinsics"])
379
+ np.save(pose_save_path / f"{id}.npy", img_info["cam_to_world"])
380
+ Ks.append(img_info["intrinsics"])
381
+ Ts.append(img_info["cam_to_world"])
382
+ Ks = np.stack(Ks)
383
+ Ts = np.stack(Ts)
384
+
385
+ if len(rgb_path) == 0 or len(img_infos) == 0:
386
+ return
387
+ h = img_infos[0]["height"]
388
+ w = img_infos[0]["width"]
389
+ correspondenceSave(
390
+ mesh,
391
+ im_output_path / name,
392
+ coord,
393
+ Ks,
394
+ Ts,
395
+ im_output_path / name,
396
+ (h, w),
397
+ parse_depths,
398
+ )
399
+
400
+
401
+ def filter_map_classes(mapping, mapping_type):
402
+ if mapping_type == "semantic":
403
+ map_key = "semantic_map_to"
404
+ elif mapping_type == "instance":
405
+ map_key = "instance_map_to"
406
+ else:
407
+ raise NotImplementedError
408
+ # create a dict with classes to be mapped
409
+ # classes that don't have mapping are entered as x->x
410
+ # otherwise x->y
411
+ map_dict = OrderedDict()
412
+
413
+ for i in range(mapping.shape[0]):
414
+ row = mapping.iloc[i]
415
+ class_name = row["class"]
416
+ map_target = row[map_key]
417
+
418
+ # map to None or some other label -> don't add this class to the label list
419
+ try:
420
+ if len(map_target) > 0:
421
+ # map to None -> don't use this class
422
+ if map_target == "None":
423
+ pass
424
+ else:
425
+ # map to something else -> use this class
426
+ map_dict[class_name] = map_target
427
+ except TypeError:
428
+ # nan values -> no mapping, keep label as is
429
+ if class_name not in map_dict:
430
+ map_dict[class_name] = class_name
431
+
432
+ return map_dict
433
+
434
+
435
+ if __name__ == "__main__":
436
+ parser = argparse.ArgumentParser()
437
+ parser.add_argument(
438
+ "--dataset_root",
439
+ required=True,
440
+ help="Path to the ScanNet dataset containing scene folders",
441
+ )
442
+ parser.add_argument(
443
+ "--output_root",
444
+ required=True,
445
+ help="Output path where train/val folders will be located",
446
+ )
447
+ parser.add_argument(
448
+ "--pointclouds_root",
449
+ default="data/scannetpp",
450
+ type=str,
451
+ help="Input path where previous pointclouds folder located",
452
+ )
453
+ parser.add_argument(
454
+ "--ignore_index",
455
+ default=-1,
456
+ type=int,
457
+ help="Default ignore index.",
458
+ )
459
+ parser.add_argument(
460
+ "--frame_gap", default=20, type=int, help="Frame gap for processing"
461
+ )
462
+ parser.add_argument(
463
+ "--parse_pointclouds", action="store_true", help="Whether parse point clouds"
464
+ )
465
+ parser.add_argument(
466
+ "--parse_depths", action="store_true", help="Whether parse depths"
467
+ )
468
+ parser.add_argument(
469
+ "--num_workers",
470
+ default=mp.cpu_count(),
471
+ type=int,
472
+ help="Num workers for preprocessing.",
473
+ )
474
+ parser.add_argument(
475
+ "--thread_id",
476
+ default=0,
477
+ type=int,
478
+ help="Thread id for parallel processing",
479
+ )
480
+ config = parser.parse_args()
481
+
482
+ print("Loading meta data...")
483
+ config.dataset_root = Path(config.dataset_root)
484
+ config.output_root = Path(config.output_root)
485
+ config.pointclouds_root = Path(config.pointclouds_root)
486
+
487
+ train_list = np.loadtxt(
488
+ config.dataset_root / "splits" / "nvs_sem_train.txt",
489
+ dtype=str,
490
+ )
491
+ print("Num samples in training split:", len(train_list))
492
+
493
+ val_list = np.loadtxt(
494
+ config.dataset_root / "splits" / "nvs_sem_val.txt",
495
+ dtype=str,
496
+ )
497
+ print("Num samples in validation split:", len(val_list))
498
+
499
+ test_list = np.loadtxt(
500
+ config.dataset_root / "splits" / "sem_test.txt",
501
+ dtype=str,
502
+ )
503
+ print("Num samples in testing split:", len(test_list))
504
+
505
+ data_list = np.concatenate([train_list, val_list, test_list])
506
+ split_list = np.concatenate(
507
+ [
508
+ np.full_like(train_list, "train"),
509
+ np.full_like(val_list, "val"),
510
+ np.full_like(test_list, "test"),
511
+ ]
512
+ )
513
+
514
+ # Parsing label information and mapping
515
+ segment_class_names = np.loadtxt(
516
+ config.dataset_root / "metadata" / "semantic_benchmark" / "top100.txt",
517
+ dtype=str,
518
+ delimiter=".", # dummy delimiter to replace " "
519
+ )
520
+ print("Num classes in segment class list:", len(segment_class_names))
521
+
522
+ instance_class_names = np.loadtxt(
523
+ config.dataset_root / "metadata" / "semantic_benchmark" / "top100_instance.txt",
524
+ dtype=str,
525
+ delimiter=".", # dummy delimiter to replace " "
526
+ )
527
+ print("Num classes in instance class list:", len(instance_class_names))
528
+
529
+ label_mapping = pd.read_csv(
530
+ config.dataset_root / "metadata" / "semantic_benchmark" / "map_benchmark.csv"
531
+ )
532
+ label_mapping = filter_map_classes(label_mapping, mapping_type="semantic")
533
+ class2idx = {
534
+ class_name: idx for (idx, class_name) in enumerate(segment_class_names)
535
+ }
536
+ # Load scene paths
537
+
538
+ data_list_list = np.array_split(data_list, config.num_workers)
539
+ data_list_ = data_list_list[config.thread_id]
540
+ split_list_list = np.array_split(split_list, config.num_workers)
541
+ split_list_ = split_list_list[config.thread_id]
542
+ # Preprocess data.
543
+ print("Processing scenes...")
544
+ for data_list_i, split_list_i in zip(data_list_, split_list_):
545
+ handle_process(
546
+ data_list_i,
547
+ split_list_i,
548
+ config.dataset_root,
549
+ config.output_root,
550
+ config.pointclouds_root,
551
+ label_mapping,
552
+ class2idx,
553
+ config.ignore_index,
554
+ config.frame_gap,
555
+ config.parse_pointclouds,
556
+ config.parse_depths,
557
+ )
pointcept/datasets/preprocessing/concerto/scannetpp/preprocess_scannetpp.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ dataset_root=""
4
+ output_root=""
5
+ num_workers=16
6
+ parse_depths=false
7
+ parse_pointclouds=false
8
+
9
+ while getopts "d:o:n:pc" opt; do
10
+ case $opt in
11
+ d) dataset_root=$OPTARG ;;
12
+ o) output_root=$OPTARG ;;
13
+ n) num_workers=$OPTARG ;;
14
+ p) parse_depths=true ;;
15
+ c) parse_pointclouds=true ;;
16
+ *) echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"; exit 1 ;;
17
+ esac
18
+ done
19
+
20
+ if [ -z "$dataset_root" ] || [ -z "$output_root" ]; then
21
+ echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"
22
+ exit 1
23
+ fi
24
+
25
+ for i in $(seq 0 $((num_workers - 1))); do
26
+ cmd="python pointcept/datasets/preprocessing/concerto/scannetpp/preprocess_scannetpp.py --thread_id $i \
27
+ --num_workers $num_workers \
28
+ --dataset_root $dataset_root \
29
+ --output_root $output_root"
30
+
31
+ if $parse_depths; then
32
+ cmd="$cmd --parse_depths"
33
+ fi
34
+
35
+ if $parse_pointclouds; then
36
+ cmd="$cmd --parse_pointclouds"
37
+ fi
38
+
39
+ eval "$cmd &"
40
+ done
41
+
42
+ wait
pointcept/datasets/preprocessing/concerto/scannetpp/splits.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ im_path = os.path.join(dataset_path, "images")
9
+ pc_path = dataset_path
10
+ splits = ["train", "val", "test"]
11
+ split_path = os.path.join(dataset_path, "splits")
12
+ os.makedirs(split_path, exist_ok=True)
13
+ for split in splits:
14
+ im_split_path = os.path.join(im_path, split)
15
+ pc_split_path = os.path.join(pc_path, split).replace(
16
+ dataset_path, "data/scannetpp"
17
+ )
18
+ split_names = [f.name for f in os.scandir(im_split_path) if f.is_dir()]
19
+ split_dict = {}
20
+ for name in split_names:
21
+ im_split_name_path = os.path.join(im_split_path, name, "color")
22
+ co_split_name_path = os.path.join(im_split_path, name, "correspondence")
23
+ png_files = [
24
+ f for f in os.listdir(im_split_name_path) if f.endswith(".JPG")
25
+ ]
26
+ png_files = sorted(
27
+ png_files, key=lambda x: int(x.split(".")[0].split("DSC")[-1])
28
+ )
29
+ # Get the full paths of the .png files
30
+ png_file_paths = [
31
+ os.path.join(im_split_name_path, f).replace(
32
+ dataset_path, "data/scannetpp"
33
+ )
34
+ for f in png_files
35
+ ]
36
+ co_file_paths = [
37
+ os.path.join(co_split_name_path, f.replace(".JPG", ".npy")).replace(
38
+ dataset_path, "data/scannetpp"
39
+ )
40
+ for f in png_files
41
+ ]
42
+ for i in range(0, len(png_file_paths), 4):
43
+ split_dict[f"{name}_{i//4}"] = {}
44
+ split_dict[f"{name}_{i//4}"]["pointclouds"] = os.path.join(
45
+ pc_split_path, name
46
+ )
47
+ split_dict[f"{name}_{i//4}"]["images"] = png_file_paths[i : i + 4]
48
+ split_dict[f"{name}_{i//4}"]["correspondences"] = co_file_paths[
49
+ i : i + 4
50
+ ]
51
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
52
+ json.dump(split_dict, f, indent=4)
53
+
54
+
55
+ # Example usage
56
+ if __name__ == "__main__":
57
+ parser = argparse.ArgumentParser()
58
+ parser.add_argument(
59
+ "--dataset_root",
60
+ required=True,
61
+ help="Path to the ScanNet dataset containing scene folders",
62
+ )
63
+ config = parser.parse_args()
64
+ get_splits_paths(config.dataset_root)
pointcept/datasets/preprocessing/concerto/structured3d/preprocess_structured3d.py ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing Script for Structured3D
3
+
4
+ Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Yujia Zhang (yujia.zhang.cs@gmail.com)
5
+ Please cite our work if the code is helpful to you.
6
+ """
7
+
8
+ import argparse
9
+ import io
10
+ import os
11
+ from PIL import Image
12
+ import cv2
13
+ import zipfile
14
+ import numpy as np
15
+ import multiprocessing as mp
16
+
17
+ VALID_CLASS_IDS_25 = (
18
+ 1,
19
+ 2,
20
+ 3,
21
+ 4,
22
+ 5,
23
+ 6,
24
+ 7,
25
+ 8,
26
+ 9,
27
+ 11,
28
+ 14,
29
+ 15,
30
+ 16,
31
+ 17,
32
+ 18,
33
+ 19,
34
+ 22,
35
+ 24,
36
+ 25,
37
+ 32,
38
+ 34,
39
+ 35,
40
+ 38,
41
+ 39,
42
+ 40,
43
+ )
44
+ CLASS_LABELS_25 = (
45
+ "wall",
46
+ "floor",
47
+ "cabinet",
48
+ "bed",
49
+ "chair",
50
+ "sofa",
51
+ "table",
52
+ "door",
53
+ "window",
54
+ "picture",
55
+ "desk",
56
+ "shelves",
57
+ "curtain",
58
+ "dresser",
59
+ "pillow",
60
+ "mirror",
61
+ "ceiling",
62
+ "refrigerator",
63
+ "television",
64
+ "nightstand",
65
+ "sink",
66
+ "lamp",
67
+ "otherstructure",
68
+ "otherfurniture",
69
+ "otherprop",
70
+ )
71
+
72
+
73
+ def normal_from_cross_product(points_2d: np.ndarray) -> np.ndarray:
74
+ xyz_points_pad = np.pad(points_2d, ((0, 1), (0, 1), (0, 0)), mode="symmetric")
75
+ xyz_points_ver = (xyz_points_pad[:, :-1, :] - xyz_points_pad[:, 1:, :])[:-1, :, :]
76
+ xyz_points_hor = (xyz_points_pad[:-1, :, :] - xyz_points_pad[1:, :, :])[:, :-1, :]
77
+ xyz_normal = np.cross(xyz_points_hor, xyz_points_ver)
78
+ xyz_dist = np.linalg.norm(xyz_normal, axis=-1, keepdims=True)
79
+ xyz_normal = np.divide(
80
+ xyz_normal, xyz_dist, out=np.zeros_like(xyz_normal), where=xyz_dist != 0
81
+ )
82
+ return xyz_normal
83
+
84
+
85
+ class Structured3DReader:
86
+ def __init__(self, files):
87
+ super().__init__()
88
+ if isinstance(files, str):
89
+ files = [files]
90
+ self.readers = [zipfile.ZipFile(f, "r") for f in files]
91
+ self.names_mapper = dict()
92
+ for idx, reader in enumerate(self.readers):
93
+ for name in reader.namelist():
94
+ self.names_mapper[name] = idx
95
+
96
+ def filelist(self):
97
+ return list(self.names_mapper.keys())
98
+
99
+ def listdir(self, dir_name):
100
+ dir_name = dir_name.lstrip(os.path.sep).rstrip(os.path.sep)
101
+ file_list = list(
102
+ np.unique(
103
+ [
104
+ f.replace(dir_name + os.path.sep, "", 1).split(os.path.sep)[0]
105
+ for f in self.filelist()
106
+ if f.startswith(dir_name + os.path.sep)
107
+ ]
108
+ )
109
+ )
110
+ if "" in file_list:
111
+ file_list.remove("")
112
+ return file_list
113
+
114
+ def read(self, file_name):
115
+ split = self.names_mapper[file_name]
116
+ return self.readers[split].read(file_name)
117
+
118
+ def read_camera(self, camera_path):
119
+ z2y_top_m = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]], dtype=np.float32)
120
+ cam_extr = np.fromstring(self.read(camera_path), dtype=np.float32, sep=" ")
121
+ cam_t = np.matmul(z2y_top_m, cam_extr[:3] / 1000)
122
+ if cam_extr.shape[0] > 3:
123
+ cam_front, cam_up = cam_extr[3:6], cam_extr[6:9]
124
+ cam_n = np.cross(cam_front, cam_up)
125
+ cam_r = np.stack((cam_front, cam_up, cam_n), axis=1).astype(np.float32)
126
+ cam_r = np.matmul(z2y_top_m, cam_r)
127
+ cam_f = cam_extr[9:11]
128
+ else:
129
+ cam_r = np.eye(3, dtype=np.float32)
130
+ cam_f = None
131
+ return cam_r, cam_t, cam_f
132
+
133
+ def read_depth(self, depth_path):
134
+ depth = cv2.imdecode(
135
+ np.frombuffer(self.read(depth_path), np.uint8), cv2.IMREAD_UNCHANGED
136
+ )[..., np.newaxis]
137
+ depth[depth == 0] = 65535
138
+ return depth
139
+
140
+ def read_color(self, color_path):
141
+ color = cv2.imdecode(
142
+ np.frombuffer(self.read(color_path), np.uint8), cv2.IMREAD_UNCHANGED
143
+ )[..., :3][..., ::-1]
144
+ return color
145
+
146
+ def read_segment(self, segment_path):
147
+ segment = np.array(Image.open(io.BytesIO(self.read(segment_path))))[
148
+ ..., np.newaxis
149
+ ]
150
+ return segment
151
+
152
+
153
+ def parse_scene(
154
+ scene,
155
+ dataset_root,
156
+ output_root,
157
+ ignore_index=-1,
158
+ grid_size=None,
159
+ fuse_prsp=True,
160
+ fuse_pano=True,
161
+ parse_pointclouds=True,
162
+ parse_depths=True,
163
+ vis=False,
164
+ ):
165
+ assert fuse_prsp or fuse_pano
166
+ pc_output_root = output_root
167
+ im_output_root = os.path.join(output_root, "images")
168
+ reader = Structured3DReader(
169
+ [
170
+ os.path.join(dataset_root, f)
171
+ for f in os.listdir(dataset_root)
172
+ if f.endswith(".zip")
173
+ ]
174
+ )
175
+ scene_id = int(os.path.basename(scene).split("_")[-1])
176
+ if scene_id < 3000:
177
+ split = "train"
178
+ elif 3000 <= scene_id < 3250:
179
+ split = "val"
180
+ else:
181
+ split = "test"
182
+
183
+ print(f"Processing: {scene} in {split}")
184
+
185
+ rooms = reader.listdir(os.path.join("Structured3D", scene, "2D_rendering"))
186
+ for room in rooms:
187
+ im_save_path = os.path.join(
188
+ im_output_root, split, os.path.basename(scene), f"room_{room}"
189
+ )
190
+ pc_save_path = os.path.join(
191
+ pc_output_root, split, os.path.basename(scene), f"room_{room}"
192
+ )
193
+ if os.path.exists(im_save_path) and os.path.exists(pc_save_path):
194
+ print(f"exist {split}/{os.path.basename(scene)}/room_{room}")
195
+ continue
196
+
197
+ room_path = os.path.join("Structured3D", scene, "2D_rendering", room)
198
+ coord_list = list()
199
+ color_list = list()
200
+ normal_list = list()
201
+ segment_list = list()
202
+ prsp_list = list()
203
+ pano_list = list()
204
+ prsp_depth_list = list()
205
+ pano_depth_list = list()
206
+ prsp_correspondence_list = list()
207
+ pano_correspondence_list = list()
208
+ Ks_list = list()
209
+ Ts_list = list()
210
+ if fuse_prsp and scene:
211
+ prsp_path = os.path.join(room_path, "perspective", "full")
212
+ frames = reader.listdir(prsp_path)
213
+
214
+ for frame_id, frame in enumerate(frames):
215
+ try:
216
+ cam_r, cam_t, cam_f = reader.read_camera(
217
+ os.path.join(prsp_path, frame, "camera_pose.txt")
218
+ )
219
+ depth = reader.read_depth(
220
+ os.path.join(prsp_path, frame, "depth.png")
221
+ )
222
+ color = reader.read_color(
223
+ os.path.join(prsp_path, frame, "rgb_rawlight.png")
224
+ )
225
+ segment = reader.read_segment(
226
+ os.path.join(prsp_path, frame, "semantic.png")
227
+ )
228
+ except:
229
+ print(
230
+ f"Skipping {scene}_room{room}_frame{frame} perspective view due to loading error"
231
+ )
232
+ else:
233
+ fx, fy = cam_f
234
+ height, width = depth.shape[0], depth.shape[1]
235
+ pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
236
+ pixel = pixel.reshape((-1, 2))
237
+ pixel = np.hstack((pixel, np.ones((pixel.shape[0], 1))))
238
+ k = np.diag([1.0, 1.0, 1.0])
239
+
240
+ k[0, 2] = width / 2
241
+ k[1, 2] = height / 2
242
+
243
+ k[0, 0] = k[0, 2] / np.tan(fx)
244
+ k[1, 1] = k[1, 2] / np.tan(fy)
245
+ coord = (
246
+ depth.reshape((-1, 1)) * (np.linalg.inv(k) @ pixel.T).T
247
+ ).reshape(height, width, 3)
248
+ coord = coord @ np.array([[0, 0, 1], [0, -1, 0], [1, 0, 0]])
249
+ normal = normal_from_cross_product(coord)
250
+
251
+ # Filtering invalid points
252
+ view_dist = np.maximum(
253
+ np.linalg.norm(coord, axis=-1, keepdims=True), float(10e-5)
254
+ )
255
+ cosine_dist = np.sum(
256
+ (coord * normal / view_dist), axis=-1, keepdims=True
257
+ )
258
+ cosine_dist = np.abs(cosine_dist)
259
+ mask = ((cosine_dist > 0.15) & (depth < 65535) & (segment > 0))[
260
+ ..., 0
261
+ ].reshape(-1)
262
+
263
+ coord = np.matmul(coord / 1000, cam_r.T) + cam_t
264
+ normal = normal_from_cross_product(coord)
265
+
266
+ T = np.eye(4)
267
+ T[:3, :3] = cam_r
268
+ T[:3, 3] = cam_t
269
+
270
+ Ks_list.append(k)
271
+ Ts_list.append(T)
272
+
273
+ pixel[:, 2] = pixel[:, 2] * frame_id
274
+ pixel_valid = pixel[mask]
275
+
276
+ if sum(mask) > 0:
277
+ prsp_depth_list.append(depth)
278
+ coord_list.append(coord.reshape(-1, 3)[mask])
279
+ color_list.append(color.reshape(-1, 3)[mask])
280
+ normal_list.append(normal.reshape(-1, 3)[mask])
281
+ segment_list.append(segment.reshape(-1, 1)[mask])
282
+ prsp_list.append(color)
283
+ prsp_correspondence_list.append(pixel_valid)
284
+ else:
285
+ print(
286
+ f"Skipping {scene}_room{room}_frame{frame} perspective view due to all points are filtered out"
287
+ )
288
+
289
+ if fuse_pano:
290
+ pano_path = os.path.join(room_path, "panorama")
291
+ try:
292
+ _, cam_t, _ = reader.read_camera(
293
+ os.path.join(pano_path, "camera_xyz.txt")
294
+ )
295
+ depth = reader.read_depth(os.path.join(pano_path, "full", "depth.png"))
296
+ color = reader.read_color(
297
+ os.path.join(pano_path, "full", "rgb_rawlight.png")
298
+ )
299
+ segment = reader.read_segment(
300
+ os.path.join(pano_path, "full", "semantic.png")
301
+ )
302
+ except:
303
+ print(f"Skipping {scene}_room{room} panorama view due to loading error")
304
+ else:
305
+ p_h, p_w = depth.shape[:2]
306
+ pixel = np.transpose(np.indices((p_w, p_h)), (2, 1, 0))
307
+ pixel = pixel.reshape((-1, 2))
308
+ p_a = np.arange(p_w, dtype=np.float32) / p_w * 2 * np.pi - np.pi
309
+ p_b = np.arange(p_h, dtype=np.float32) / p_h * np.pi * -1 + np.pi / 2
310
+ p_a = np.tile(p_a[None], [p_h, 1])[..., np.newaxis]
311
+ p_b = np.tile(p_b[:, None], [1, p_w])[..., np.newaxis]
312
+ p_a_sin, p_a_cos, p_b_sin, p_b_cos = (
313
+ np.sin(p_a),
314
+ np.cos(p_a),
315
+ np.sin(p_b),
316
+ np.cos(p_b),
317
+ )
318
+ x = depth * p_a_cos * p_b_cos
319
+ y = depth * p_b_sin
320
+ z = depth * p_a_sin * p_b_cos
321
+ coord = np.concatenate([x, y, z], axis=-1) / 1000
322
+ normal = normal_from_cross_product(coord)
323
+
324
+ # Filtering invalid points
325
+ view_dist = np.maximum(
326
+ np.linalg.norm(coord, axis=-1, keepdims=True), float(10e-5)
327
+ )
328
+ cosine_dist = np.sum(
329
+ (coord * normal / view_dist), axis=-1, keepdims=True
330
+ )
331
+ cosine_dist = np.abs(cosine_dist)
332
+ mask = ((cosine_dist > 0.15) & (depth < 65535) & (segment > 0))[
333
+ ..., 0
334
+ ].reshape(-1)
335
+ coord = coord + cam_t
336
+
337
+ pixel = np.hstack((pixel, -np.ones((pixel.shape[0], 1))))
338
+ pixel_valid = pixel[mask]
339
+
340
+ if sum(mask) > 0:
341
+ pano_depth_list.append(depth)
342
+ coord_list.append(coord.reshape(-1, 3)[mask])
343
+ color_list.append(color.reshape(-1, 3)[mask])
344
+ normal_list.append(normal.reshape(-1, 3)[mask])
345
+ segment_list.append(segment.reshape(-1, 1)[mask])
346
+ pano_list.append(color)
347
+ pano_correspondence_list.append(pixel_valid)
348
+ else:
349
+ print(
350
+ f"Skipping {scene}_room{room} panorama view due to all points are filtered out"
351
+ )
352
+ if len(prsp_correspondence_list) > 0 and len(pano_correspondence_list) > 0:
353
+ coord = np.concatenate(coord_list, axis=0)
354
+ coord = coord @ np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]])
355
+ color = np.concatenate(color_list, axis=0)
356
+ normal = np.concatenate(normal_list, axis=0)
357
+ normal = normal @ np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]])
358
+ segment = np.concatenate(segment_list, axis=0)
359
+ segment25 = np.ones_like(segment, dtype=np.int64) * ignore_index
360
+ Ks_list = np.stack(Ks_list, axis=0)
361
+ Ts_list = np.stack(Ts_list, axis=0)
362
+ Ts_list = Ts_list @ np.array(
363
+ [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]
364
+ )
365
+ for idx, value in enumerate(VALID_CLASS_IDS_25):
366
+ mask = np.all(segment == value, axis=-1)
367
+ segment25[mask] = idx
368
+ correspondence = np.concatenate(prsp_correspondence_list, axis=0)
369
+ correspondence = np.concatenate(
370
+ [correspondence, pano_correspondence_list[0]], axis=0
371
+ )
372
+ data_dict = dict(
373
+ coord=coord.astype(np.float32),
374
+ color=color.astype(np.uint8),
375
+ normal=normal.astype(np.float32),
376
+ segment=segment25.astype(np.int16),
377
+ correspondence=correspondence.astype(np.int32),
378
+ )
379
+
380
+ # exclude ignore, wall, floor, ceiling
381
+ valid = np.sum(~np.isin(data_dict["segment"], [-1, 0, 1, 16]))
382
+ if valid == 0:
383
+ print(
384
+ f"Skip {scene}_room{room} due to no effective points (exclude wall, floor, ceiling)"
385
+ )
386
+ continue
387
+
388
+ # Grid sampling data
389
+ if grid_size is not None:
390
+ grid_coord = np.floor(coord / grid_size).astype(int)
391
+ _, idx = np.unique(grid_coord, axis=0, return_index=True)
392
+ # coord = coord[idx]
393
+ for key in data_dict.keys():
394
+ data_dict[key] = data_dict[key][idx]
395
+
396
+ correspondence = data_dict["correspondence"]
397
+ correspondence = np.concatenate(
398
+ [correspondence, np.arange(correspondence.shape[0])[:, None]], axis=1
399
+ )
400
+ frame_id_list = np.unique(correspondence[:, 2])
401
+ frame_id_list = frame_id_list[frame_id_list != -1]
402
+ pano_correspondence_list = [
403
+ correspondence[correspondence[:, 2] == -1][:, [0, 1, 3]]
404
+ ]
405
+ prsp_correspondence_list = [
406
+ correspondence[correspondence[:, 2] == i][:, [0, 1, 3]]
407
+ for i in frame_id_list
408
+ ]
409
+ if parse_pointclouds:
410
+ os.makedirs(pc_save_path, exist_ok=True)
411
+ # Save data
412
+ for key in data_dict.keys():
413
+ if key in ["correspondence"]:
414
+ continue
415
+ np.save(os.path.join(pc_save_path, f"{key}.npy"), data_dict[key])
416
+
417
+ os.makedirs(im_save_path, exist_ok=True)
418
+ if fuse_prsp:
419
+ prsp_save_path = os.path.join(im_save_path, "color", "prsp")
420
+ prsp_correspondence_save_path = os.path.join(
421
+ im_save_path, "correspondence", "prsp_correspondence"
422
+ )
423
+ Ks_save_path = os.path.join(im_save_path, "intrinsic")
424
+ Ts_save_path = os.path.join(im_save_path, "pose")
425
+ os.makedirs(prsp_correspondence_save_path, exist_ok=True)
426
+ os.makedirs(prsp_save_path, exist_ok=True)
427
+ os.makedirs(Ks_save_path, exist_ok=True)
428
+ os.makedirs(Ts_save_path, exist_ok=True)
429
+ if parse_depths:
430
+ prsp_depth_save_path = os.path.join(im_save_path, "depth", "prsp")
431
+ os.makedirs(prsp_depth_save_path, exist_ok=True)
432
+ for idx in range(len(prsp_list)):
433
+ prsp_img_path = os.path.join(prsp_save_path, f"{idx}.png")
434
+ cv2.imwrite(prsp_img_path, prsp_list[idx][..., ::-1])
435
+ if parse_depths:
436
+ prsp_depth_img_path = os.path.join(
437
+ prsp_depth_save_path, f"{idx}.png"
438
+ )
439
+ cv2.imwrite(prsp_depth_img_path, prsp_depth_list[idx])
440
+ np.save(
441
+ os.path.join(prsp_correspondence_save_path, f"{idx}.npy"),
442
+ prsp_correspondence_list[idx],
443
+ )
444
+ np.save(
445
+ os.path.join(Ks_save_path, f"{idx}.npy"),
446
+ Ks_list[idx],
447
+ )
448
+ np.save(
449
+ os.path.join(Ts_save_path, f"{idx}.npy"),
450
+ Ts_list[idx],
451
+ )
452
+
453
+ if fuse_pano:
454
+ pano_save_path = os.path.join(im_save_path, "color", "pano")
455
+ pano_correspondence_save_path = os.path.join(
456
+ im_save_path, "correspondence", "pano_correspondence"
457
+ )
458
+ os.makedirs(pano_save_path, exist_ok=True)
459
+ os.makedirs(pano_correspondence_save_path, exist_ok=True)
460
+ if parse_depths:
461
+ pano_depth_save_path = os.path.join(im_save_path, "depth", "pano")
462
+ os.makedirs(pano_depth_save_path, exist_ok=True)
463
+ for idx in range(len(pano_list)):
464
+ pano_img_path = os.path.join(pano_save_path, f"{idx}.png")
465
+ cv2.imwrite(pano_img_path, pano_list[idx][..., ::-1])
466
+ if parse_depths:
467
+ pano_depth_img_path = os.path.join(
468
+ pano_depth_save_path, f"{idx}.png"
469
+ )
470
+ cv2.imwrite(pano_depth_img_path, pano_depth_list[idx])
471
+ for idx, pano_correspondence in enumerate(pano_correspondence_list):
472
+ np.save(
473
+ os.path.join(pano_correspondence_save_path, f"{idx}.npy"),
474
+ pano_correspondence,
475
+ )
476
+
477
+ if vis:
478
+ from pointcept.utils.visualization import save_point_cloud
479
+
480
+ os.makedirs("./vis", exist_ok=True)
481
+ save_point_cloud(
482
+ coord, color / 255, f"./vis/{scene}_room{room}_color.ply"
483
+ )
484
+ save_point_cloud(
485
+ coord, (normal + 1) / 2, f"./vis/{scene}_room{room}_normal.ply"
486
+ )
487
+ else:
488
+ print(f"Skipping {scene}_room{room} due to no valid points")
489
+
490
+
491
+ if __name__ == "__main__":
492
+ parser = argparse.ArgumentParser()
493
+ parser.add_argument(
494
+ "--dataset_root",
495
+ required=True,
496
+ help="Path to the Structured3D dataset containing scene folders.",
497
+ )
498
+ parser.add_argument(
499
+ "--output_root",
500
+ required=True,
501
+ help="Output path where train/val folders will be located.",
502
+ )
503
+ parser.add_argument(
504
+ "--num_workers",
505
+ default=mp.cpu_count(),
506
+ type=int,
507
+ help="Num workers for preprocessing.",
508
+ )
509
+ parser.add_argument(
510
+ "--thread_id",
511
+ default=0,
512
+ type=int,
513
+ help="thread_id.",
514
+ )
515
+ parser.add_argument(
516
+ "--grid_size", default=None, type=float, help="Grid size for grid sampling."
517
+ )
518
+ parser.add_argument("--ignore_index", default=-1, type=float, help="Ignore index.")
519
+ parser.add_argument(
520
+ "--fuse_prsp", action="store_true", help="Whether fuse perspective view."
521
+ )
522
+ parser.add_argument(
523
+ "--fuse_pano", action="store_true", help="Whether fuse panorama view."
524
+ )
525
+ parser.add_argument(
526
+ "--parse_pointclouds", action="store_true", help="Whether parse point clouds"
527
+ )
528
+ parser.add_argument(
529
+ "--parse_depths", action="store_true", help="Whether parse depths"
530
+ )
531
+ config = parser.parse_args()
532
+
533
+ reader = Structured3DReader(
534
+ [
535
+ os.path.join(config.dataset_root, f)
536
+ for f in os.listdir(config.dataset_root)
537
+ if f.endswith(".zip")
538
+ ]
539
+ )
540
+
541
+ scenes_list = reader.listdir("Structured3D")
542
+ scenes_list = sorted(scenes_list)
543
+ split_scenes_list = np.array_split(scenes_list, config.num_workers)
544
+ split_scenes_list_ = split_scenes_list[config.thread_id]
545
+
546
+ for scenes_list_i in split_scenes_list_:
547
+ parse_scene(
548
+ scenes_list_i,
549
+ config.dataset_root,
550
+ config.output_root,
551
+ config.ignore_index,
552
+ config.grid_size,
553
+ config.fuse_prsp,
554
+ config.fuse_pano,
555
+ config.parse_pointclouds,
556
+ config.parse_depths,
557
+ )
pointcept/datasets/preprocessing/concerto/structured3d/preprocess_structured3d.sh ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ dataset_root=""
4
+ output_root=""
5
+ num_workers=16
6
+ parse_depths=false
7
+ parse_pointclouds=false
8
+
9
+ while getopts "d:o:n:pc" opt; do
10
+ case $opt in
11
+ d) dataset_root=$OPTARG ;;
12
+ o) output_root=$OPTARG ;;
13
+ n) num_workers=$OPTARG ;;
14
+ p) parse_depths=true ;;
15
+ c) parse_pointclouds=true ;;
16
+ *) echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"; exit 1 ;;
17
+ esac
18
+ done
19
+
20
+ if [ -z "$dataset_root" ] || [ -z "$output_root" ]; then
21
+ echo "Usage: $0 -d <dataset_root> -o <output_root> [-n <num_workers>] [-p] [-c]"
22
+ exit 1
23
+ fi
24
+
25
+ for i in $(seq 0 $((num_workers - 1))); do
26
+ cmd="python pointcept/datasets/preprocessing/concerto/structured3d/preprocess_structured3d.py --thread_id $i \
27
+ --num_workers $num_workers \
28
+ --dataset_root $dataset_root \
29
+ --output_root $output_root \
30
+ --fuse_prsp --fuse_pano"
31
+
32
+ if $parse_depths; then
33
+ cmd="$cmd --parse_depths"
34
+ fi
35
+
36
+ if $parse_pointclouds; then
37
+ cmd="$cmd --parse_pointclouds"
38
+ fi
39
+
40
+ eval "$cmd &"
41
+ done
42
+
43
+ wait
pointcept/datasets/preprocessing/concerto/structured3d/splits.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def get_splits_paths(dataset_path):
7
+ # Get the names of all subfolders in the given folder
8
+ im_path = os.path.join(dataset_path, "images")
9
+ pc_path = dataset_path
10
+ splits = ["train", "val"]
11
+ split_path = os.path.join(dataset_path, "splits")
12
+ os.makedirs(split_path, exist_ok=True)
13
+ for split in splits:
14
+ im_split_path = os.path.join(im_path, split)
15
+ pc_split_path = os.path.join(pc_path, split).replace(
16
+ dataset_path, "data/structured3d"
17
+ )
18
+ split_names = [f.name for f in os.scandir(im_split_path) if f.is_dir()]
19
+ split_dict = {}
20
+ for name in split_names:
21
+ cam_split_name_path = os.path.join(im_split_path, name)
22
+ cam_names = [f.name for f in os.scandir(cam_split_name_path) if f.is_dir()]
23
+ for cam_name in cam_names:
24
+ im_split_name_path = os.path.join(
25
+ cam_split_name_path, cam_name, "color", "prsp"
26
+ )
27
+ co_split_name_path = os.path.join(
28
+ cam_split_name_path,
29
+ cam_name,
30
+ "correspondence",
31
+ "prsp_correspondence",
32
+ )
33
+ png_files = [
34
+ f for f in os.listdir(im_split_name_path) if f.endswith(".png")
35
+ ]
36
+ png_files = sorted(png_files, key=lambda x: int(x.split(".")[0]))
37
+ # Get the full paths of the .png files
38
+ png_file_paths = [
39
+ os.path.join(im_split_name_path, f).replace(
40
+ dataset_path, "data/structured3d"
41
+ )
42
+ for f in png_files
43
+ ]
44
+ co_file_paths = [
45
+ os.path.join(co_split_name_path, f.replace(".png", ".npy")).replace(
46
+ dataset_path, "data/structured3d"
47
+ )
48
+ for f in png_files
49
+ ]
50
+ split_dict[f"{name}_{cam_name}"] = {}
51
+ split_dict[f"{name}_{cam_name}"]["pointclouds"] = os.path.join(
52
+ pc_split_path, name, cam_name
53
+ )
54
+ split_dict[f"{name}_{cam_name}"]["images"] = png_file_paths
55
+ split_dict[f"{name}_{cam_name}"]["correspondences"] = co_file_paths
56
+ with open(os.path.join(split_path, f"{split}.json"), "w") as f:
57
+ json.dump(split_dict, f, indent=4)
58
+
59
+
60
+ # Example usage
61
+ if __name__ == "__main__":
62
+ parser = argparse.ArgumentParser()
63
+ parser.add_argument(
64
+ "--dataset_root",
65
+ required=True,
66
+ help="Path to the Structured3D dataset containing scene folders",
67
+ )
68
+ config = parser.parse_args()
69
+ get_splits_paths(config.dataset_root)