Initial release: PT-v3m1 segmentation model (BIMStruct3D pipeline)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +3 -0
- README.md +154 -0
- configs/model_config.py +114 -0
- pointcept/__init__.py +0 -0
- pointcept/datasets/__init__.py +9 -0
- pointcept/datasets/aeo.py +29 -0
- pointcept/datasets/builder.py +15 -0
- pointcept/datasets/cap3d.py +245 -0
- pointcept/datasets/dataloader.py +205 -0
- pointcept/datasets/defaults.py +676 -0
- pointcept/datasets/hk.py +94 -0
- pointcept/datasets/hm3d.py +45 -0
- pointcept/datasets/modelnet.py +156 -0
- pointcept/datasets/nuscenes.py +583 -0
- pointcept/datasets/partnet.py +79 -0
- pointcept/datasets/partnete.py +111 -0
- pointcept/datasets/preprocessing/arkitscenes/preprocess_arkitscenes_mesh.py +93 -0
- pointcept/datasets/preprocessing/concerto/arkitscenes/preprocess_arkitscenes.py +343 -0
- pointcept/datasets/preprocessing/concerto/arkitscenes/preprocess_arkitscenes.sh +42 -0
- pointcept/datasets/preprocessing/concerto/arkitscenes/rotation.py +140 -0
- pointcept/datasets/preprocessing/concerto/arkitscenes/splits.py +64 -0
- pointcept/datasets/preprocessing/concerto/cap3d/preprocess_cap3d.py +261 -0
- pointcept/datasets/preprocessing/concerto/cap3d/preprocess_cap3d.sh +35 -0
- pointcept/datasets/preprocessing/concerto/cap3d/splits.py +76 -0
- pointcept/datasets/preprocessing/concerto/graspnet/preprocess_graspnet_poses.py +56 -0
- pointcept/datasets/preprocessing/concerto/graspnet/splits.py +91 -0
- pointcept/datasets/preprocessing/concerto/hk/hkremote_download.sh +60 -0
- pointcept/datasets/preprocessing/concerto/hk/preprocess_hk.py +110 -0
- pointcept/datasets/preprocessing/concerto/hk/preprocess_hk.sh +30 -0
- pointcept/datasets/preprocessing/concerto/hk/splits.py +35 -0
- pointcept/datasets/preprocessing/concerto/hm3d/__init__.py +0 -0
- pointcept/datasets/preprocessing/concerto/hm3d/common/utils.py +113 -0
- pointcept/datasets/preprocessing/concerto/hm3d/preprocessed_hm3d.py +617 -0
- pointcept/datasets/preprocessing/concerto/hm3d/preprocessed_hm3d.sh +42 -0
- pointcept/datasets/preprocessing/concerto/hm3d/splits.py +60 -0
- pointcept/datasets/preprocessing/concerto/nuscenes/preprocess_nuscenes_info.py +687 -0
- pointcept/datasets/preprocessing/concerto/re10k/preprocess_re10k.py +556 -0
- pointcept/datasets/preprocessing/concerto/re10k/splits.py +57 -0
- pointcept/datasets/preprocessing/concerto/s3dis/preprocess_s3dis.py +472 -0
- pointcept/datasets/preprocessing/concerto/s3dis/preprocess_s3dis.sh +45 -0
- pointcept/datasets/preprocessing/concerto/s3dis/splits.py +74 -0
- pointcept/datasets/preprocessing/concerto/scannet/preprocess_scannet.py +574 -0
- pointcept/datasets/preprocessing/concerto/scannet/preprocess_scannet.sh +42 -0
- pointcept/datasets/preprocessing/concerto/scannet/splits.py +62 -0
- pointcept/datasets/preprocessing/concerto/scannetpp/preprocess_scannetpp.py +557 -0
- pointcept/datasets/preprocessing/concerto/scannetpp/preprocess_scannetpp.sh +42 -0
- pointcept/datasets/preprocessing/concerto/scannetpp/splits.py +64 -0
- pointcept/datasets/preprocessing/concerto/structured3d/preprocess_structured3d.py +557 -0
- pointcept/datasets/preprocessing/concerto/structured3d/preprocess_structured3d.sh +43 -0
- 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 |
+
[](https://creativecommons.org/licenses/by-nc-sa/4.0/) [](https://arxiv.org/abs/2604.24311) [](https://ec-3.org/publication/ec32026_197/) [](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)
|